LLM Integration#
discopt includes optional LLM-powered features for model explanation, natural language formulation, infeasibility diagnosis, conversational model building, and more.
These features use litellm as a universal adapter supporting 100+ LLM providers (Anthropic, OpenAI, Google, Ollama, etc.).
Important
LLM features are purely advisory — they never affect solver correctness.
All formulations pass through model.validate() before use.
Setup#
Install the LLM dependencies:
pip install discopt[llm]
Set your API key for your chosen provider:
# Anthropic (default)
export ANTHROPIC_API_KEY="sk-ant-..."
# Or use any other provider:
export OPENAI_API_KEY="sk-..."
export DISCOPT_LLM_MODEL="openai/gpt-4o"
# Or use a local model (no API key needed):
export DISCOPT_LLM_MODEL="ollama/llama3"
import discopt.modeling as dm
import numpy as np
from discopt.llm import is_available
print(f"LLM features available: {is_available()}")
LLM features available: False
1. Model Serialization#
The serializer module converts discopt models and results into structured text
that LLMs can understand. This is used internally by all LLM features.
from discopt.llm.serializer import serialize_model
# Build a simple model
m = dm.Model("facility_location")
x = m.continuous("shipment", shape=(3, 5), lb=0, ub=200)
y = m.binary("open", shape=(3,))
supply = np.array([100, 150, 200])
demand = np.array([80, 60, 70, 40, 50])
cost = np.random.RandomState(42).rand(3, 5) * 100
m.minimize(
dm.sum(lambda i: dm.sum(lambda j: cost[i, j] * x[i, j], over=range(5)), over=range(3))
+ dm.sum(lambda i: 1000 * y[i], over=range(3))
)
# Demand satisfaction
for j in range(5):
m.subject_to(
dm.sum(lambda i: x[i, j], over=range(3)) >= demand[j],
name=f"demand_{j}",
)
# Supply capacity (only if open)
for i in range(3):
m.subject_to(
dm.sum(lambda j: x[i, j], over=range(5)) <= supply[i] * y[i],
name=f"capacity_{i}",
)
m.validate()
print(serialize_model(m))
# Model: facility_location
## Variables
- shipment (continuous, shape=(3, 5), lb=array((3, 5)), ub=array((3, 5)))
- open (binary, shape=(3,), lb=array((3,)), ub=array((3,)))
## Objective
minimize (Σ[3 terms] + Σ[3 terms])
## Constraints (8 total)
- [demand_0] (80 - Σ[3 terms]) <= 0.0
- [demand_1] (60 - Σ[3 terms]) <= 0.0
- [demand_2] (70 - Σ[3 terms]) <= 0.0
- [demand_3] (40 - Σ[3 terms]) <= 0.0
- [demand_4] (50 - Σ[3 terms]) <= 0.0
- [capacity_0] (Σ[5 terms] - (100 * open[3][0])) <= 0.0
- [capacity_1] (Σ[5 terms] - (150 * open[3][1])) <= 0.0
- [capacity_2] (Σ[5 terms] - (200 * open[3][2])) <= 0.0
## Statistics
- Total variables: 18
- Continuous: 15
- Integer/Binary: 3
- Constraints: 8
2. Solver Strategy Advisor#
The advisor analyzes model structure and recommends solver parameters using
rule-based heuristics. With llm=True, it augments suggestions with LLM analysis.
The structure questions it asks are answered by the same passes the solver uses —
classify_nonlinear_terms for products, classify_problem for the class — not by
inspecting the printed form of a constraint. That matters: until #1362 both this
function and the reformulation analyzer below decided “has bilinear terms” with
" * " in str(constraint), which matches every constant * variable. The
facility-location model above is a MILP with no bilinear terms at all, and both
recommended a 4-partition piecewise-McCormick relaxation for it.
from discopt.llm.advisor import suggest_solver_params
params = suggest_solver_params(m)
print("Recommended solver parameters:")
for key, value in params.items():
print(f" {key}: {value}")
Recommended solver parameters:
reasoning: Using POUNCE, the default Rust interior-point engine
nlp_solver: pounce
cutting_planes: False
batch_size: 16
gap_tolerance: 0.0001
time_limit: 300
3. Auto-Reformulation Analysis#
Analyze a model for reformulation opportunities: big-M tightening, McCormick partitioning, symmetry breaking, bound tightening.
Run on two models, so the empty answer is meaningful: a linear one, which should suggest no McCormick partitioning, and a genuinely bilinear one, which should. An analyzer that returns the same list for both is not analyzing anything.
from discopt.llm.reformulation import analyze_reformulations
def report(label, model):
suggestions = analyze_reformulations(model)
print(f"{label}: {len(suggestions)} reformulation suggestion(s)")
for i, s in enumerate(suggestions, 1):
print(f" {i}. [{s.category}] {s.description}")
return {s.category for s in suggestions}
linear_cats = report("facility_location (linear)", m)
print()
# The same question, on a model that really does have a product of two variables.
m_bilinear = dm.Model("blending")
a = m_bilinear.continuous("a", lb=0, ub=4)
b = m_bilinear.continuous("b", lb=0, ub=4)
sel = m_bilinear.binary("sel")
m_bilinear.minimize(a + b + sel)
m_bilinear.subject_to(a * b >= 2, name="quality")
bilinear_cats = report("blending (bilinear)", m_bilinear)
assert "mccormick_partitioning" not in linear_cats, linear_cats
assert "mccormick_partitioning" in bilinear_cats, bilinear_cats
facility_location (linear): 0 reformulation suggestion(s)
blending (bilinear): 1 reformulation suggestion(s)
1. [mccormick_partitioning] Found bilinear terms in 1 constraints — if the default relaxation leaves a large gap, try solver="amp" (adaptive partitioning of the McCormick relaxation)
4. Result Explanation (requires API key)#
After solving, use explain(llm=True) to get a rich, context-aware explanation.
Falls back to a template string if the LLM is unavailable.
result = m.solve()
print(result.explain(llm=True))
5. Infeasibility Diagnosis (requires API key)#
When a model is infeasible, the diagnosis module identifies likely conflicting constraints and suggests minimal relaxations.
from discopt.llm.diagnosis import diagnose_infeasibility
# Create an intentionally infeasible model
m_bad = dm.Model("infeasible")
x = m_bad.continuous("x", lb=0, ub=10)
m_bad.minimize(x)
m_bad.subject_to(x >= 20, name="impossible")
m_bad.subject_to(x <= 5, name="tight_upper")
result = m_bad.solve()
if result.status == "infeasible":
print(diagnose_infeasibility(m_bad, result))
6. Conversational Model Building (requires API key)#
discopt.chat() starts an interactive session where you describe optimization
problems in natural language and the LLM builds, modifies, solves, and explains
models iteratively.
import discopt
session = discopt.chat()
session.send("I need to schedule 5 workers across 7 shifts")
session.send("Each shift needs at least 2 workers")
session.send("Workers can't work more than 5 shifts")
session.send("Minimize total labor cost")
session.send("Solve it and explain the result")
session.close()
7. Natural Language Formulation (requires API key)#
from_description() uses structured tool calling to build type-safe models
from natural language — no arbitrary code generation.
model = dm.from_description(
"Minimize total shipping cost from 3 warehouses to 5 customers. "
"Each warehouse has limited supply. Each customer demand must be met.",
data={
"supply": [100, 150, 200],
"demand": [80, 60, 70, 40, 50],
"costs": np.random.rand(3, 5) * 100,
},
)
print(model.summary())
Provider Configuration#
discopt uses litellm for universal LLM access. Configure your provider:
Provider |
Env Variable |
Model String |
|---|---|---|
Anthropic |
|
|
OpenAI |
|
|
|
|
|
Ollama |
(local, no key) |
|
# Use a specific model for any LLM feature:
result.explain(llm=True, model="openai/gpt-4o")
# Or set globally:
import os
os.environ["DISCOPT_LLM_MODEL"] = "ollama/llama3"