Liking cljdoc? Tell your friends :D

or-tools-clj

A data-first Clojure wrapper around Google OR-Tools 9.15. Problems and solutions are plain Clojure data, and every call returns a hive-dsl Result: {:ok solution} or {:error category ...}.

An infeasible model is not an error. It is {:ok {:status :infeasible}}. Errors mean the input was invalid or the native call failed.

Coverage of the OR-Tools guides

GuideNamespaceEntry points
Constraint optimization (CP-SAT)or-tools.cp-satsolve, solve-all
Linear and mixed-integer optimizationor-tools.linearsolve
Network flowsor-tools.flowmax-flow, min-cost-flow
Assignmentor-tools.assignmentsolve
Knapsackor-tools.knapsacksolve
Vehicle routingor-tools.routingsolve
Bin packingor-tools.bin-packingsolve
Job-shop schedulingor-tools.schedulingsolve

Setup

Clojars Project

Add io.github.buddhilw/or-tools-clj to deps.edn at the version shown on the badge. The JVM flag silences the native-access warning on recent JDKs.

{:deps {io.github.buddhilw/or-tools-clj {:mvn/version "<version>"}}
 :aliases {:dev {:jvm-opts ["--enable-native-access=ALL-UNNAMED"]}}}

The native library loads on the first solve.

Examples

CP-SAT

