The Gurobi Solver Backend
Gurobi is a state-of-the-art commercial solver for mathematical optimization.
Use it to solve complex linear, mixed-integer, and quadratic programs using RelationalAI’s SolverModel
API.
Capabilities
Section titled “Capabilities”Gurobi supports the following problem types:
Problem Type | Supported |
---|---|
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 |
Version and Licensing
Section titled “Version and Licensing”Gurobi is a commercial solver that requires a valid license. You must install and configure your Gurobi license before using it with RelationalAI.
Detail | Info |
---|---|
Version | 12.0.1 |
License Type | Commercial (License required) |
See the Gurobi docs for details on obtaining a license.
Required Setup
Section titled “Required Setup”To use Gurobi with RelationalAI, you must store your Gurobi license key in Snowflake and configure network access for the RAI Native App to authenticate the license.
Follow these steps to set up Gurobi in RelationalAI:
-
Get a Gurobi license key.
Your license key has the following form:
WLSACCESSID=...WLSSECRET=...LICENSEID=...Visit the Gurobi website to obtain a license, if necessary.
-
Store your license key in Snowflake.
First, choose the database and schema to store your Gurobi license key, or create a new database and schema if needed:
CREATE DATABASE IF NOT EXISTS solvers;CREATE SCHEMA IF NOT EXISTS solvers.secrets;Then create a Snowflake secret to store your Gurobi license key:
CREATE SECRET IF NOT EXISTS solvers.secrets.gurobi_licenseTYPE=generic_string-- Replace <MY_GUROBI_LICENSE_KEY> with your actual Gurobi license key.SECRET_STRING='<MY_GUROBI_LICENSE_KEY>'GRANT USAGE ON DATABASE solvers TO APPLICATION RELATIONALAI;GRANT USAGE ON SCHEMA solvers.secrets TO APPLICATION RELATIONALAI;GRANT READ ON SECRET solvers.secrets.gurobi_license TO APPLICATION RELATIONALAI; -
Allow RAI to authenticate your license key.
The RAI Native App needs to access the Gurobi license server to authenticate your license key, so you must create a network rule and an external access integration in Snowflake.
First, choose the database and schema to store the network rule and integration. Here, we create a schema named
networking
in thesolvers
database created in the previous step:CREATE SCHEMA IF NOT EXISTS solvers.networking;Then create an the network rule and external access integration:
CREATE OR REPLACE NETWORK RULE solvers.networking.gurobi_network_ruleMODE = EGRESSTYPE = HOST_PORTVALUE_LIST = ('token.gurobi.com:443');CREATE OR REPLACE EXTERNAL ACCESS INTEGRATION gurobi_integrationALLOWED_NETWORK_RULES = (solvers.networking.gurobi_network_rule)ENABLED = true;GRANT USAGE ON SCHEMA solvers.networking TO APPLICATION RELATIONALAI;GRANT USAGE ON INTEGRATION gurobi_integration TO APPLICATION RELATIONALAI;
To use Gurobi in RelationalAI:
-
Enable the Gurobi solver backend.
Add the following to the bottom of your
raiconfig.toml
file:raiconfig.toml active_profile = "default"[profile.default]9 collapsed linesplatform = "snowflake"user = "c.janeway@starfleet.org"password = "C0ffee>PrimeDirective"account = "ncc74656-voyager"role = "CAPTAIN"warehouse = "MAIN"rai_app_name = "relationalai"engine = "catherine_janeway"engine_size = "HIGHMEM_X64_S"[experimental.solvers]gurobi.enabled=truegurobi.license_secret_name="<MY_GUROBI_SECRET_NAME>" # E.g., "solvers.secrets.gurobi_license"gurobi.external_access_integration="<MY_GUROBI_INTEGRATION_NAME>" # E.g., "gurobi_integration"See Required Setup for details on setting up the license secret and external access integration.
-
Create a
Solver
object with"gurobi"
.In your Python code, pass the string
"gurobi"
to theSolver
constructor to create a Gurobi solver instance:regional_water_allocation.py import relationalai as raifrom relationalai.std import aliasfrom 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.regionPipeline.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 reservoirname_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("gurobi")# 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)
Solver Options
Section titled “Solver Options”In addition to the standard solver options, Gurobi supports other options, like Method
or MemLimit
, that can be set when solving a prescriptive model using the SolverModel
API.
To set these options, pass them as a keyword argument to a SolverModel
object’s .solve()
method while preserving the PascalCase naming convention used by Gurobi:
import relationalai as raifrom relationalai.std import aliasfrom 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 Gurobi Solver instance.solver = solvers.Solver("gurobi")
# Solve the model.solver_model.solve(solver, Method=1, MemLimit=1024)
Limitations
Section titled “Limitations”- Some RAI operators, like
solvers.if_then_else()
, aren’t supported. - Advanced Gurobi features, like callbacks, aren’t supported.
- Although Gurobi supports multi-objective optimization, this capability is not yet exposed by RelationalAI.