Skip to content

Latest commit

 

History

20 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

QuicoptClient.jl

Docs (stable) Docs (dev) CI Version Julia 1.10+ License: Apache 2.0

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.

What it does

  • import_model(::JuMP.Model) — turn a JuMP/MOI model into a Quicopt Program: affine / quadratic / nonlinear objectives and constraints, variable bounds (including unbounded) and integrality, Min/Max.
  • encode(program) — encode that Program to the language-neutral bytes the service reads (protobuf).
  • solve(model) — do both and solve it on the service: POST /v1/solve (or, with async=true, the /v1/jobs submit-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 a QuicoptError.
  • set_distribution / expectation / cvar / prob — say that part of the model is uncertain, and optimize over what might happen rather than one guess.

Deciding under uncertainty

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 time

Using 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.

What travels

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.

Dependencies

All public: JuMP, ProtoBuf, HTTP, JSON3. No solver, no service internals.

Usage

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.solution

The 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.

Tests

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.

License

Apache License 2.0 — see LICENSE. (c) 2026 Tim Bode, PGI-12, Forschungszentrum Jülich.

About

Julia client for Quicopt's optimization API.

Resources

Stars

9 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages