API reference
The supported way to call Quicopt is the official Python client:
pip install "quicopt[mathopt]" # or quicopt[pyomo]
This page describes it in full — Client, solve(), the Result it returns,
error handling, and the async job API for long solves. Package links:
PyPI ·
source & docs (Apache-2.0).
Client(base_url=DEFAULT_BASE_URL, api_key=None, *, timeout=60.0, key_path=None, cache=True)
A connection to the Quicopt service. Construct it with no arguments and it points at the free tier; if you don't have a key, your first call sets one up.
from quicopt import Client
client = Client()
| Parameter | Type | Meaning |
|---|---|---|
base_url | str | the service address; defaults to DEFAULT_BASE_URL, the free tier at https://try.quicoptapi.pgi.fz-juelich.de. Pass a URL to reach a different server |
api_key | str, optional | a key you already have; leave it out and your first call sets one up |
timeout | float, keyword-only | per-request timeout in seconds (default 60) |
key_path | str or Path, keyword-only | where the free key is cached (default ~/.cache/quicopt/free_key, or $XDG_CACHE_HOME/quicopt/free_key) |
cache | bool, keyword-only | pass False to hold the key in memory only, reading and writing no file |
That key is cached on your machine, so it comes back on your later calls, and
on later runs too — not just later calls on the same Client. You stay the
same caller from one script to the next, and you never copy a key anywhere. Read
it back from client.api_key if you want it.
Two cases are worth knowing. A key you pass in yourself is used as it stands and
never written to the cache, so authenticating with a specific key cannot
overwrite the free key of whoever runs your code. And where the home directory
doesn't survive the run — CI, a container, Colab — point QUICOPT_KEY_PATH (or
key_path) at storage that does, so the same key is still there the next time
your job starts.
client.solve(model, *, gzip=False)
Solves a model synchronously and returns a Result. This
is the call you'll use most:
result = client.solve(model)
print(result.status, result.objective)
print(result.display) # the framed, ready-to-print summary
| Parameter | Type | Meaning |
|---|---|---|
model | Pyomo or MathOpt model | the model to solve, passed directly — the client converts it for you (see Modeling front-ends) |
gzip | bool, keyword-only | compress the request body — useful for large models |
If the service declines a request — for example a problem class that isn't
solved yet — solve() raises QuicoptError. A declined
request never returns a half-filled result.
The Result object
| Field | Type | Meaning |
|---|---|---|
status | str | optimal · heuristic · iteration_limit · infeasible · unbounded |
objective | float or None | objective value; None when there is no solution |
feasible | bool or None | whether the returned solution satisfies all constraints; None where that's undefined (e.g. an unconstrained heuristic) |
solution | dict[str, float] | variable name → value; empty for infeasible/unbounded |
model_class | str or None | how the service classified the model (lp, qp, milp, qubo, …) |
solve_time_seconds | float | solve wall time |
display | str | the framed console report — print it and you get the result view from the examples, shots block included |
With the MathOpt front-end, solution keys are your variable names from the
model — name your variables and you can read the result without bookkeeping.
The Pyomo front-end currently returns generic x1, x2, … keys.
The shots block
result.display opens with a shots block. The service takes several shots at
your model, and the block is one row per shot: the value it reached, how long it
took, and ◀ best on the one that won.
├── shots
│ ├── 1 · primal-dual · global 42 0.0s ◀ best
│ ├── 2 · primal-dual · global 42 0.01s
│ └── 3 · primal 42 0.01s
Each row is labelled method · certificate. A primal shot only ever
holds a solution; a primal-dual shot also maintains a bound on what the
best solution could be, which is what puts it in a position to certify. The
certificate is what the shot ended up proving: global for a proven
optimum, local for a point proven optimal in its own neighbourhood, and no
certificate at all where it proved neither — a good value, not a guarantee.
Read the block when you want to know how much weight the answer carries;
result.status is the one-word summary of it. How many rows you get depends on
the model, so don't count on a fixed number.
Status semantics
optimal— the value is certified, and the shots block shows how far the proof reaches:global, orlocalon a non-convex model where local optimality is what can be proven.feasible: True.heuristic— the best value found across the shots, with nothing in the block certifying it (typical for QUBO);feasibleisNonewhen there are no constraints to check.iteration_limit— the solver stopped at its iteration cap and returns the best point found (can happen on hard NLPs); treat it likeheuristic.infeasible— no assignment satisfies the constraints;objectiveisNone,solutionis empty. This is a regular status, not an error — checkresult.statusrather than catching exceptions. There's a worked example.unbounded— the objective can be improved without limit; also a regular status.
Errors: QuicoptError
Anything the service declines raises QuicoptError with three useful fields:
from quicopt import Client, QuicoptError
try:
result = client.solve(model)
except QuicoptError as e:
print(e.reason) # stable code, e.g. "unsupported_model"
print(e.display) # the service's readable message, with a help contact
| Field | Meaning |
|---|---|
reason | a stable snake_case code, e.g. unsupported_model for a problem class that isn't solved yet |
display | the framed, human-readable message — including how to get in touch |
status_code | the HTTP status behind it |
Remember: infeasible and unbounded are results, not errors — you get a
normal Result for those.
Async jobs: client.submit()
For a long solve, don't block — submit the model and poll a job handle:
job = client.submit(model) # returns immediately
job.status() # {"status": "queued" | "running" | "done", ...}
result = job.result() # polls until done, returns the same Result as solve()
print(job.log()) # the solver log so far
job.delete() # remove the job and its stored result
| Method | Meaning |
|---|---|
job.status() | the job's state and a tail of its log |
job.result(wait=True, timeout=120.0, poll=0.5) | the finished Result; with wait=False it fetches once and raises if the job isn't done yet |
job.log() | the job's full plain-text log |
job.delete() | delete the job and its stored result on the server |
submit() takes the same models and the same gzip option as solve().
Beyond the free tier
The free tier is a one-time entry point to try the API. Questions about the client, or real models you want to try Quicopt on? Just ask:
Questions? Talk to us.
Whether something's unclear or you want to try Quicopt on your real models — tell us what you're optimizing.