Skip to main content

A Python ORM interface for building and solving clingo ASP programs

Project description

ASPAlchemy

PyPI CI Coverage: 100% License: MIT

ASPAlchemy is a Python ORM for clingo: you author Answer Set Programming rules as typed Python objects, it renders them to ASP source, solves through the clingo API, and hands the answer sets back as typed Python values.

Why ASPAlchemy?

Your rules are Python objects. Predicates are classes with typed fields; atoms are instances; rules are built from both. A misspelled field or a missing argument is a type error before clingo ever runs, and the pieces you use over and over live in ordinary Python variables instead of being typed out again each time.

The code reads as English. program.when(...).derive(...). program.forbid(...). Choice(...).exactly(1). The example below can be read aloud, roughly correctly, by someone who has never seen ASP.

Your program is dynamic. This is the headline. A .lp file is frozen text; ASP written in Python has if statements, loops, functions, and imports, so the rules you emit reshape themselves around the input data and configuration in ways a static program never can. This is what SQLAlchemy did for SQL — hence the name.

You never need to see the clingo code. And just as with SQLAlchemy, the generated language becomes an implementation detail: the whole loop — express the rules, solve, consume the answers — happens in typed Python. The ASP is always there when you want it (render() prints it; the example below shows one), but you can go from problem to solution without reading a line of it.

It emits correct clingo. Illegal ASP is unrepresentable: constructs clingo would reject — unsafe variables, malformed rules, syntax errors — are refused as you build them, so if your Python runs, the generated program runs. And much of that is enforced statically: the type system encodes what may appear where, so autocomplete and your type checker tell you what's permitted before anything runs at all.

It's opinionated, with guardrails. ASPAlchemy deliberately does not admit everything gringo does. Constructs that are nearly always a mistake — singleton variables, negated heads, comparisons against pools — are refused, and every refusal says what you probably meant instead. When you genuinely need the full language, raw_asp() accepts verbatim clingo (with a predicates= seatbelt so its atoms still round-trip typed).

Every exception is a teaching moment. Rules are validated as they're constructed, so errors land on the Python line at fault and explain the fix. For problems only clingo can catch, every generated ASP line carries a reverse map to the Python that authored it: grounding diagnostics come back annotated "generated by yourfile.py:42", and render(annotate=True) prints the full mapping.

Solutions are first-class. Iterate models lazily; run brave/cautious consequence analyses; solve prioritized optimization problems via optimize(); read atoms back as typed objects with plain int/str fields. And when a program grounds big or slow, ground().analyze_grounding() shows exactly which rules blew up.

And it's tested against reality. The test suite holds a hard 100% line-coverage gate, and claims about clingo behavior are never taken on faith from documentation — they're pinned by tests that run gringo and clasp and assert on what actually comes back. Even corners like gringo's arithmetic binding order — which as far as we can tell is documented nowhere — have been nailed down precisely, with the receipts executable in CI.

Example

Color a map so that no two neighbours have the same color:

from aspalchemy import ANY, ASPProgram, Choice, Field, Predicate, Variable

class Edge(Predicate, show=False):
    """States a and b share a border."""
    a: Field[str]
    b: Field[str]

class Color(Predicate, show=False):
    """A color available to paint with."""
    color: Field[str]

class Node(Predicate, show=False):
    """A state on the map, derived from the border list."""
    name: Field[str]

class ColoredNode(Predicate):
    """The color assigned to a state — the solution we read back."""
    name: Field[str]
    color: Field[str]

program = ASPProgram()

# Define the ASP variables our rules will use
N, C, A, B = (Variable(v) for v in "NCAB")

# The map: six states and their borders
borders = [
    ("washington", "oregon"), ("washington", "idaho"),
    ("oregon", "idaho"), ("oregon", "california"), ("oregon", "nevada"),
    ("idaho", "nevada"), ("idaho", "utah"),
    ("california", "nevada"), ("nevada", "utah"),
]
program.fact(*(Edge(a=a, b=b) for a, b in borders))

# Three colors to paint with
colors = ["red", "green", "blue"]
program.fact(*(Color(color=c) for c in colors))

# A state is anything that appears on either side of a border
program.when(Edge(a=N, b=ANY)).derive(Node(name=N))
program.when(Edge(a=ANY, b=N)).derive(Node(name=N))

