Skip to content

This feature is currently in Preview.

Create a Problem object

Turn a Model into a solver-backed decision problem by creating a Problem. This guide covers choosing a default numeric type, creating the Problem, and using display() to confirm it starts empty before you add variables, requirements, or an objective.

A Problem is a container for the variables, requirements, and objective that make up one decision problem. It is associated with a Model, and it compiles your model definitions into something a solver backend can execute.

When you create a Problem, you choose a default numeric type that affects how the problem is interpreted by solvers and which solver families are compatible. Use this table to choose the default numeric type passed to Problem(model, ...):

What to useWhen to use it
Problem(m, Float)You want linear, mixed-integer, or nonlinear optimization with solvers like "highs", "gurobi", or "ipopt".
Problem(m, Integer)You want constraint-programming style solves with "minizinc".
  • The numeric type you pick here is a default.
  • You still control whether a specific decision variable is continuous, integer, or binary when you declare it with Problem.solve_for().

Create an instance of the Problem class to create a decision problem associated with a model:

from relationalai.semantics import Float, Integer, Model
from relationalai.semantics.reasoners.prescriptive import Problem
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.
41 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 Problem object for solver-backed decision problems
p = Problem(m, Float)

In this example:

  • Model("ShiftAssignment") creates a model to hold all schema, data, and reasoning artifacts for this problem.
  • Worker and Shift are the two concepts in the model, and both are identified by an integer id property.
  • Worker-Shift availability, costs, and required coverage are represented as relationships and properties.
  • Base facts are loaded from inline data to populate the inputs to the decision problem.
  • Problem(m, Float) creates a decision problem associated with the model m.
  • At this point you have created a Problem, but you have not declared any decision variables, requirements, or objectives.

Use Problem.display() to see an overview of the current state of the problem. This is a good check to run after each major formulation step:

from relationalai.semantics import Float, Integer, Model
from relationalai.semantics.reasoners.prescriptive import Problem
m = Model("ShiftAssignment")
# Declare the model's schema
49 collapsed lines
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.
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 Problem object for solver-backed decision problems
p = Problem(m, Float)
p.display()
  • When you called .display() immediately after creating the Problem, you will see an “empty” problem with no decision variables, requirements, or objectives.
  • This is expected, since you have not yet declared any of those things.