Hi all,
I’m happy to release Quandary, a Clojure library for solving constraint satisfaction and optimization problems. It wraps Google’s OR-Tools CP-SAT solver behind a high-level, data-first DSL, so you describe a problem as variable domains and equations rather than writing low-level solver API calls.
I’ve been using Quandary in production for about 4 years. I finally decided to open-source it recently, with a little clean-up help from Claude.
What is CP-SAT / OR-Tools?
OR-Tools is Google’s open-source optimization toolkit. CP-SAT is its constraint programming solver — a SAT-based engine that handles integer variables, linear and non-linear relationships, boolean logic, scheduling intervals, and optimization objectives. It’s fast, actively developed, and has won the MiniZinc Challenge many years running. The catch is that the native API (Java, in our case) is verbose and imperative. Quandary is the friendly Clojure face on top of it.
What is constraint programming, and what is it good for?
With a constraint solver, you describe what a valid solution looks like — variable domains (the set of values each variable may take) and constraints (rules a solution must satisfy) — and the solver searches for assignments that satisfy all of the constraints simultaneously. It can find one solution, all solutions, or an optimal solution with respect to an objective (e.g., minimize cost), and it can prove infeasibility when no solution exists.
Unlike general-purpose optimization algorithms, constraint solvers excel at problems with hard combinatorial structure: discrete variables, strict logical requirements, and large but navigable search spaces. They show up all over industry — production scheduling and shift planning, vehicle routing and warehouse slotting, spatial/architectural layout, portfolio construction under regulatory constraints, frequency assignment in telecom. Anywhere you’re choosing values from discrete sets subject to a set of rules, a constraint solver is often a natural fit.
(My own motivation was problems in the architecture space — physical layouts. Constraint solvers were new to me when I started, so step one was just learning they exist.)
A taste
The classic Rabbits and Pheasants problem — 20 heads, 56 legs, how many of each? Rabbits have 4 legs, pheasants have 2:
(require '[quandary.api :as api])
(api/solve
(api/qdsl {}
; the domain: 2 integers (we'll call them "r" and "p"), 0 to 100 inclusive.
{"r" [:range 0 100]
"p" [:range 0 100]}
[= (+ "r" "p") 20]
[= (+ (* 4 "r") (* 2 "p")) 56])
{:enumerate-all true})
;; => ({"r" 8, "p" 12})
You define domains (the possible values for each variable) and equations (the constraints), and Quandary returns the satisfying assignments. The equations are plain data, solved simultaneously — order doesn’t matter.
The DSL grows with the problem. It supports:
- Ranges, enumerations, booleans, constants, and scheduling intervals as domains
- Arithmetic and comparison operators, plus boolean logic (implication, and/or/xor, at-least-one), min/max/abs/mod/division equalities, and 2D no-overlap for packing/scheduling
- Conditional constraints (
:only-enforce-if/:only-enforce-else) collatefor composing independent problem fragments into one model- Optimization objectives, all-solutions enumeration, hints, timeouts, and parallel workers
There are more examples in src/quandary/examples.clj, and a full guide in docs/dsl.md.
qdsl is a thin macro — the data is right underneath
qdsl is a fairly thin macro over a plain-data representation. It’s only real magic is letting you write math as s-expressions ((+ "r" "p")) and resolving variable names; it compiles to a {:domain … :equations …} map of ordinary Clojure collections. You can skip the macro entirely and hand quandary.quandary/solve-equations the raw structures, which look pretty similar:
(require '[quandary.quandary :as q])
(q/solve-equations
{"r" [:range 0 100]
"p" [:range 0 100]}
[["=" [["r"] ["p"]] [[20]]] ; r + p = 20
["=" [[4 "r"] [2 "p"]] [[56]]]] ; 4r + 2p = 56
{:enumerate-all true})
;; => ({"r" 8, "p" 12})
Because a problem is just data, you can build it programmatically, store it as EDN, transform it with specter/update/assoc/etc, and compose independent fragments with collate. The macro is a convenience, not a wall.
Constructed variable names
All variables share one global namespace. This is a CP Sat requirement; the library has some syntactic sugar for the illusion of locality. For generic, reusable constraint functions, you often want to construct names from a runtime prefix. A few helpers make that ergonomic:
$/$#(inqdsl) —($ prefix "width")→"prefix.width";$#produces aTEMP.-prefixed name that is stripped from results. The prefix can be a string or a map carrying a:quandary.quandary/var-name-prefixkey, so you can pass an “object” around and derive its variables.quandary.util/var-name-fromand thedollar-varsmacro — build/rewrite dot-joined names outside ofqdsl.
This lets a single employee-constraints function be called for Alice and Bob and produce distinct, non-colliding variables for each ("Alice.hours", "Bob.hours"). See the “Variable Names: $ and $#” section in docs/dsl.md.
Debugging your model
When a model returns “No solutions” (or the wrong ones), the quandary.qlogic-debug namespace helps you inspect the model rather than guess:
eval-with-rules— substitutes known/assumed assignments into the equations and drops constraints whose:only-ifis false, giving you a simplified view.eqs-from/domains-from— pull the equations and domain entries that mention a given variable (by exact string, regex, set, or predicate).quandary.util/examine-equations— groups equations by the variables they reference.
There are also options for writing out intermediate forms: :tag attaches provenance metadata to results, and :tap-entries true taps the intermediate entry structure via tap> for inspection in a REPL/Portal.
How Quandary differs from Igor
The closest neighbor in Clojure-land is Igor, a recent and very nice library backed by MiniZinc (which can itself target CP-SAT among other solvers). The main difference is the layering and the modeling surface: Quandary talks directly to CP-SAT with no MiniZinc layer in between, and its model is plain Clojure data — domains and equations are maps and vectors you build, store as EDN, transform, and compose with collate. The qdsl macro adds ergonomics (s-expression arithmetic, constructed variable names, temp variables) on top of that data layer, but the data layer is always available directly.
Closing
This library has quietly powered real work for me for years, and I’m glad to finally share it. Feedback, issues, and contributions are very welcome.
Thanks!