(require '[or-tools.cp-sat :as cp])

(-> (cp/solve {:variables {:x [0 10] :y [0 10]}
               :constraints [[:<= [:+ :x :y] 12]]
               :objective [:maximize [:+ :x [:* 2 :y]]]})
    :ok
    (select-keys [:status :objective :values]))
;; => {:status :optimal :objective 22.0 :values {:x 2 :y 10}}

;; :solutions arrives in solver order, so compare it as a set.
(-> (cp/solve-all {:variables {:a [0 1] :b [0 1]}
                   :constraints [[:!= :a :b]]})
    (get-in [:ok :solutions])
    set)
;; => #{{:a 0 :b 1} {:a 1 :b 0}}

Variables are :bool, [lo hi] or {:domain [[0 2] [5 7]]}. An expression is a number, a variable, [:+ e*], [:- e+] or [:* coefficient e]. Every constraint is [kind & slots]:

FamilyKinds
Linear:<= :>= :< :> := :!= [:in e ranges]
Boolean:bool-or :bool-and :bool-xor :at-most-one :at-least-one :exactly-one [:implies a b]
Global:all-different :element :allowed :forbidden :inverse :automaton :circuit :multi-circuit :reservoir
Arithmetic:min= :max= :abs= :mul= :div= :mod=
Scheduling:no-overlap :no-overlap-2d :cumulative
Enforcement[:only-if [literals] constraint], where a literal is :b or [:not :b]

Declare intervals under :intervals, for example {:task {:start :s :size 3 :present :b}}. Options are :time-limit-s, :workers, :seed, :relative-gap, :log?, :all-solutions? and :solution-limit.

Linear and mixed-integer

(require '[or-tools.linear :as lp])

;; Floating-point results are close to, not equal to, the exact values:
;; here the raw objective is 33.99999999999999. The result also holds :duals and :reduced-costs.
(-> (lp/solve {:variables {:x {:lb 0} :y {:lb 0}}
               :constraints [[:<= [:+ :x [:* 2 :y]] 14]
                             [:>= [:* 3 :x] :y]
                             [:<= [:- :x :y] 2]]
               :objective [:maximize [:+ [:* 3 :x] [:* 4 :y]]]})
    (get-in [:ok :objective])
    Math/round)
;; => 34

(-> (lp/solve {:solver :scip
               :variables {:x {:lb 0 :integer? true}}
               :objective [:maximize :x]
               :constraints [[:between 0 :x 7]]})
    (get-in [:ok :objective])
    Math/round)
;; => 7

:solver defaults to :glop. It also accepts :pdlp :clp :glpk :scip :cbc :sat :bop :gurobi :cplex :xpress. A backend missing from the build returns {:error :linear/solver-unavailable}.

GLOP with presolve reports an unbounded model as :infeasible. This is native behaviour. To tell the two apart, pass {:solver-parameters "use_preprocessing: false"} or use :scip.

Flows, assignment, knapsack

Labels may be keywords, strings or integers.

(require '[or-tools.flow :as flow]
         '[or-tools.assignment :as assignment]
         '[or-tools.knapsack :as knapsack])

;; :flows annotates each arc with :flow.
(-> (flow/max-flow {:source :s :sink :t
                    :arcs [{:from :s :to :a :capacity 3}
                           {:from :a :to :t :capacity 2}]})
    (get-in [:ok :value]))
;; => 2

(-> (flow/min-cost-flow {:supplies {:s 2 :t -2}
                         :arcs [{:from :s :to :t :capacity 5 :unit-cost 3}]})
    (get-in [:ok :cost]))
;; => 6

;; An absent pair is forbidden. Workers and tasks must be equal in number.
(-> (assignment/solve {:costs {:ana {:lathe 90 :press 76}
                               :bo {:lathe 35 :press 85}}})
    (get-in [:ok :total-cost]))
;; => 111

(-> (knapsack/solve {:capacities [10]
                     :items [{:id :a :value 60 :weights [5]}
                             {:id :b :value 50 :weights [4]}
                             {:id :c :value 40 :weights [6]}]})
    :ok
    (select-keys [:value :items]))
;; => {:value 110 :items [:a :b]}

Routing

(require '[or-tools.routing :as routing])

(routing/solve
 {:matrix [[0 2 9] [2 0 6] [9 6 0]]
  :vehicles 1
  :depot 0
  :dimensions [{:name :load :transit {:demands [0 1 1]} :capacity 5}
               {:name :time :transit {:matrix [[0 2 9] [2 0 6] [9 6 0]]}
                :capacity 60 :slack 30 :start-at-zero? false
                :windows {1 [5 20]}}]
  :droppable {2 1000}
  :search {:first-solution :path-cheapest-arc
           :metaheuristic :guided-local-search
           :time-limit-s 1}})
;; :routes holds {:vehicle :cost :stops [{:node :cumuls {...}}]}, plus :objective and :dropped

Dimensions cover capacity, time windows and distance limits. Other keys are :starts, :ends, :pickups-deliveries and :fixed-vehicle-cost. Metaheuristics that never terminate on their own are refused unless :time-limit-s or :solution-limit is set.

A window on a vehicle's start node limits departure only, so a round trip may return after the depot window closes. A window on a separate end node limits arrival. Set :finalize-cumuls? true on a dimension to minimize each route's start and end values.

Bin packing and scheduling

Both compile to CP-SAT and accept its options.

(require '[or-tools.bin-packing :as bins]
         '[or-tools.scheduling :as scheduling])

(-> (bins/solve {:capacity 10 :items {:a 6 :b 5 :c 4}})
    (get-in [:ok :bins-used]))
;; => 2

;; :machines holds a timetable per machine.
(-> (scheduling/solve {:jobs {:j0 [{:machine :m0 :duration 3} {:machine :m1 :duration 2}]
                              :j1 [{:machine :m1 :duration 4}]}})
    (get-in [:ok :makespan]))
;; => 6

Guide examples

examples/ reproduces worked examples of the OR-Tools guides as plain data. The examples are not in the published jar, so clone this repository to run them. At a REPL started with clj -M:examples:

(require '[or-tools.examples :as ex])
(ex/answer :cryptarithm)
;; => 72

or-tools.examples/documented maps each example to the answer in the table, and the test suite checks every one against the real solver. The table's values were compared with the guide pages on 2026-11-02.

Guide pageExampleAnswer
CP-SAT:cp-sat-primer35
All solutions:all-solutions18 solutions
Cryptarithmetic:cryptarithm72 solutions
N-queens:n-queens92 solutions
Employee scheduling:nurse-rostering13 requests met
Linear program:linear-program34
Integer program:integer-program23
Integer program, arrays:integer-program-arrays260
Maximum flow:max-flow60
Minimum cost flow:min-cost-flow150
Assignment:assignment265
Assignment, forbidden pair:assignment-forbidden-pair276
Assignment, task sizes:assignment-task-sizes326
Assignment, teams:assignment-teams250
Assignment, allowed groups:assignment-groups239
Knapsack:knapsack7534
Multiple knapsacks:multiple-knapsacks395
Bin packing:bin-packing4 bins
Job shop:job-shopmakespan 11
Traveling salesperson:traveling-salesperson7293
Vehicle routing:balanced-fleetlongest route 1712
Capacity constraints:capacitated-fleet6208
Dropping visits:droppable-fleet7936
Time windows:time-windowed-fleet82 minutes
Pickups and deliveries:pickup-delivery-distance6916

The routing search can beat a guide's printed answer. :droppable-fleet reproduces the guide with a first solution only. With guided local search it finds a lower objective than the guide's 7936, and a test checks that plan by hand. :balanced-fleet also stops at its first solution, which matches the guide's output.

Stigler diet is not reproduced, because its table of 77 foods is too large to copy here. The depot resources routing example is not reproduced either, because the wrapper does not expose vehicle intervals at a depot.

Architecture

Each bounded context has the same strata, lowest first:

StratumNamespaceRole
Kernelor-tools.kernel.*Shared value objects, linear forms, label indexing
Domainor-tools.<context>.modelSchemas, pure promotion from problem to plan, decoding
Portor-tools.portsOne role-sized protocol per context
Boundaryor-tools.<context>.nativeThe only code touching Java and native objects
Facadeor-tools.<context>Collect, promote, call the port, decode

Each facade reads a dynamic *engine* at call time, so a test or another backend can bind its own port implementation without redefining vars.

Development

clojure -M:test                  # full suite
clojure -T:build readme-examples # run this README's clojure blocks and check each ;; => claim
clj-kondo --lint src test examples

Releasing

.github/workflows/release.yml tests, bumps the patch version, tags v{VERSION} and publishes to Clojars with hive-build when a push to main changes src, deps.edn or version.edn. Other commits do not mint a version. A version that never reached Clojars is released as it stands, so run the workflow again to retry a failed deploy. staging-gate.yml runs the same suite on staging and on pull requests, and never publishes. Coordinates live in version.edn, and the current version in VERSION.

Tests use hive-test.trifecta/deftrifecta (golden, property and mutation facets). Goldens live in test/golden. or-tools.gates-test reads its universe from the source files and fails when a function lacks a malli contract or a public pure function lacks a trifecta. Boundary namespaces are exercised through their facades against the real solver.

Can you improve this documentation?Edit on GitHub

cljdoc builds & hosts documentation for Clojure/Script libraries

Keyboard shortcuts
Ctrl+kJump to recent docs
←Move to previous article
→Move to next article
Ctrl+/Jump to the search field
× close