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: True
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.
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 pure-JAX IPM (default); Enabling 4-partition McCormick (bilinear terms detected)
nlp_solver: ipm
partitions: 4
cutting_planes: False
branching_policy: fractional
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.
from discopt.llm.reformulation import analyze_reformulations
suggestions = analyze_reformulations(m)
print(f"Found {len(suggestions)} reformulation suggestions:\n")
for i, s in enumerate(suggestions, 1):
print(f"{i}. [{s.category}] {s.description}")
print(f" Impact: {s.impact}\n")
Found 1 reformulation suggestions:
1. [mccormick_partitioning] Found bilinear terms in 3 constraints — use partitions=4 or partitions=8 for tighter McCormick relaxations
Impact: Piecewise McCormick can significantly tighten relaxations for bilinear/pooling problems
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"