Arithmetic
In ASPAlchemy, you write math expressions using standard Python notation, with Python’s usual precedence, and should almost never need to think about what’s going on in clingo. The renderer translates your Python expressions into clingo, adding parentheses where needed so that what Python thinks is going on is exactly how clingo sees it too.
There was an “almost” in there. Clingo works only with 32-bit integers, and its integer arithmetic differs from Python’s in a small number of edge cases. ASPAlchemy can’t change how clingo evaluates an expression, so those are the places where you do need to know the difference.
This page covers how the operators map between the two languages, the rules of integer math in clingo, the edges of the 32-bit range, and what happens when you divide by zero.
Operators and precedence
| Python | ASP rendering | Notes |
|---|---|---|
x + y |
x + y |
|
x - y |
x - y |
|
x * y |
x * y |
|
x // y |
x / y |
integer division; Python’s / is refused |
x % y |
x \ y |
modulo |
x ** y |
x ** y |
right-associative in ASP |
x & y |
x & y |
bitwise and |
x \| y |
x ? y |
bitwise or |
x ^ y |
x ^ y |
bitwise xor |
-x |
-x |
refused at the int32 floor |
Compl(x) |
~x |
bitwise complement; Python’s ~ is reserved for default negation |
abs(x) |
\|x\| |
refused at the int32 floor |
Bitwise complement is spelled Compl(x) because ~ already means
default negation on literals. Write ~X on a variable and
ASPAlchemy tells you so:
from aspalchemy import Number, Variable
X, Y = Variable("X"), Variable("Y")
>>> ~X
Traceback (most recent call last):
...
TypeError: ~ is default negation and applies to literals ...
ASPAlchemy uses Python’s precedence rules, and renders parentheses to ensure that gringo interprets
expressions as intended. In case you’re curious though, gringo’s precedence goes: ^, then ?,
then &, then + -, then * / \, then ** (right-associative), then unary - and complement.
Two traps for Python intuitions: the bitwise operators bind looser than +, and unary minus
binds tighter than ** (in gringo -2**2 is (-2)**2 = 4; in Python it is -(2**2) = -4).
Generally speaking, ASPAlchemy emits as few parentheses as possible. Power and the bitwise operators always get explicit parentheses, so nobody has to remember gringo’s table to read the output.
a, b, c = Number(1), Number(2), Number(3)
>>> (a + b * c).render() # classic arithmetic: minimal parentheses
'1 + 2 * 3'
>>> (a - (b - c)).render() # have to keep these parentheses
'1 - (2 - 3)'
>>> (a + (b - c)).render() # ... but a `+` parent has nothing to protect
'1 + 2 - 3'
>>> (a + b**c).render() # power and bitwise: explicit parentheses, always
'1 + (2 ** 3)'
>>> (a**b**c).render()
'1 ** (2 ** 3)'
>>> ((a & b) | c).render()
'(1 & 2) ? 3'
>>> (-(b**b)).render() # Python parses -2**2 as -(2**2); gringo is told this explicitly
'-(2 ** 2)'
ASPAlchemy also tidies negative numbers so the rendering reads the way you would write it by hand:
X + -1 is spelled X - 1, and a subtracted negative turns into an addition. Both spellings are
legal ASP, so this only changes how the term reads, never what it means.
>>> (X + Number(-1)).render()
'X - 1'
>>> (X - (-Y)).render()
'X + Y'
Integer math
Clingo has integers and nothing else: no floats, and no arithmetic on strings. Python’s / is the
first division most people reach for, but it produces a float, so ASPAlchemy refuses it and points
you at //:
>>> X / 2
Traceback (most recent call last):
...
TypeError: clingo has no true division; use // ...
Integer division throws away the remainder, which means that regrouping a product can change its
value. So ASPAlchemy is cautious here: the right operand of * keeps its parentheses, because its
own left term may hide a division that regrouping would not preserve. 5 * (7 / 3 * 2) is 20, while
the same characters regrouped left to right, 5 * 7 / 3 * 2, are 22.
>>> (Number(5) * ((Number(7) // Number(3)) * Number(2))).render() # Number(...) stops Python doing the math
'5 * (7 / 3 * 2)'
>>> (Number(5) * Number(3) * Number(2)).render() # left-nested: already in order
'5 * 3 * 2'
Strings have no arithmetic in clingo at all, for any operator, so a string operand is refused when you write it rather than turning into an undefined operation at grounding:
>>> from aspalchemy import String
>>> String("a") + 1
Traceback (most recent call last):
...
TypeError: Strings have no arithmetic in clingo ...
Within the integers, there are two places where clingo and Python give different answers.
Integer division and modulo round differently on negatives. Python floors: -7 // 2 == -4 and
-7 % 2 == 1. Clingo truncates toward zero: -7/2 == -3 and -7\2 == -1. On non-negative operands
they agree.
Negative exponents. Python’s 2 ** -1 is 0.5, a float. Clingo evaluates 2**(-1) to 0, and
likewise for every base but one: 0**(-1) is undefined, and behaves like a
division by zero.
These come from how clingo evaluates expressions, and ASPAlchemy cannot compensate for them. To see
it happen, here is clingo computing -7 // 2, next to Python doing the same:
>>> from aspalchemy import ASPProgram, Predicate
>>> Result = Predicate.define("result", ["value"])
>>> program = ASPProgram()
>>> program.fact(Result(value=Number(-7) // Number(2)))
>>> [atom["value"].value for atom in next(iter(program.solve())).atoms(Result)]
[-3]
>>> -7 // 2 # what Python would have said for the same operands
-4
Some good news though: the bitwise operators agree with Python exactly, negatives included. Both use
the same infinite-precision two’s-complement definitions (-5 & 3 == 3, ~(-6) == 5).
Working with int32
Clingo integers are 32 bits, and they wrap silently. 100000 * 100000 evaluates to 1410065408
(that is 10^10 mod 2^32) and 2**40 evaluates to 0, with no warning of any kind. Python integers
never overflow, so if your arithmetic can go past ±2^31, clingo will compute confidently wrong
answers.
The floor of the range, −2147483648, is the sharpest edge. It is the one value with no positive counterpart, so every operation that negates is a silent no-op on it. Here are four of them, each returning the floor unchanged where Python returns 2147483648:
| clingo | Python | |
|---|---|---|
-(-2147483648) |
−2147483648 | 2147483648 |
\|-2147483648\| |
−2147483648 | 2147483648 |
-2147483648 / -1 |
−2147483648 | 2147483648 |
-2147483648 * -1 |
−2147483648 | 2147483648 |
ASPAlchemy refuses the first two when you write them against the literal, rather than emitting a term that quietly does nothing:
>>> -Number(-2147483648)
Traceback (most recent call last):
...
ValueError: -(-2147483648) is a no-op in clingo: ...
>>> abs(Number(-2147483648))
Traceback (most recent call last):
...
ValueError: abs(-2147483648) is a no-op in clingo: ...
The other two can’t be caught that way, because X / -1 is only a problem once X turns out to be
the floor, and nothing knows that until grounding. The floor itself is a perfectly good value to
hold (clingo hands it back from any overflow), and adding to it or taking Compl of it are both
correct. Terms that merely contain it are left as written:
>>> (X + Number(-2147483648)).render()
'X + (-2147483648)'
A word of caution: because of the floor, |x| is not always non-negative. ASPAlchemy refuses
abs() of the literal, but it cannot know when a variable holds the floor, so abs(X) in a
constraint is not a guaranteed magnitude. If your values can reach the floor, bound them first.
Division by zero
Raw clingo quietly drops any ground instance of a rule whose arithmetic is undefined, such as a
division or modulo by zero, or 0**(-1). All it emits is an info-level message, and your program
silently shrinks. ASPAlchemy’s default stop_on_log_level=INFO turns that message into a
GroundingError, so a division by zero fails loudly instead. The error points at the line of
Python that wrote the expression; see Clingo’s messages for what
it looks like, and for how to relax the threshold if you really do want clingo’s behavior.