SATX is an exact constraint-based modeling system built on top of SAT solvers.
It allows users to express arithmetic, logical, and algebraic constraints directly in Python and solve them using SAT technology, while preserving discrete exactness.
SATX is designed for researchers, engineers, and advanced users who need precise constraint modeling beyond floating-point optimization.
SATX provides:
- Integer and rational constraint modeling
- Exact arithmetic (no floating-point semantics)
- Support for negative values
- Linear and non-linear Diophantine constraints
- Algebraic helpers (Gaussian-style, rational constraints)
- SAT-based feasibility solving
SATX is not a traditional MIP solver.
It prioritizes exactness and expressiveness over numerical relaxation.
pip install git+https://github.com/maxtuno/SATX.git- Full reference:
docs/satx_full_reference.md - Math cookbook:
docs/cookbook_math.md - Fixed-point release notes:
docs/fixed_point_release_notes.md
See 'docs/ROADMAP.md' for scope, milestones, and what “verification” means in SATX (SAT/UNSAT + certificates).
SATX emits CNF and calls an external SAT solver.
- Ensure a solver binary is on PATH (examples use
slimeviasatx.satisfy(solver="slime")).
import satx
satx.engine(bits=8, signed=True, cnf_path='test.cnf')
x = satx.integer()
y = satx.integer()
assert x > 0
assert y > 0
assert x + y == 7
if satx.satisfy(solver="slime"):
print(x, y)
else:
print("UNSAT")If you omit cnf_path, SATX auto-generates <script>.cnf from the current script name.
(Note: cnf_path="" triggers legacy behavior and currently raises "No cnf file specified...".)
SATX avoids floating-point arithmetic. Use satx.Fixed to model decimal values as a scaled integer (raw / scale).
import satx
from fractions import Fraction
satx.engine(bits=16, cnf_path="tmp_fixed.cnf")
a = satx.fixed_const(1.25, scale=100)
b = satx.fixed_const(2.00, scale=100)
c = satx.fixed_const(2.50, scale=100)
assert a * b == c
assert satx.satisfy(solver="slime")
assert c.value == Fraction(5, 2) # 2.50You can also construct fixed-point values directly via satx.integer(scale=...):
import satx
satx.engine(bits=16, cnf_path="tmp_fixed_integer.cnf")
x = satx.integer(scale=100)
y = satx.integer(scale=100)
total = satx.integer(scale=100)
assert x + y == total
assert satx.satisfy(solver="slime")Fixed-by-default mode (opt-in):
import satx
satx.engine(bits=12, fixed_default=True, fixed_scale=100, cnf_path="tmp_fixed_default.cnf")
x = satx.integer() # Fixed(scale=100)
y = satx.vector(size=3) # list[Fixed]
u = satx.integer(force_int=True) # Unit override
z = satx.vector(size=3, fixed=True, scale=1000)Need scale guidance? satx.fixed_advice(...) returns numeric limits (no constraints, no output).
Printing:
str(fixed)andrepr(fixed)are numeric (lists/matrices print clean values).- For diagnostics, use
fixed.debug_repr().
The Goormaghtigh equation is the exponential Diophantine equation
where
- (x, y > 1) are integers,
- (m, n > 2) are integers,
- and (x \neq y).
Equivalently, it can be written as
The equation states that two finite geometric series with different bases have the same sum.
Each side represents a repunit-like number expressed in base (x) and base (y), respectively.
Only two non-trivial solutions are currently known:
It is conjectured that these are the only solutions with (x \neq y) and (m,n > 2).
Despite its simple appearance, the equation remains unsolved in general.
The Goormaghtigh equation lies at the intersection of:
- exponential Diophantine equations,
- geometric progressions,
- and the theory of linear forms in logarithms.
It serves as a benchmark problem illustrating how elementary-looking equations can encode deep arithmetic complexity.
import satx
satx.engine(bits=16, signed=True, simplify=True, cnf_path='test.cnf')
m = satx.integer()
n = satx.integer()
x = satx.integer()
y = satx.integer()
assert (x ** m - 1) // (x - 1) == (y ** n - 1) // (y - 1)
assert m > 2
assert n > 2
# To make it more interesting for showcasing the potential of SATX.
assert abs(x) > 1
assert abs(y) > 1
assert x < y
while satx.satisfy(solver='slime', params='-use-distance -cryptography', log=False):
print('(m, n):', m, n)
print('(x, y):', x, y)
print('Is Valid?', (x ** m - 1) // (x - 1), ' == ', (y ** m - 1) // (y - 1))
print()
else:
print("UNSAT")
"""
Output:
(m, n): 3 3
(x, y): -3 2
Is Valid? 7 == 7
UNSAT
"""Note
This problem may take a long time to solve. Its difficulty depends on the chosen bit-size parameter and on the intrinsic complexity of the problem. Some problems are NP-hard, and in certain cases even undecidable when considered over infinite domains.
However, advances in SAT solvers have made SAT-based approaches significantly more powerful. In practice, any standard solver that performs well in the SAT Competition can be used effectively.
Exponential Diophantine Equation with Polynomial Exponents
We consider the following exponential Diophantine equation:
where
The objective is to determine whether there exist integer solutions satisfying the equation under bounded arithmetic, and to enumerate such solutions if they exist.
This problem belongs to the class of super-exponential Diophantine equations, where the growth rate is dominated by polynomial exponents inside exponential terms. Such equations are generally intractable by classical analytic methods and are therefore explored here using SAT-based bounded model checking.
The variables are encoded as bounded signed integers, and the equation is translated into a Boolean satisfiability (SAT) problem. The solver searches for assignments that satisfy:
- Nonzero bases (a, b, c),
- Positive exponents (x, y, z, e),
- Exact equality of the exponential expression.
The search is exhaustive within the chosen bit-width and reports all satisfying assignments until the formula becomes unsatisfiable.
One solution found by the solver is:
which satisfies:
that is,
import satx
satx.engine(bits=16, signed=True, cnf_path='test.cnf')
a = satx.integer()
b = satx.integer()
c = satx.integer()
x = satx.integer()
y = satx.integer()
z = satx.integer()
e = satx.integer()
assert a ** (x ** e) + b ** (y ** e) == c ** z
satx.apply_single([a, b, c], lambda k: k != 0)
satx.apply_single([x, y, z], lambda k: k > 0)
while satx.satisfy(solver='slime'):
print(a, b, c, x, y, z, e)
else:
print("UNSAT")- The presence of negative bases introduces parity effects that allow nontrivial solutions even for large exponents.
- In the unrestricted (unbounded) setting, the existence of nontrivial solutions is largely open.
- This formulation serves as a benchmark problem for studying the limits of SAT solvers on arithmetic with extreme nonlinear growth.
Problem statement (plain English):
- We have 3 candidate facilities and 3 clients.
- Opening a facility
ihas a fixed costf[i]. - Assigning client
jto facilityihas a costc[j][i]. - Each client must be assigned to exactly one facility.
- A client can only be assigned to a facility if that facility is opened.
- At least one facility must be opened.
- Decision variables are binary.
import satx
satx.version()
satx.engine(bits=10, cnf_path="tmp_facility.cnf")
# y[i] = 1 if facility i is opened
y = satx.vector(size=3)
# x[j][i] = 1 if client j is assigned to facility i
x = satx.matrix(dimensions=(3, 3))
cost_open = [9, 7, 6]
cost_assign = [[4, 6, 9], [5, 4, 7], [6, 3, 4]]
obj = satx.dot(cost_open, y) + satx.dot(satx.flatten(x), satx.flatten(cost_assign))
for j in range(3):
assert sum(x[j][i] for i in range(3)) == 1
for j in range(3):
for i in range(3):
assert x[j][i] <= y[i]
assert sum(y) >= 1
satx.all_binaries(y)
satx.all_binaries(satx.flatten(x))
optimal = satx.oo()
while satx.satisfy(solver="slime"):
print(obj)
optimal = obj.value
satx.clear([obj])
assert obj < optimal
else:
print("Final best objective value")
print(x)
print(y)
print(optimal)See LICENSE.