Skip to content

This feature is currently in Preview.

The Ipopt Solver Backend

Ipopt is an open-source solver for large-scale nonlinear optimization. Use it to solve continuous nonlinear programs (NLP) defined using RelationalAI’s SolverModel API.

Ipopt supports the following problem types:

Problem TypeSupported
Linear Programs (LP)
Mixed-Integer Linear Programs (MILP)
Quadratic Programs (QP)
Quadratically Constrained Programs (QCP)
Nonlinear Programs (NLP)
Constraint Programming (CP)
Discrete Variables
Continuous Variables

Ipopt is bundled with RelationalAI and does not require a license key or additional setup.

DetailInfo
Version3.14.4
License TypeEclipse Public License (Free and Open Source)

To use Ipopt with RelationalAI, you can define a prescriptive model using the SolverModel API and specify "ipopt" as the solver backend:

regional_water_allocation.py
import relationalai as rai
from relationalai.std import alias
from relationalai.experimental import solvers
# Create a RAI model.
model = rai.Model("RegionalWaterAllocation")
67 collapsed lines
# Declare water distribution stage type.
WaterSource = model.Type("WaterSource")
Region = model.Type("Region")
Consumer = model.Type("Consumer")
Consumer.region.declare() # Region where the consumer is located.
Consumer.demand.declare() # Water demand of the consumer.
# Declare Pipeline type to connect stages.
Pipeline = model.Type("Pipeline")
Pipeline.source.declare()
Pipeline.destination.declare()
Pipeline.capacity.declare()
# Define water sources.
with model.rule(dynamic=True):
WaterSource.add(name="Reservoir")
# Define regions.
for region in [
{"id": 1, "name": "North"},
{"id": 2, "name": "South"},
{"id": 3, "name": "East"},
{"id": 4, "name": "West"},
]:
with model.rule():
Region.add(id=region["id"]).set(name=region["name"])
# Define consumers.
for consumer in [
{"id": 1, "region_id": 1, "name": "Residential", "demand": 150.0},
{"id": 2, "region_id": 1, "name": "Industrial", "demand": 60.0},
{"id": 3, "region_id": 1, "name": "Farms", "demand": 40.0},
{"id": 4, "region_id": 2, "name": "Residential", "demand": 80.0},
{"id": 5, "region_id": 2, "name": "Industrial", "demand": 140.0},
{"id": 6, "region_id": 2, "name": "Farms", "demand": 50.0},
{"id": 7, "region_id": 3, "name": "Residential", "demand": 90.0},
{"id": 8, "region_id": 3, "name": "Industrial", "demand": 180.0},
{"id": 9, "region_id": 4, "name": "Residential", "demand": 40.0},
{"id": 10, "region_id": 4, "name": "Industrial", "demand": 30.0},
{"id": 11, "region_id": 4, "name": "Farms", "demand": 200.0},
]:
with model.rule():
region = Region(id=consumer["region_id"])
Consumer.add(id=consumer["id"]).set(
name=consumer["name"],
region=region,
demand=consumer["demand"]
)
# Define pipelines from the reservoir to each region.
for region_id, capacity in [(1, 260.0), (2, 300.0), (3, 200.0), (4, 220.0)]:
with model.rule():
source = WaterSource(name="Reservoir")
region = Region(id=region_id)
Pipeline.add(source=source, destination=region).set(capacity=capacity)
# Define pipelines between consumers and their regions.
for consumer_id, capacity in [
(1, 150.0), (2, 60.0), (3, 40.0), # North
(4, 80.0), (5, 140.0), (6, 50.0), # South
(7, 90.0), (8, 180.0), # East
(9, 40.0), (10, 30.0), (11, 200.0) # West
]:
with model.rule():
consumer = Consumer(id=consumer_id)
region = consumer.region
Pipeline.add(source=region, destination=consumer).set(capacity=capacity)
# Create a solver model.
solver_model = solvers.SolverModel(model)
48 collapsed lines
# Specify variables, constraints, and objective.
# Define continuous variables for each pipeline to represent it's flow.
with model.rule():
pipe = Pipeline()
solver_model.variable(
pipe,
name_args=["pipe", pipe.source.name, pipe.destination.name],
lower=0,
upper=pipe.capacity,
)
# Define a constraint to ensure flow conservation in each region.
# NOTE: `solvers.operators()` overrides infix operators like `==` to build
# solver expressions instead of filter expressions.
with solvers.operators():
region = Region()
flow_in = solvers.sum(Pipeline(destination=region), per=[region])
flow_out = solvers.sum(Pipeline(source=region), per=[region])
solver_model.constraint(
flow_in == flow_out,
name_args=["preserve_flow", region.name]
)
# Define a constraint satisfy at least half of high-demand consumers' demand.
with model.rule():
consumer = Consumer()
consumer.demand >= 100.0 # Filter for high-demand consumers.
pipe = Pipeline(source=consumer.region, destination=consumer)
with solvers.operators():
solver_model.constraint(
pipe >= .5 * consumer.demand,
name_args=["satisfy_consumer", consumer.id]
)
# Define a constraint to limit the total flow from the reservoir.
with solvers.operators():
total_flow = solvers.sum(Pipeline(source=WaterSource(name="Reservoir")))
solver_model.constraint(
total_flow <= 1000.0, # Max flow from the reservoir
name_args=["max_reservoir_flow"]
)
# Define the objective to minimize unmet demand.
with solvers.operators():
consumer = Consumer()
pipe = Pipeline(source=consumer.region, destination=consumer)
unmet_demand_per_consumer = consumer.demand - solvers.sum(pipe, per=[consumer])
solver_model.min_objective(solvers.sum(unmet_demand_per_consumer))
# Create an Ipopt Solver instance.
solver = solvers.Solver("ipopt")
# Solve the model.
solver_model.solve(solver)
# View the solution's objective value.
with model.query() as select:
response = select(solver_model.objective_value)
print(f"\n\nUnmet demand (optimized): {response.results.iloc[0, 0]}")
# View the optimized flow for each pipeline.
with model.query() as select:
pipe = Pipeline()
response = select(
alias(pipe.source.name, "source_name"),
alias(pipe.destination.name, "destination_name"),
solver_model.value(pipe)
)
print("\n\nOptimized pipeline flows:")
print(response.results)

