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.

Write Clingo using Python

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. Code that clingo would reject — unsafe variables, malformed rules, syntax errors — is refused as you build it, at the Python line at fault. The net is deliberately one-sided: every refusal is a certain clingo rejection, and the few shapes only grounding can judge (arithmetic gringo can't invert, a #const bound whose value only grounding knows) still fail loudly at solve, mapped back to your source. 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: you pay for the schema once, and the program you maintain stops growing while the problem it solves keeps going.

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.2.tar.gz (112.9 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.2-py3-none-any.whl (124.4 kB view details)

Uploaded Python 3

File details

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

File metadata

  • Download URL: aspalchemy-1.0.2.tar.gz
  • Upload date:
  • Size: 112.9 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.2.tar.gz
Algorithm Hash digest
SHA256 3449ebddb45fc1f28a78f67798371c35148e43ad4ae6f7964203e5495792e515
MD5 0b361835c0e191bb124937e469166507
BLAKE2b-256 06d113ce6912aa9285676d71258f9cac5394ea00a0be6ebb428dbdaaa502601e

See more details on using hashes here.

File details

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

File metadata

  • Download URL: aspalchemy-1.0.2-py3-none-any.whl
  • Upload date:
  • Size: 124.4 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.2-py3-none-any.whl
Algorithm Hash digest
SHA256 1bc685caefecb6e3c427eb87415520d9f146150f142f1af2969d3e4c000b16ac
MD5 0a0c911abf5476ca67bdabbaeafab302
BLAKE2b-256 b0efc61c3e6a15ed9e3cc45b68ab7454a5dfe3ba9b3e8078d40d724592b11dfb

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