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()
ParameterTypeMeaning
base_urlstrthe 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_keystr, optionala key you already have; leave it out and your first call sets one up
timeoutfloat, keyword-onlyper-request timeout in seconds (default 60)
key_pathstr or Path, keyword-onlywhere the free key is cached (default ~/.cache/quicopt/free_key, or $XDG_CACHE_HOME/quicopt/free_key)
cachebool, keyword-onlypass 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
ParameterTypeMeaning
modelPyomo or MathOpt modelthe model to solve, passed directly — the client converts it for you (see Modeling front-ends)
gzipbool, keyword-onlycompress 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

FieldTypeMeaning
statusstroptimal · heuristic · iteration_limit · infeasible · unbounded
objectivefloat or Noneobjective value; None when there is no solution
feasiblebool or Nonewhether the returned solution satisfies all constraints; None where that's undefined (e.g. an unconstrained heuristic)
solutiondict[str, float]variable name → value; empty for infeasible/unbounded
model_classstr or Nonehow the service classified the model (lp, qp, milp, qubo, …)
solve_time_secondsfloatsolve wall time
displaystrthe 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, or local on 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); feasible is None when 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 like heuristic.
  • infeasible — no assignment satisfies the constraints; objective is None, solution is empty. This is a regular status, not an error — check result.status rather 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
FieldMeaning
reasona stable snake_case code, e.g. unsupported_model for a problem class that isn't solved yet
displaythe framed, human-readable message — including how to get in touch
status_codethe 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
MethodMeaning
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.

Talk to us →