The HiGHS Solver Backend
HiGHS is a fast, open-source solver for linear optimization.
Use it to solve mixed-integer linear problems and problems with convex quadratic objectives using RelationalAI’s SolverModel
API.
Capabilities
Section titled “Capabilities”HiGHS 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”HiGHS is bundled with RelationalAI and does not require a license key or additional setup.
Detail | Info |
---|---|
Version | 1.7.1 |
License Type | MIT License (Free and Open Source) |
To use HiGHS with RelationalAI, you can define a prescriptive model using the SolverModel
API and specify "highs"
as the solver backend:
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.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 a HiGHS Solver instance.solver = solvers.Solver("highs")
# 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, HiGHS supports other options, like threads
or random_seed
, that can be set when solving a prescriptive model using the SolverModel
API.
To set these options, pass them as keyword arguments to a SolverModel
object’s .solve()
method:
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 HiGHS Solver instance.solver = solvers.Solver("highs")
# Solve the model.solver_model.solve(solver, threads=4, random_seed=8675)
Limitations
Section titled “Limitations”- Some RAI operators, like
solvers.if_then_else()
, aren’t supported. - Some advanced HiGHS features, like callbacks, aren’t supported.
- Although HiGHS supports blended and lexicographic multi-objective optimization, this capability is not yet exposed by RAI.