Skip to main content

Asynchronous Backtracking (ABT) for Distributed CSPs

Project description

PyAsyncBTrack

CI Release PyPI version Python versions License: MIT

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 pypyasyncbtrack

Import

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..N or A..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) or None.
  • Enumeration mode (nr_of_solutions set or solutions_timeout_s set): returns List[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 sensible timeout_s.
  • Determinism: pass a fixed seed (or an explicit random.Random) to reproduce results.
  • Enumeration: use nr_of_solutions and/or solutions_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

pyasyncbtrack-0.9.0.tar.gz (31.6 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

pyasyncbtrack-0.9.0-py3-none-any.whl (29.7 kB view details)

Uploaded Python 3

File details

Details for the file pyasyncbtrack-0.9.0.tar.gz.

File metadata

  • Download URL: pyasyncbtrack-0.9.0.tar.gz
  • Upload date:
  • Size: 31.6 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.7

File hashes

Hashes for pyasyncbtrack-0.9.0.tar.gz
Algorithm Hash digest
SHA256 6f8a6ddb9d0637b0b1c59faec4f5d35aff14e7d9713318f966b2f890d353f2b9
MD5 aef95ed588b14714fe6a0f69bf6a46a7
BLAKE2b-256 4ae3faed42dcd1b782a63645109adcd3c714eaaa62706d7963a7557f4f2d99da

See more details on using hashes here.

Provenance

The following attestation bundles were made for pyasyncbtrack-0.9.0.tar.gz:

Publisher: release.yml on Pieter-Cawood/PyAsyncBTrack

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file pyasyncbtrack-0.9.0-py3-none-any.whl.

File metadata

  • Download URL: pyasyncbtrack-0.9.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

Hashes for pyasyncbtrack-0.9.0-py3-none-any.whl
Algorithm Hash digest
SHA256 86139b609a397b466e8c1630ef0732cb9f70f5bc7b624224fd23784acf01b922
MD5 ee81021d2481298531eb78011dd2bc50
BLAKE2b-256 739300a8d19728b5f9a53b8cf5696e5dd1e4c10cb3c87c2d10306f7db4769f17

See more details on using hashes here.

Provenance

The following attestation bundles were made for pyasyncbtrack-0.9.0-py3-none-any.whl:

Publisher: release.yml on Pieter-Cawood/PyAsyncBTrack

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page