# Every state picks exactly one color
program.when(Node(name=N)).derive(Choice(ColoredNode(name=N, color=C), condition=Color(color=C)).exactly(1))

# Bordering states never share a color
program.forbid(Edge(a=A, b=B), ColoredNode(name=A, color=C), ColoredNode(name=B, color=C))

model = program.solve().first()
for atom in sorted(model.atoms(ColoredNode), key=lambda c: c.name):
    print(f"{atom.name} -> {atom.color}")  # .name and .color are plain strs
california -> red
idaho -> red
nevada -> blue
oregon -> green
utah -> green
washington -> blue

program.render() shows exactly the clingo program you would have written by hand:

edge("washington", "oregon").
edge("washington", "idaho").
edge("oregon", "idaho").
edge("oregon", "california").
edge("oregon", "nevada").
edge("idaho", "nevada").
edge("idaho", "utah").
edge("california", "nevada").
edge("nevada", "utah").
color("red").
color("green").
color("blue").
node(N) :- edge(N, _).
node(N) :- edge(_, N).
{ colored_node(N, C) : color(C) } = 1 :- node(N).
:- edge(A, B), colored_node(A, C), colored_node(B, C).

#show.
#show colored_node/2.

"But the Python is three times longer!" It is — on a toy. Look at where the length isn't, though: the rules, the part ASPAlchemy actually specializes in, are four statements in both versions, line for line. Most of the extra Python is the predicate declarations, and that cost is fixed: it buys typed fields, autocomplete, and typed answers, and it doesn't grow with the problem. The part that does grow is data — and in real use borders arrives as json.load(...) or a database query, not as facts you retype into a .lp file. Scale the map to fifty states and the Python program doesn't change size; the clingo program does. That's the ORM trade, the same one SQLAlchemy makes: a few declarations up front so that the hundredth rule is as safe as the first.

Why not clorm?

Different job. clorm types the data boundary — facts in, models out, with a relational query layer over solutions. ASPAlchemy types the program itself: the rules are Python objects, validated as you build them, which clorm by design leaves as ASP text. Solution handling here is deliberately minimal (typed atoms, loud failures); if you want to query solutions relationally while writing your rules in ASP, clorm is the right tool. Note that both tools can be used together: ASPAlchemy to define the program, clorm to handle the data.

Installation

pip install aspalchemy    # or: uv add aspalchemy

Requires Python 3.14+ (clingo is installed automatically).

Documentation

For examples of ASPAlchemy at work solving logic puzzles, see aspuzzle, a puzzle-solving framework built on top of it.

License

MIT. Copyright Jolyon Bloomfield 2025-2026.

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

aspalchemy-1.0.1.tar.gz (112.3 kB view details)

Uploaded Source

Built Distribution

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

aspalchemy-1.0.1-py3-none-any.whl (124.1 kB view details)

Uploaded Python 3

File details

Details for the file aspalchemy-1.0.1.tar.gz.

File metadata

  • Download URL: aspalchemy-1.0.1.tar.gz
  • Upload date:
  • Size: 112.3 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: uv/0.11.28 {"installer":{"name":"uv","version":"0.11.28","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

File hashes

Hashes for aspalchemy-1.0.1.tar.gz
Algorithm Hash digest
SHA256 b041c39b383457f1287ed914fa40ddefde202afe9b3afd487c117a0acef10879
MD5 a7b7ed1e566def283865153cc6f835e2
BLAKE2b-256 fa323fcd7fcd51750ce59236ee8817778896cab2ad9ee8de71ef6fa93c1682fc

See more details on using hashes here.

File details

Details for the file aspalchemy-1.0.1-py3-none-any.whl.

File metadata

  • Download URL: aspalchemy-1.0.1-py3-none-any.whl
  • Upload date:
  • Size: 124.1 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: uv/0.11.28 {"installer":{"name":"uv","version":"0.11.28","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

File hashes

Hashes for aspalchemy-1.0.1-py3-none-any.whl
Algorithm Hash digest
SHA256 ad5994e8e225e6a41c55eac0d0520968379ac20e113042dbcf2edefede74d3bb
MD5 ede083e8c6ea035ecffdfd1bbc43085f
BLAKE2b-256 884e5c946cdebdc1e1dfc5bcd2d72a240addc6e23fc81a9c4e4a078e1418c720

See more details on using hashes here.

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