In addition to the standard solver options, Ipopt supports other options, like s_max or warm_start_init_point, that can be set when solving a prescriptive model using the SolverModel API.

To set these options, you can pass them as keyword arguments to a SolverModel object’s .solve() method:

regional_water_allocation.py
import relationalai as rai
from relationalai.std import alias
from relationalai.experimental import solvers
# Create a RAI model.
model = rai.Model("RegionalWaterAllocation")
119 collapsed lines
# Declare water distribution stage type.
WaterSource = model.Type("WaterSource")
Region = model.Type("Region")
Consumer = model.Type("Consumer")
Consumer.region.declare() # Region where the consumer is located.
Consumer.demand.declare() # Water demand of the consumer.
# Declare Pipeline type to connect stages.
Pipeline = model.Type("Pipeline")
Pipeline.source.declare()
Pipeline.destination.declare()
Pipeline.capacity.declare()
# Define water sources.
with model.rule(dynamic=True):
WaterSource.add(name="Reservoir")
# Define regions.
for region in [
{"id": 1, "name": "North"},
{"id": 2, "name": "South"},
{"id": 3, "name": "East"},
{"id": 4, "name": "West"},
]:
with model.rule():
Region.add(id=region["id"]).set(name=region["name"])
# Define consumers.
for consumer in [
{"id": 1, "region_id": 1, "name": "Residential", "demand": 150.0},
{"id": 2, "region_id": 1, "name": "Industrial", "demand": 60.0},
{"id": 3, "region_id": 1, "name": "Farms", "demand": 40.0},
{"id": 4, "region_id": 2, "name": "Residential", "demand": 80.0},
{"id": 5, "region_id": 2, "name": "Industrial", "demand": 140.0},
{"id": 6, "region_id": 2, "name": "Farms", "demand": 50.0},
{"id": 7, "region_id": 3, "name": "Residential", "demand": 90.0},
{"id": 8, "region_id": 3, "name": "Industrial", "demand": 180.0},
{"id": 9, "region_id": 4, "name": "Residential", "demand": 40.0},
{"id": 10, "region_id": 4, "name": "Industrial", "demand": 30.0},
{"id": 11, "region_id": 4, "name": "Farms", "demand": 200.0},
]:
with model.rule():
region = Region(id=consumer["region_id"])
Consumer.add(id=consumer["id"]).set(
name=consumer["name"],
region=region,
demand=consumer["demand"]
)
# Define pipelines from the reservoir to each region.
for region_id, capacity in [(1, 260.0), (2, 300.0), (3, 200.0), (4, 220.0)]:
with model.rule():
source = WaterSource(name="Reservoir")
region = Region(id=region_id)
Pipeline.add(source=source, destination=region).set(capacity=capacity)
# Define pipelines between consumers and their regions.
for consumer_id, capacity in [
(1, 150.0), (2, 60.0), (3, 40.0), # North
(4, 80.0), (5, 140.0), (6, 50.0), # South
(7, 90.0), (8, 180.0), # East
(9, 40.0), (10, 30.0), (11, 200.0) # West
]:
with model.rule():
consumer = Consumer(id=consumer_id)
region = consumer.region
Pipeline.add(source=region, destination=consumer).set(capacity=capacity)
# Create a solver model.
solver_model = solvers.SolverModel(model)
# Specify variables, constraints, and objective.
# Define continuous variables for each pipeline to represent it's flow.
with model.rule():
pipe = Pipeline()
solver_model.variable(
pipe,
name_args=["pipe", pipe.source.name, pipe.destination.name],
lower=0,
upper=pipe.capacity,
)
# Define a constraint to ensure flow conservation in each region.
# NOTE: `solvers.operators()` overrides infix operators like `==` to build
# solver expressions instead of filter expressions.
with solvers.operators():
region = Region()
flow_in = solvers.sum(Pipeline(destination=region), per=[region])
flow_out = solvers.sum(Pipeline(source=region), per=[region])
solver_model.constraint(
flow_in == flow_out,
name_args=["preserve_flow", region.name]
)
# Define a constraint satisfy at least half of high-demand consumers' demand.
with model.rule():
consumer = Consumer()
consumer.demand >= 100.0 # Filter for high-demand consumers.
pipe = Pipeline(source=consumer.region, destination=consumer)
with solvers.operators():
solver_model.constraint(
pipe >= .5 * consumer.demand,
name_args=["satisfy_consumer", consumer.id]
)
# Define a constraint to limit the total flow from the reservoir.
with solvers.operators():
total_flow = solvers.sum(Pipeline(source=WaterSource(name="Reservoir")))
solver_model.constraint(
total_flow <= 1000.0, # Max flow from the reservoir
name_args=["max_reservoir_flow"]
)
# Define the objective to minimize unmet demand.
with solvers.operators():
consumer = Consumer()
pipe = Pipeline(source=consumer.region, destination=consumer)
unmet_demand_per_consumer = consumer.demand - solvers.sum(pipe, per=[consumer])
solver_model.min_objective(solvers.sum(unmet_demand_per_consumer))
# Create a HiGHS Solver instance.
solver = solvers.Solver("ipopt")
# Solve the model.
solver_model.solve(solver, s_max=1000, warm_start_init_point="yes")
  • Some RAI operators, like solvers.if_then_else(), aren’t supported.
  • Ipopt requires a single, scalar objective, even if multi-objective support is added to RAI in the future.