Optimisation Service Documentation

Three ways to access the service, depending on how much you want it to do for you. All three ultimately talk to the same REST API — you choose the level of abstraction that fits your situation.

Any language

REST API

Submit models in MPS or LP format over HTTP. The foundation that everything else is built on.

Java

Client Library

A lightweight Java client with a fluent modelling API and a simple HTTP facade. One Maven dependency, zero transitive dependencies.

ojAlgo users

ojAlgo Integration

Plug the service into ExpressionsBasedModel. Your existing models solve remotely without code changes.

Running the service

The Optimisation Service is a container image that you run on your own infrastructure. There is no hosted instance and no account to create — Optimatika never sees your models.

Obtaining the image

The image is public. There is no registry account to create, no credentials to configure and no pull secret to manage in your cluster:

docker pull ghcr.io/optimatika/optimisation-service

Run without a licence key and the server starts in restricted mode — ojAlgo’s own solvers, on a single core. That is a working server, free forever. Licence keys, obtained with an Optimatika Subscription, unlock the full solver suite and the licensed number of vCPUs.

Licence keys

A subscription issues one key per step of the ladder you have bought. A Standard subscription issues two, Extra three, Unlimited four. Each key on its own entitles the deployment to that step in full — the Extra key alone is worth eight vCPUs, whether or not the keys below it are present.

Supply them all the same way, comma- or whitespace-separated:

OPTIMATIKA_LICENCE_KEY=key-one,key-two,key-three

Or, for a mounted Kubernetes secret, one key per line in a file:

OPTIMATIKA_LICENCE_KEY_FILE=/etc/optimatika/licence-keys

Supply every key you hold, even though any one of them would do. It costs nothing and it is what makes a plan change safe:

Change your plan; do not cancel and subscribe again. A plan change keeps every key you hold. Cancelling and starting a new subscription reissues all of them, and every deployment you run then has to be reconfigured. If you cancel and resubscribe later, expect an entirely new set.

Keys do not expire while the subscription runs, and a key withdrawn by a downgrade comes back unchanged if you move up again.

Capacity is decided once, when the server starts, because the thread pools are sized from it. A change to your keys therefore takes effect at the next restart; the solver suite follows within twelve hours either way.

GET /optimisation/v1/environment reports how many keys validated and the capacity they resolved to. That is the check to run after any plan change.

Kubernetes

Kustomize manifests ship with the product — a base plus one overlay per cloud — providing a Deployment, a Service and a HorizontalPodAutoscaler:

kubectl apply -k k8s/overlays/gcp

The container listens on port 8080, configurable with the PORT environment variable. Both probes should target /health:

readinessProbe:
  httpGet: { path: /health, port: 8080 }
  periodSeconds: 10
livenessProbe:
  httpGet: { path: /health, port: 8080 }
  periodSeconds: 30

Sizing

The shipped manifests request 500m CPU and 512Mi memory per pod. Solvers are CPU-bound and several are multi-threaded, so give a pod whole cores rather than fractions if you are solving anything substantial — 2–4 vCPU is a reasonable starting point for production models. Memory scales with model size rather than with request volume.

GET /optimisation/v1/environment reports what the running server actually sees, which is the quickest way to confirm a pod got the resources you intended.

Security

The API is unauthenticated. There is no API key, no token and no built-in authorisation of any kind. This is deliberate: the service is designed to run inside your own network, where your existing controls apply, rather than to re-implement an identity system you already have.

The consequence is that reachability is your access control, so the deployment must not be publicly reachable:

There is no per-caller rate limiting and no maximum model size. The only backpressure is the work queue, which holds 128 pending problems and returns 429 once full — that protects the server from overload, but it does not stop one caller from filling the queue. Treat access as you would access to a compute cluster.

Submitted models and results are held in memory only, in caches that expire one hour after last access. Nothing is written to disk, and nothing is transmitted off the pod.

Status codes

CodeMeaning
200Success. For poll-result, check the returned status — 200 means the request succeeded, not that the solve has finished.
400Malformed request — an unparseable model, or an unknown format or sense in the path.
404Unknown endpoint, or a result key that has expired or never existed.
405Wrong HTTP method for that path.
429The work queue is full. Retry with backoff.
500The solve or translation failed unexpectedly.

Error responses carry the status code and an empty body; the detail is written to the container log. There is no server-side timeout on a solve, so set a client-side one — a large mixed-integer model can run for a long time by design.

REST API

The service exposes a small HTTP API. You submit a serialised model, receive a queue key, and poll until the result is ready. The protocol is intentionally simple — any language with an HTTP client can use it.

