QuicoptClient.jl
JuMP.Model ──import_model──▶ Program ──encode──▶ bytes ──solve/HTTP──▶ result
the same model what travels
as plain data over the networkQuicoptClient — Module
QuicoptClient — the Julia client for the Quicopt optimization service.
Write a model in JuMP, hand it to solve, and read the answer back. Underneath, import_model turns the JuMP model into a Program — what a model is to Quicopt: variables, expressions and constraints as data, in a published, language-neutral form — encode turns that into bytes, and solve sends them over HTTP. There is no solver here; the service does the solving.
The Program types are generated from the published program.proto via ProtoBuf.jl (src/proto/, regenerated by gen/gen.jl), so every dependency is public (JuMP, ProtoBuf, HTTP, JSON3). See the README.
Installation
using Pkg
Pkg.add("QuicoptClient")All dependencies are public — JuMP, ProtoBuf, HTTP, JSON3. No solver and no service internals ship with the client.
Quickstart
using QuicoptClient, JuMP
m = Model()
@variable(m, 0 <= x <= 4)
@variable(m, 0 <= y <= 4)
@objective(m, Max, 3x + 2y)
@constraint(m, x + y <= 5)
result = solve(m) # POSTs to the service; prints the result banner
@show result.status result.objective result.solutionThe first call mints a free API key, cached at $XDG_CACHE_HOME/quicopt/free_key (~/.cache/… by default, 0o600) and replayed on every later call — including from later runs, so one caller keeps one key. Point QUICOPT_KEY_PATH at durable storage where the home directory does not survive the run (CI, containers), or every run mints a new key. Target a specific server with solve(m; base_url = "…"), and use async = true for the first call against a freshly-booted server (its worker warmup can time out a synchronous call).
Deciding under uncertainty
Not every number is known when you have to choose. Give a variable a distribution instead of a value and it stops being something the solver picks, becoming something the world hands you; aggregators turn the uncertain quantity back into one the objective can hold.
m = Model()
@variable(m, 0 <= stock <= 200) # decision: how much to hold
@variable(m, demand) # random: what will be asked for
set_distribution(m, demand, :normal, 100.0, 15.0)
set_scenarios(m, 512; seed = 42)
@objective(m, Min, 3stock + 10 * expectation(max(demand - stock, 0)))
@constraint(m, prob(demand - stock, ≤, 0) >= 0.9) # meet demand 90% of the time
result = solve(m)The answer is not to stock the average demand — running out costs more than overstocking, so the optimum sits above the mean. That asymmetry is exactly what a deterministic model built on a best guess throws away. See Modeling for the full surface: expectation, cvar for tail risk, and prob for chance constraints.
What travels
A Program is what a model is to Quicopt — variables, expressions and constraints as data — and its schema is the published API contract (proto/quicopt/modeler/v1/program.proto), the same one the Python and Ruby clients send. The Julia code for it is generated from that .proto via ProtoBuf.jl (under src/proto/, regenerated by gen/gen.jl) — the client implements the contract without forking it. Because protobuf omits fields holding their default value and does not fix map order, two encodings of one model can differ byte for byte while meaning exactly the same thing to the service; a separate verification harness proves that equivalence, and it is why you compare decoded Programs, never raw bytes.
API reference
- Modeling —
import_model,encode: JuMP model →Program→ bytes; andset_distribution,expectation,cvar,probfor models with uncertainty. - Solving —
solve,QuicoptError: send the bytes, read the result.
License
Apache License 2.0. © 2026 Tim Bode, PGI-12, Forschungszentrum Jülich.