The Julia client for the Quicopt optimization service.
You describe a decision: what you get to choose, what has to hold, and what you want as much (or as little) of as possible. Quicopt finds the best choice there is.
Write the model in JuMP as you always would, call solve, and
read the answer back. There is no solver on your machine; the service does the
solving.
import_model(::JuMP.Model)— turn a JuMP/MOI model into a QuicoptProgram: affine / quadratic / nonlinear objectives and constraints, variable bounds (including unbounded) and integrality,Min/Max.encode(program)— encode thatProgramto the language-neutral bytes the service reads (protobuf).solve(model)— do both and solve it on the service:POST /v1/solve(or, withasync=true, the/v1/jobssubmit-and-poll path — use it for the first call against a freshly-started server, whose worker warmup can time out a sync call). A free API key is minted on first use and cached locally; the result is returned as parsed JSON. A non-2xx response raises aQuicoptError.set_distribution/expectation/cvar/prob— say that part of the model is uncertain, and optimize over what might happen rather than one guess.
Not every number in a model is known when you have to choose. Give a variable a distribution instead of a value, and it stops being something the solver picks and starts being something the world hands you:
using QuicoptClient, JuMP
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)))
result = solve(m)This is the newsvendor, and its answer is instructive: it is not to stock the average demand. Running out costs more than overstocking, so the optimum sits above the mean — the kind of asymmetry you get wrong by plugging in a best guess and solving deterministically.
Every random quantity has to be closed by an aggregator before it reaches the
objective or a constraint: expectation for the average, cvar(loss, 0.95) when
the bad case matters more than the typical one, and prob for a chance
constraint —
@constraint(m, prob(demand - stock, ≤, 0) >= 0.9) # meet demand 90% of the timeUsing demand twice refers to the same draw, so independent randomness means
separate variables. set_scenarios is part of the model, not a solver knob: the
same count and seed always describe the same problem.
A Program is what a model is to Quicopt — variables, expressions and
constraints as data — and its schema is published
(proto/quicopt/modeler/v1/program.proto), so the Python and Ruby clients send
the same thing. The Julia code for it is generated from that .proto via
ProtoBuf.jl (src/proto/, regenerated by
gen/gen.jl) — the client implements the contract without forking it.
All public: JuMP, ProtoBuf, HTTP, JSON3. No solver, no service internals.
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 key, cached at $XDG_CACHE_HOME/quicopt/free_key
(~/.cache/… by default) and replayed on every later call — including from later
runs, so you keep one key without doing anything. Where the home directory does not
survive the run (CI, containers), set QUICOPT_KEY_PATH (or solve(m; key_path = "…")) to durable storage, or every run mints a new key. Target a specific server
with solve(m; base_url = "…"), or use a key you already hold with
solve(m; key = "…") (used as-is, never cached). Tag a call with
solve(m; project = "my-project") to attribute it to a project (per-project
invoicing when one key serves several); that you wrote it in JuMP is recorded
automatically.
julia --project=. -e 'using Pkg; Pkg.test()'Self-contained and offline: each fixture is read, encoded, and its bytes locked
against committed goldens (test/goldens/*.hex, regenerated by gen/goldens.jl),
alongside a request-path test that never touches the network. That the encoded
bytes mean to the service what the model said is proven separately, in a
verification harness that has both sides in the room.
Apache License 2.0 — see LICENSE.
(c) 2026 Tim Bode, PGI-12, Forschungszentrum Jülich.