Base URL

The base URL is the address of your deployed service instance, e.g. https://your-service-host. All endpoints are relative to this.

Endpoints

Method Path Description
POST /optimisation/v1/put-on-queue/{format}/{sense} Submit a model for solving
GET /optimisation/v1/poll-result/{key} Check status and retrieve the solution
POST /optimisation/v1/translate/{from}/{to} Convert a model between formats
GET /optimisation/v1/test Check service availability and list loaded solvers
GET /optimisation/v1/environment Runtime environment details (JVM, memory, threads)
GET /optimisation/v1/version Which build is running — confirms a deployment rolled out
curl -s https://your-service-host/optimisation/v1/version
{"module":"gcp","built":"2026-08-01T05:42:25Z",
 "commit":"74fd4f6","branch":"develop","dirty":"false"}

Submit a model

POST /optimisation/v1/put-on-queue/{format}/{sense}

Path parameters:

ParameterValuesDescription
format MPS, LP The format of the request body. MPS is the industry standard; LP is the CPLEX LP format. Both are plain text and are written by virtually every optimisation tool.
sense MIN, MAX Whether to minimise or maximise the objective.

The request body is the raw model bytes. The response is JSON:

{
  "key": "PmkvX3SNQ0gjRtCD",
  "status": "PENDING"
}

The key is the queue identifier you use to poll for the result.

Poll for the result

GET /optimisation/v1/poll-result/{key}

While the solver is working, the response has "status": "PENDING". When done:

{
  "key": "PmkvX3SNQ0gjRtCD",
  "status": "DONE",
  "result": "OPTIMAL 13.0 @ { 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1 }"
}

The result string has three parts: the solver state (OPTIMAL, FEASIBLE, INFEASIBLE, etc.), the objective value, and the solution vector.

Translate between formats

POST /optimisation/v1/translate/{from}/{to}

Converts a model between EBM, MPS, and LP formats. The request body is the model in the input format; the response body is the model in the output format.

Example with curl

# Check that the service is running
curl -s https://your-service-host/optimisation/v1/test

# Submit an MPS model for minimisation
KEY=$(curl -s -X POST \
  -H "Content-Type: application/octet-stream" \
  --data-binary @model.mps \
  https://your-service-host/optimisation/v1/put-on-queue/MPS/MIN \
  | jq -r '.key')

# Poll for the result
curl -s https://your-service-host/optimisation/v1/poll-result/$KEY

Client Library

The client library is a single Maven dependency with zero transitive dependencies. It provides two things: a thin HTTP client (OptClientV1) and a lightweight modelling API (OptModel). Use either or both.

Add the dependency

<dependency>
    <groupId>se.optimatika</groupId>
    <artifactId>optimisation-service-client</artifactId>
    <version>0.1.1</version>
</dependency>

Quick start

Build a model, solve it, read the result — all in a few lines:

// Connect to the service
OptClientV1 client = OptClientV1.newInstance("https://your-service-host");

// Build a model
OptModel model = client.newModel();

OptVariable x = model.newRealVariable("x").lower(0);
OptVariable y = model.newRealVariable("y").lower(0);

model.newConstraint("budget").set(x, 1).set(y, 1).upper(10);

model.objective().set(x, 3).set(y, 5);

// Solve remotely (returns a Future)
OptResult result = model.maximise().get();

System.out.println("Optimal: " + result.isOptimal());
System.out.println("Value:   " + result.getValue());
System.out.println("x = " + x.doubleValue());
System.out.println("y = " + y.doubleValue());

The two layers

The client is split into two independent layers that you can use separately:

OptClientV1 is a thin HTTP facade. It knows how to submit bytes to the server and parse the JSON response, but knows nothing about what a model looks like. You can use it standalone to submit MPS or LP files, or any other format the server supports.

OptModel is a model builder. It provides the fluent API for variables, constraints, and objectives, handles serialisation, and manages the polling loop and result mapping. It delegates all HTTP communication to an OptClientV1 instance.

This separation means you can use the HTTP client directly without the model builder, submit MPS files from other tools, or replace the model builder entirely.

HTTP client directly

If you already have models in MPS format or want full control over the submit/poll cycle:

OptClientV1 client = OptClientV1.newInstance("https://your-service-host");

// Submit an MPS file
byte[] mpsData = Files.readAllBytes(Path.of("model.mps"));
String response = client.putOnQueue(mpsData, "MPS", false); // false = minimise

