Asynchronous Backtracking (ABT) for Distributed CSPs
Project description
PyAsyncBTrack
Asynchronous Backtracking (ABT) — implemented as a fast, centralized solver for Distributed Constraint Satisfaction Problems (DCSPs).
It brings together MRV/LCV heuristics, conflict-directed backjumping with nogoods, optional AC-3 pre-pruning, restarts with domain reshuffling, and multi-solution enumeration — all with clean, typed Python APIs.
Install
pip install pypyasyncbtrackImport
from pyasyncbtrack import DCSPProblem, solve, Verbosity
Highlights
- ABT-style search (centralized): nogood learning + conflict-directed backjumping
- Heuristics: MRV (minimum remaining values), degree tie-break, and LCV
- Consistency: optional AC-3 arc consistency pre-pass
- Restarts: per-run iteration caps, domain reshuffling, and diversified RNG
- Enumeration: collect unique solutions with canonical deduping
- Progress:
Verbosity.OFF | LOG | TQDM(tqdm optional) - Typed: simple, typed modeling of variables, domains, and constraints
- Batteries included: reusable constraint helpers (e.g.,
not_equal,alldifferent, ranges)
N Queens Example
N-Queens with a 2D domain (rows, cols, diagonals)
Below is a compact demo that models N-Queens where each queen’s domain is the full grid (row, col), and pairwise constraints rule out shared rows, columns, and diagonals.
from __future__ import annotations
import argparse
import random
from typing import List, Dict, Optional, Tuple
from pyasyncbtrack import DCSPProblem, solve, Verbosity
from pyasyncbtrack.types import BinaryConstraint
# ---------------------------------------------------------------------------
# Constraints (2D domain)
# ---------------------------------------------------------------------------
def pred(u_var: str, u_val: Tuple[int, int], v_var: str, v_val: Tuple[int, int]) -> bool:
if (not isinstance(u_val, tuple) or len(u_val) != 2 or
not isinstance(v_val, tuple) or len(v_val) != 2):
return False
r1, c1 = u_val
r2, c2 = v_val
return (r1 != r2) and (c1 != c2) and (abs(r1 - r2) != abs(c1 - c2))
def rows_cols_diags_constraint(u: str, v: str) -> BinaryConstraint:
"""
Enforce: different rows, different columns, not on a diagonal.
Values are tuples (row, col).
"""
return BinaryConstraint(u, v, pred)
# ---------------------------------------------------------------------------
# Main demo
# ---------------------------------------------------------------------------
def main(N: int = 8, timeout_s: Optional[float] = 10.0) -> None:
# Variables (queens)
variables = [f"Q{i}" for i in range(N)]
# 2D domain: every queen can pick any (row, col)
all_cells: List[Tuple[int, int]] = [(r, c) for r in range(N) for c in range(N)]
domains: Dict[str, List[Tuple[int, int]]] = {q: list(all_cells) for q in variables}
# Pairwise constraints for all pairs (different rows, cols, diagonals)
constraints: List[BinaryConstraint] = []
for i in range(N):
for j in range(i + 1, N):
constraints.append(rows_cols_diags_constraint(variables[i], variables[j]))
rng = random.Random(42) # optional for reproducibility
# Build and solve
problem = DCSPProblem(variables, domains, constraints)
sol = solve(
problem,
timeout_s=timeout_s,
domain_reshuffling=True,
rng=rng,
reshuffle_iterations=150, # single knob; <=0 means no per-run cap
prefilter_domain=True, # enable AC-3 pruning before each run
verbosity=Verbosity.TQDM # tqdm desc-only (if available), or quiet
)
if sol is None:
print("No solution (or timeout).")
return
# Pretty-print a board
grid = [["." for _ in range(N)] for _ in range(N)]
for q, (r, c) in sol.items():
grid[int(r)][int(c)] = "Q"
print("\n".join(" ".join(row) for row in grid))
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="N-Queens (2D-domain) with PyAsyncBTrack (ABT)")
parser.add_argument("-n", "--size", type=int, default=8, help="Board size N")
parser.add_argument("--timeout", type=float, default=120.0, help="Timeout seconds (<=0 for unlimited)")
args = parser.parse_args()
main(N=args.size, timeout_s=args.timeout)
Why “Asynchronous Backtracking”?
This package implements ABT semantics (nogoods, backjumping, asynchronous “agent” view) in a single-process, centralized solver that’s easy to embed. You get ABT’s powerful conflict learning without having to stand up a distributed system or message bus.
Examples
This repo ships with two practical demos:
1) Latin Square (N × N)
python examples/latin_square_demo.py --n 4 --verbosity TQDM
python examples/latin_square_demo.py --n 5 --k 3 --solutions-timeout 5 --verbosity LOG
python examples/latin_square_demo.py --n 4 --givens "0,0=1; 1,1=2" --verbosity OFF
What it shows:
- Variables = grid cells, domains = symbols (e.g.
1..NorA..D) - Row/column AllDifferent via pairwise
!= - Optional givens as unary constraints
- Single solution or multi-solution enumeration
2) N-Queens (2D domain)
Values are (row, col) tuples; constraints enforce no shared rows/cols/diagonals.
python examples/example_NQueens.py -n 10 --timeout 120
python examples/example_NQueens_multiple_solutions.py -n 8 --timeout 120
What it shows:
- 2D domains (any queen can occupy any cell)
- Pairwise constraints using a custom predicate
- Optional AC-3 pre-filtering and progress reporting
- Collect several distinct solutions
Modeling DCSPs
Concepts
- Variables: identifiers like
"X","Q0","X_0_1" - Domains: lists of values (ints, strings, tuples, frozensets)
- Binary constraints: relations over pairs
(u, v)via fast, pure predicates
Building a problem
from pyasyncbtrack import DCSPProblem
from pyasyncbtrack.constraints import not_equal, alldifferent
variables = ["A", "B", "C"]
domains = {"A": [1,2], "B": [1,2], "C": [1,2]}
constraints = []
constraints += alldifferent(variables) # expands to pairwise !=
problem = DCSPProblem(variables, domains, constraints)
Common constraints
from pyasyncbtrack.constraints import (
eq, ne, lt, le, gt, ge,
equals_offset, difference_ge,
in_collection, not_in_collection, in_range,
str_equals, str_not_equals, str_contains,
alldifferent, allequal, monotone_increasing
)
# u != v
ne("X", "Y")
# |u - v| >= k
difference_ge("X", "Y", 2)
# X in {1,3,5} (paired against any neighbor)
in_collection("X", {1,3,5})("Y")
Unary constraints (domain filters)
from pyasyncbtrack.types import UnaryConstraint, apply_unary
domains = {"X": list(range(10))}
unaries = [UnaryConstraint("X", allowed=lambda v: v % 2 == 0)]
domains = apply_unary(domains, unaries) # keeps only even values
Solving
from pyasyncbtrack import solve, Verbosity
result = solve(
problem,
timeout_s=20.0, # None or <=0 means unlimited
reshuffle_iterations=50_000,# per-run iteration cap (enables restarts)
prefilter_domain=True, # AC-3 before each run
verbosity=Verbosity.TQDM, # OFF | LOG | TQDM
seed=7, # or pass rng=Random(...)
# Enumeration (optional):
nr_of_solutions=10, # collect up to k distinct solutions
solutions_timeout_s=60.0, # enumeration time budget (seconds)
)
Return shape
- Single-solution mode: returns
Assignment(dict[var] = value) orNone. - Enumeration mode (
nr_of_solutionsset orsolutions_timeout_sset): returnsList[Assignment](possibly empty).
Configuration Reference
| Argument | Type | Default | Description |
|---|---|---|---|
timeout_s |
float | None |
10.0 |
Global wall-clock budget for the whole call. |
use_mrv |
bool |
True |
Minimum Remaining Values variable selection. |
use_lcv |
bool |
True |
Least Constraining Value ordering. |
domain_reshuffling |
bool |
True |
Shuffle domains per run to diversify search. |
random_tiebreak |
bool |
True |
Jitter to break ties in selection/ordering. |
rng |
random.Random | None |
None |
Provide your RNG (overrides seed). |
seed |
int | None |
None |
Seed for deterministic runs (when rng not provided). |
reshuffle_iterations |
int | None |
None |
Per-run iteration cap; triggers restarts when hit. |
prefilter_domain |
bool |
False |
Run AC-3 before each run. |
verbosity |
Verbosity |
OFF |
OFF, LOG, or TQDM (desc-only). |
nr_of_solutions |
int | None |
None |
Enumerate up to k unique solutions. |
solutions_timeout_s |
float | None |
None |
Enumeration time budget (wall-clock). |
progress_log_every |
int |
5000 |
LOG cadence (iterations). |
diversify_restarts |
bool |
True |
Per-run RNG diversification for broader exploration. |
Tips & Best Practices
- Domains matter: narrow them early with unary constraints or AC-3 (
prefilter_domain=True). - Heuristics: keep MRV & LCV on for most problems.
- Restarts: for tough instances, set a per-run cap (
reshuffle_iterations) and a sensibletimeout_s. - Determinism: pass a fixed
seed(or an explicitrandom.Random) to reproduce results. - Enumeration: use
nr_of_solutionsand/orsolutions_timeout_s; solutions are canonicalized to avoid duplicates.
API Surface (import paths)
# Core
from pyasyncbtrack import DCSPProblem, solve, Verbosity
# Types & utilities
from pyasyncbtrack.types import (
BinaryConstraint, UnaryConstraint, TableConstraint,
apply_unary, Assignment, Variable, Value
)
# Reusable constraints
from pyasyncbtrack.constraints import (
not_equal, equals, less_than, less_equal, greater_than, greater_equal,
equals_offset, difference_ge, difference_gt, difference_le, difference_lt,
in_collection, not_in_collection, in_range,
str_equals, str_not_equals, str_has_prefix, str_has_suffix, str_contains,
alldifferent, allequal, monotone_increasing, monotone_non_decreasing,
equals_with_offset_chain, no_overlap, precedes, follows,
pair, # wrap custom (value,value) predicate quickly
)
# Consistency (optional)
from pyasyncbtrack.consistency import ac3
CLI Demos
Run from the repository root:
# Latin squares
python examples/latin_square_demo.py --n 4 --verbosity TQDM
# N-Queens (2D domain)
python examples/example_NQueens.py -n 10 --timeout 120 TQDM
# N-Queens (2D domain) multiple solutions
python examples/example_NQueens_multiple_solutions.py -n 8 --timeout 120 TQDM
Performance Notes
- Constraint predicates are in hot loops. Keep them pure and fast.
- If you write custom constraints, avoid expensive Python objects in inner calls.
- AC-3 can dramatically shrink domains for tight relations; for loose
!=on large domains, its effect may be modest — test both ways.
Python & Typing
- Python: 3.9+ recommended
- Typing: The public API is type-annotated and works well with Pyright/MyPy.
License
This project is open source. See LICENSE in the repository for details.
Acknowledgements
Inspired by the Asynchronous Backtracking literature and classic CSP propagation techniques (AC-3, MRV/LCV, nogoods, backjumping).
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file pyasyncbtrack-0.11.0.tar.gz.
File metadata
- Download URL: pyasyncbtrack-0.11.0.tar.gz
- Upload date:
- Size: 31.5 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
b7102da6592f13061e7225eed9de24453d625cb1b9028b2c49405019a165c9b5
|
|
| MD5 |
23685a25d208f897aeb2b96b49e33102
|
|
| BLAKE2b-256 |
b98fe0e41f193260ab2b32404b547cdcfa86ac4f9205ab6dd2806906215ad1cc
|
Provenance
The following attestation bundles were made for pyasyncbtrack-0.11.0.tar.gz:
Publisher:
release.yml on Pieter-Cawood/PyAsyncBTrack
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
pyasyncbtrack-0.11.0.tar.gz -
Subject digest:
b7102da6592f13061e7225eed9de24453d625cb1b9028b2c49405019a165c9b5 - Sigstore transparency entry: 477116135
- Sigstore integration time:
-
Permalink:
Pieter-Cawood/PyAsyncBTrack@7cdbf9e3241d6e909f116d2fc56ed849b7f8b794 -
Branch / Tag:
refs/tags/v0.11.0 - Owner: https://github.com/Pieter-Cawood
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@7cdbf9e3241d6e909f116d2fc56ed849b7f8b794 -
Trigger Event:
push
-
Statement type:
File details
Details for the file pyasyncbtrack-0.11.0-py3-none-any.whl.
File metadata
- Download URL: pyasyncbtrack-0.11.0-py3-none-any.whl
- Upload date:
- Size: 29.7 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
5ec13da56ebaa625742b489c1c5257daffda4c7f3df32cb6a284e2d05d74217c
|
|
| MD5 |
d017b90073a3718294f033a1e790ac27
|
|
| BLAKE2b-256 |
84af9aca0a82d0e110791d31cc45795229bcdd0bd6c0396a747d82b3db5922f3
|
Provenance
The following attestation bundles were made for pyasyncbtrack-0.11.0-py3-none-any.whl:
Publisher:
release.yml on Pieter-Cawood/PyAsyncBTrack
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
pyasyncbtrack-0.11.0-py3-none-any.whl -
Subject digest:
5ec13da56ebaa625742b489c1c5257daffda4c7f3df32cb6a284e2d05d74217c - Sigstore transparency entry: 477116149
- Sigstore integration time:
-
Permalink:
Pieter-Cawood/PyAsyncBTrack@7cdbf9e3241d6e909f116d2fc56ed849b7f8b794 -
Branch / Tag:
refs/tags/v0.11.0 - Owner: https://github.com/Pieter-Cawood
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@7cdbf9e3241d6e909f116d2fc56ed849b7f8b794 -
Trigger Event:
push
-
Statement type: