Statements and Terms
If predicates are your data, statements are how you compute with it: what to derive, what to rule out, what must hold. This page is the vocabulary for writing them, and it comes in two halves — the handful of verbs that put statements into a program, and the terms those verbs accept. We’ll walk the verbs first, then each kind of term in turn. (For the predicates these statements operate on, see Predicates and Data.)
The verbs
A program is built from three kinds of statement — facts, rules, and constraints — and the one distinction to hold onto is what each does to the answer set. Facts and rules construct it: a fact puts an atom in unconditionally, a rule puts one in whenever its conditions hold. A constraint constructs nothing — it only takes answer sets away, ruling out any in which a forbidden situation occurs. Nothing but a fact or a rule ever makes an atom true.
Each kind is written with its own verb: fact() asserts a fact, when(...).derive(...) builds a rule,
and a constraint is a forbid() or a require(). (Letting the solver choose what to construct, and
ranking the solutions it finds, are separate verbs on their own pages —
Choices and Aggregates and Solving and Results.)
If you’ve written raw clingo, two things may feel unfamiliar. First, the verbs don’t line up one-to-one
with those three kinds: a rule isn’t a single call but a when() — the conditions — closed with
.derive() — what they produce, and a constraint is a deliberate forbid() or require() rather than a
bare :- line. Second, there are more verbs than kinds: constraints alone come as forbid(), require(),
and their when() forms, and choices and preferences bring their own later. The reason for these new verbs
is simply that we want to make the code readable — when you read a rule constructed in Python, its intent
should be obvious. Under the hood, everything is converted appropriately and renders in standard clingo notation.
We’ll build one small program to demonstrate each verb — a guest list:
from aspalchemy import ANY, ASPProgram, Field, Predicate, Variable
class Guest(Predicate):
name: Field[str]
age: Field[int]
class Adult(Predicate):
name: Field[str]
X, Y = Variable("X"), Variable("Y")
party = ASPProgram()
Facts, rules, and constraints all need to be attached to a program: we’ve called ours party.
Facts
fact(*atoms) asserts atoms unconditionally, and it takes any number of them — as long as they’re
ground (no variables). Here are our first two guests:
party.fact(Guest(name="alice", age=30), Guest(name="bob", age=17))
That makes fact() the natural landing point for your Python data: unpack a generator over your input
rows and the facts write themselves.
guests = [("carol", 41), ("dan", 12)]
party.fact(*(Guest(name=n, age=a) for n, a in guests))
An atom with a variable in it isn’t data though, so fact() refuses it:
>>> party.fact(Guest(name=X, age=30))
Traceback (most recent call last):
...
ValueError: fact() requires grounded predicates, but guest(X, 30) contains variable(s) X. Use when(*conditions).derive(...) to derive predicates.
Rules: when().derive()
Rules derive an atom when a set of conditions holds. Sometimes you’ll see the derived atom called a “head” and
the conditions called a “body”, named after the ASP code head :- body. that describes a rule.
ASPAlchemy splits rules into their two separate components. We start by stating when(*conditions), which
takes one or more conditions, then close the rule with .derive(atom) — the atom to derive when they
hold. Let’s see it in action, where we wish to specify that guests 18 years or older are adults.
party.when(Guest(name=X, age=Y), Y >= 18).derive(Adult(name=X))
Our when() holds two conditions: a Guest atom — matched for every guest that exists — and the comparison Y >= 18.
Because alice is 30 (and carol 41), the rule derives adult("alice") and adult("carol") —
atoms the facts never contained. bob and dan are under 18, so no adult for them. Solving the program
shows the rule’s handiwork:
>>> sorted(a.render() for a in party.solve().first().atoms(Adult))
['adult("alice")', 'adult("carol")']
The two halves when and derive are really one statement, so a when() you never close is an
error at render, and the report points back to the line where you opened it (every statement remembers
the line that authored it — see source locations). Let’s take a look:
>>> Minor = Predicate.define("minor", ["name"], show=False)
>>> pending = party.when(Guest(name=X, age=Y), Y < 18)
>>> party.render()
Traceback (most recent call last):
...
ValueError: Segment 'Rules' has incomplete when() statements: when(guest(X, Y), Y < 18) opened at ...
>>> pending.derive(Minor(name=X)) # closed; the program renders again
derive() is one way to close a when(); another option lets you create a constraint, as we’ll see below.
Two more closers, choose and penalize, belong to choices and optimization. Exactly one
closer finishes each when(), and closing it twice is an error, the same as never closing it.
Constraints: forbid() and require()
Now the third kind of statement — the one that constructs nothing. forbid(*conditions) bans a
combination outright: no answer set may satisfy all its conditions at once. It renders as :- conditions.,
clingo’s own way of writing “this must never happen”.
require(target) says it the other way round: you name the thing that must hold, and the library
forbids its opposite. Requiring an atom forbids its default negation — require(p) renders :- not p —
and requiring a comparison flips it to the inverse. A bare require(target) is a whole constraint on its
own — though that form is fairly rare.
Closing a when() with .require() is a different statement: when(*conditions).require(target) asks that
target hold whenever the conditions do, rendering :- conditions, <target's opposite>. And
crucially, those conditions bind the variables the target uses: Y draws its
values from the guest condition rather than appearing from nowhere.
The final form of constraint that you can use is when(*conditions).forbid(*targets), which is just an
alias for forbid(*conditions, *targets). However, being able to write it as when().forbid() gives you
an option to organize your thoughts around “what is the condition” and “what am I actually forbidding”.
Let’s see a couple of these in action.
party.when(Guest(name=ANY, age=Y)).forbid(Y < 0) # ban: a negative age
party.when(Guest(name=ANY, age=Y)).require(Y < 150) # every guest: age under 150
Here is the whole program now — facts, rules, and constraints together:
>>> print(party.render())
% Generated by aspalchemy ...
guest("alice", 30).
guest("bob", 17).
guest("carol", 41).
guest("dan", 12).
adult(X) :- guest(X, Y), Y >= 18.
minor(X) :- guest(X, Y), Y < 18.
:- guest(_, Y), Y < 0.
:- guest(_, Y), Y >= 150.
#show.
#show adult/1.
#show guest/2.
None of our guests break those constraints, so the program is satisfiable. Solving it returns a single
answer set — the guests, plus the two adults the rule derived (the minors are derived too, but show=False
keeps them out of the report):
>>> model = party.solve().first()
>>> sorted(atom.render() for atom in model.atoms())
['adult("alice")', 'adult("carol")', 'guest("alice", 30)', 'guest("bob", 17)', 'guest("carol", 41)', 'guest("dan", 12)']
Variable binding
That’s all three statements — fact, rule, and constraint. Everything from here on is the terms those verbs accept —
but before the catalogue, one idea sits under all of them, quietly at work in every rule so far:
binding. It’s the answer to a question the examples kept begging — how does the X in
Guest(name=X, age=Y) know which guests to stand for?
The X and Y you’ve been reading are variables: placeholders that hold no value as you write them,
and that solving fills in. clingo does this by grounding — before it searches for answer sets it
instantiates each rule, trying every substitution of concrete terms for the variables that makes the body
true, and turning one rule-with-variables into many ground copies. The adult rule,
adult(X) :- guest(X, Y), Y >= 18., is never solved as written; it’s ground against the four guest
facts, one candidate copy per matching guest.
Where do a variable’s values come from? From the positive body atoms it appears in — the plain,
un-negated predicates in the when(). In that rule guest(X, Y) is the positive atom, and it’s what
binds X and Y: each guest fact is one substitution, so X ranges over the four names and Y over
their ages. Everything else only reads a value some positive atom already supplied. Y >= 18 invents no
ages — it filters the ones guest(X, Y) handed it; adult(X) invents no names — it carries
the X that the body bound. That is the whole reason the rule derives adult("alice") and adult("carol")
and nothing else: those are the substitutions where a bound Y also cleared the comparison.
The flip side is the rule you can’t ground. A variable that shows up only in places that read — in the
head, under a not, or in a comparison — with no positive atom to bind it is unsafe: clingo has no
set of values to range it over. So the discipline is a one-liner: every variable must appear in at least
one positive body atom. ASPAlchemy enforces it and flags a violation at the line that built the rule — the
next section shows it happening. (One term does both: an equality is clingo’s assignment
and binds — X == Y + 1, or a domain X.in_(...) — so it counts as positive too; see
Comparisons.)
Variables
With binding in hand, the mechanics of the variable itself are quick.
Variable("X") is an ASP variable — during solving it ranges over every ground term: numbers, strings,
and whole atoms alike. Names start with an uppercase letter. Two shorthands cover the common cases. The
module-level V (a ready-made Vars instance) mints variables by attribute access, so V.Room is
Variable("Room") with no declaration first; and ANY is the anonymous variable _ for a don’t-care
position, as in the Edge(a=N, b=ANY) rules of
the tutorial. ASPAlchemy is slightly stricter on
variable names than clingo: all anonymous variables must be simply ANY; _X is disallowed.
>>> from aspalchemy import V
>>> V.Room.render()
'Room'
>>> ANY.render()
'_'
A variable that appears exactly once in a rule is almost always a typo, so ASPAlchemy rejects it.
The fix is either the variable you actually meant, or ANY to say the don’t-care out loud.
(Note that this is stricter than clingo, which will accept a singleton, but it can lead to
unexpected results, see deliberate strictnesses.)
Reading = Predicate.define("reading", ["sensor", "value"], show=False)
prog = ASPProgram()
>>> prog.forbid(Reading(sensor=X, value=Y), X > 0) # Y used exactly once
Traceback (most recent call last):
...
ValueError: Singleton variable(s) Y in rule: :- reading(X, Y), X > 0.
A variable used exactly once is usually a typo; use ANY for an intentional don't-care.
Unsafe variables get the same treatment — a variable that appears in the derived atom or a negative condition
but that no positive condition binds — flagged at the line that built the rule. Here X and Y sit
only under a not, where nothing binds them:
>>> prog.forbid(~Reading(sensor=X, value=Y)) # X, Y appear only under `not`
Traceback (most recent call last):
...
ValueError: Unsafe variable(s) X, Y in rule: :- not reading(X, Y).
Every variable must be bound by a positive body literal (or an equality with something bound).
Comparisons
Variables become conditions once you compare them. The Python comparison operators — ==, !=, <,
<=, >, >= — applied to variables, numbers, expressions, or aggregates build Comparison terms,
and a comparison joins the conditions like any other; the Y >= 18 back in the first rule was
one. Arithmetic composes underneath, so X + 1 < Y * 2 is a comparison over expressions (the operator
table and the clingo-vs-Python fine print are in Arithmetic), and the right-hand side can even
be a whole atom: X == Cell(row=1, col=2) compares against a nested term.
There’s a catch that falls out of == building a term: a comparison has no truth value. So if X == Y:
is almost always a bug, and ASPAlchemy makes it a loud one rather than letting you take a silently wrong
branch. Chained comparisons like X < Y < Z hit the same wall — Python evaluates them as
(X < Y) and (Y < Z), which needs a truth value in the middle — so pass each one separately:
when(X < Y, Y < Z). (Atoms are the exception: they’re ordinary data, so == between two atoms is plain
Python equality — see atom identity.)
>>> comparison = X < Y # a term, not a bool
>>> comparison.render()
'X < Y'
>>> if X == Y:
... pass
Traceback (most recent call last):
...
TypeError: A Comparison (X = Y) has no boolean value: comparison operators on aspalchemy terms build ASP terms rather than evaluating them. ...
Equality is clingo’s binding assignment: X == Y + 1 renders as X = Y + 1, and — as in clingo — that
equality binds X, so it counts as a positive condition for the safety check. It’s
the one comparison operator that binds — <, >, != and the rest only filter values something else
supplied.
>>> (X == (Y + 1)).render()
'X = Y + 1'
Default negation
Some conditions are about what isn’t there. ~Booked(room=R) reads “unless the room is known to be
booked” — that’s default negation, or negation as failure: the condition holds when the atom is not
derivable, which is ASP’s way of saying “absent from this world” rather than “provably false.” It’s the
workhorse behind rules like “a room with no booking is free”:
Room = Predicate.define("room", ["r"], show=False)
Booked = Predicate.define("booked", ["room"], show=False)
Free = Predicate.define("free", ["room"])
R = Variable("R")
hotel = ASPProgram()
hotel.fact(Room(r=1), Room(r=2), Booked(room=2))
hotel.when(Room(r=R), ~Booked(room=R)).derive(Free(room=R))
>>> print(hotel.render())
% Generated by aspalchemy ...
room(1).
room(2).
booked(2).
free(R) :- room(R), not booked(R).
#show.
#show free/1.
>>> model = hotel.solve().first()
>>> [atom.render() for atom in model.atoms(Free)]
['free(1)']
Room 1 has no booked fact, so free(1) is derived; room 2’s booking blocks it. Notice which condition
carries the weight for binding: R is bound by the positive Room(r=R), and the
negated ~Booked(room=R) only reads it — a not condition can never bind a variable, which is why a
rule negating the only occurrence of a variable is unsafe. One practical note: that
Booked(room=2) fact is load-bearing. If a default-negated predicate is never derived anywhere — no
fact, no rule — gringo flags it (“atom does not occur in any rule head”). Raw clingo shrugs and
grounds anyway (the negation trivially holds), but a never-derivable atom in a condition is usually a
misspelled predicate name, so ASPAlchemy makes that message loud at
solve time by default.
~ and the named form Not() are the same operation — use whichever reads better. (The other negation,
the classical minus sign that’s part of the atom itself, lives in
Predicates and Data.)
The fine print, for when you nest negations or negate a comparison:
- On atoms, a double negation is preserved:
not not pis not equivalent topunder stable-model semantics, and a triple collapses to a single. (Contrast arithmetic, where-andComplreally are involutions, so a doubled one collapses — see doubled unary operators.) - On a plain comparison,
~/Not()return the complement rather than wrapping:Not(X != 5)is the binding equalityX = 5. That’s gringo’s own normalization, done at construction where you can see it. - A comparison carrying an aggregate keeps the
notwrapper — a negated aggregate literal can’t be complement-flipped.
>>> from aspalchemy import Count, Not
>>> b = Booked(room=R)
>>> Not(Not(b)).render() # double survives
'not not booked(R)'
>>> Not(Not(Not(b))).render() # triple collapses
'not booked(R)'
>>> Not(X != 5).render() # complement, not wrapper
'X = 5'
>>> Not(Count(X, condition=Booked(room=X)) > 2).render() # aggregates keep "not"
'not #count{ X : booked(X) } > 2'
Conditional literals
A conditional literal is clingo’s p(X) : q(X). In a rule it means “for every X where q(X)
holds, p(X) holds too.” A picture that might help: p(X) is a key and q(X) a lock,
and the literal holds when every lock has a matching key. Keys without locks are fine; a lock with no key
fails.
from aspalchemy import ConditionalLiteral
Cell = Predicate.define("cell", ["id"], show=False)
Covered = Predicate.define("covered", ["id"], show=False)
AllCovered = Predicate.define("all_covered", [])
board = ASPProgram()
board.when(ConditionalLiteral(Covered(id=X), Cell(id=X))).derive(AllCovered())
>>> print(board.render())
% Generated by aspalchemy ...
all_covered :- covered(X) : cell(X).
#show.
#show all_covered/0.
You build a ConditionalLiteral by hand in exactly two places: rule bodies (as above) and show_when()
(conditional visibility). Conditional structure shows up elsewhere
too — in the elements of choices and aggregates — but those build their own
elements through add(), so you never hand them one.
Pools and ranges
A pool is clingo’s several-values-at-once notation: RangePool(1, 5) renders as 1..5, and
ExplicitPool([1, 3, 5]) as (1; 3; 5). Usually you let the pool() helper pick — it takes plain Python
ranges, lists, and tuples, turning a step-1 range into a RangePool and everything else into an
ExplicitPool:
>>> from aspalchemy import pool
>>> pool(range(1, 6)).render()
'1..5'
>>> pool([1, 3, 5]).render()
'(1; 3; 5)'
>>> pool(["a", "b"]).render()
'("a"; "b")'
>>> pool(range(1, 10, 2)).render()
'(1; 3; 5; 7; 9)'
Pools show up in exactly two positions. As a predicate argument, one atom expands to many — a whole
row of the board in a single fact():
Slot = Predicate.define("slot", ["n"], show=False)
grid = ASPProgram()
grid.fact(Slot(n=pool(range(1, 4))))
>>> print(grid.render())
% Generated by aspalchemy ...
slot(1..3).
#show.
And on the right of a comparison, where X.in_(...) is the idiomatic way to spell a domain
restriction:
>>> X.in_(range(1, 6)).render()
'X = 1..5'
>>> X.in_([2, 4, 8]).render()
'X = (2; 4; 8)'
Note that this usage counts as binding the variable X.
Those two are the only legal positions. A bare pool sitting alone as a rule element is a clingo syntax
error, so ASPAlchemy refuses it at construction — along with the subtler shapes (a negated pool
comparison, a pool inside require()) whose clingo reading is never what the code seems to say: see
better errors, not restrictions.
Constants and extremes
define_constant(name, value) registers a clingo #const and hands back a DefinedConstant you use in
rules — one named number, string, or ground atom threaded through the program and defined in exactly one place:
config = ASPProgram()
max_size = config.define_constant("max_size", 10)
Size = Predicate.define("size", ["n"], show=False)
N = Variable("N")
config.when(Size(n=N)).require(N <= max_size)
>>> print(config.render())
% Generated by aspalchemy ...
#const max_size = 10.
:- size(N), N > max_size.
#show.
A str value renders as a quoted ASP string — "n" is never the bare symbol n. For a symbolic
constant, pass a ground atom instead: define_constant("dir", North()) with
North = Predicate.define("n", []) renders #const dir = n.
Two extremes round out the vocabulary:
SUP and INF render as clingo’s #sup and #inf, the greatest and least terms of the ordering. Their
everyday use is the empty-set answer — Min over nothing is #sup (and Max is #inf) — so comparing
against SUP is really asking “was the set empty?”
>>> from aspalchemy import INF, SUP
>>> SUP.render()
'#sup'
>>> INF.render()
'#inf'