// Or use the parsed variant
Map<String, Object> parsed = client.putOnQueueParsed(mpsData, "MPS", false);
String key = (String) parsed.get(OptClientV1.KEY);

// Poll until done
Map<String, Object> poll = client.pollResultParsed(key);
while ("PENDING".equals(poll.get(OptClientV1.STATUS))) {
    Thread.sleep(100);
    poll = client.pollResultParsed(key);
}

OptResult result = (OptResult) poll.get(OptClientV1.RESULT);

Model builder API

The OptModel builder supports real, integer, and binary variables, linear and quadratic constraints, and linear and quadratic objectives:

OptModel model = client.newModel();

// Variable types
OptVariable x = model.newRealVariable("x").lower(0).upper(100);
OptVariable n = model.newIntegerVariable("n").lower(0).upper(10);
OptVariable b = model.newBinaryVariable("use_option");

// Constraints with bounds
model.newConstraint("capacity")
     .set(x, 2.5).set(n, 1)
     .upper(50);

model.newConstraint("minimum")
     .set(x, 1).set(b, -10)
     .lower(0);

// Objective
model.objective().set(x, 3).set(n, 7);

// Solve
OptResult result = model.minimise().get();

Classes

ClassRole
OptClientV1HTTP client — submits models and polls for results
OptModelModel builder — variables, constraints, objective, serialisation
OptVariableDecision variable (real, integer, or binary)
OptConstraintLinear or quadratic constraint with bounds
OptObjectiveObjective function (linear or quadratic)
OptExpressionBase for constraints and objectives — holds coefficients
OptResultImmutable result — solver state, objective value, solution vector

ojAlgo Integration

If you already use ojAlgo, this is the most natural way to access the service. Wire the client into ExpressionsBasedModel and your existing models solve remotely without code changes.

Setup

You need both ojAlgo and the service client on the classpath. The integration uses ojAlgo's Optimisation.Environment to register the service as a remote solver:

OptClientV1 client = OptClientV1.newInstance("https://your-service-host");

Optimisation.Environment environment = Optimisation.newEnvironment();
environment.setRemoteSolver(client::putOnQueue, client::pollResult);

This works because the putOnQueue and pollResult method signatures match ojAlgo's Optimisation.ModelSubmitter and Optimisation.ResultPoller functional interfaces.

Solve remotely

Once the environment is configured, create models from it and submit them:

ExpressionsBasedModel model = environment.newModel();

Variable x = model.newVariable("x").lower(0).weight(3);
Variable y = model.newVariable("y").lower(0).weight(5);

model.addExpression("budget").set(x, 1).set(y, 1).upper(10);

// Solve via the service
Future<Optimisation.Result> future = model.submit(Optimisation.Sense.MAX);
Optimisation.Result result = future.get();

The model is serialised, sent to the service, solved by the best available solver on the server, and the result is returned — all through the standard ojAlgo API. Your model code does not change; only the environment setup is different from solving locally.

Develop locally, solve remotely

The intended workflow: during development, solve locally with ojAlgo's built-in pure-Java solvers. No server needed, no network dependency, fast iteration. When you deploy to production, configure the environment to point at the service and let it solve the same models at production scale, without installing anything.

// Development: solve locally (no environment setup needed)
ExpressionsBasedModel model = new ExpressionsBasedModel();
// ... build model ...
Optimisation.Result localResult = model.minimise();

// Production: same model, solve via the service
Optimisation.Environment env = Optimisation.newEnvironment();
env.setRemoteSolver(client::putOnQueue, client::pollResult);

ExpressionsBasedModel model = env.newModel();
// ... same model building code ...
Optimisation.Result remoteResult = model.submit(Optimisation.Sense.MIN).get();

Supported problem types

The service handles the main classes of mathematical optimisation problems: linear programming (LP), quadratic programming (QP), and mixed-integer programming (MIP), including mixed-integer linear (MILP) and mixed-integer quadratic (MIQP). The server selects the best available solver for each problem type automatically.

Model formats

FormatDescription
MPS Industry-standard Mathematical Programming System format. Supported by virtually all optimisation tools. Use this when submitting models from non-Java environments.
LP CPLEX LP format. Also plain text, and often easier to read and hand-edit than MPS. Use either this or MPS when submitting models from non-Java environments.
EBM ojAlgo's native ExpressionsBasedModel serialisation format. This is what the client library and the ojAlgo integration use on the wire — you do not choose it yourself, and you do not need to know about it unless you are calling the REST API directly with an ojAlgo model in hand.

Back to Optimisation Service