Escape Hatches
ASPAlchemy supports most of clingo, but not all of it, and sooner or later you may need something it
doesn’t do: a disjunctive head, a #project directive, a solver option, or a call into Python
during grounding. The list of what isn’t supported, and why, is in
What We Don’t Support. This page is about how to get at those things anyway.
Using one of these doesn’t mean giving up on the rest of the library. Atoms derived by raw ASP text still come back as typed Python objects, clingo’s messages still point at your Python, and solving works just the same.
raw_asp(): verbatim clingo
raw_asp(text, predicates=()) adds a block of ASP text to the program, exactly as written. You can
call it on the program (which writes to the default segment) or on any
named segment. Here it’s used for a disjunctive head, which
ASPAlchemy doesn’t support directly, and the answer sets still come back as typed atoms:
from aspalchemy import ASPProgram, Field, Predicate
class Left(Predicate):
pass
class Right(Predicate):
pass
duo = ASPProgram()
duo.raw_asp("left ; right.", predicates=[Left, Right])
The text appears in the render as written, along with #show directives for the predicates you
declared:
>>> print(duo.render())
% Generated by aspalchemy ...
left ; right.
#show.
#show left/0.
#show right/0.
>>> sorted(sorted(a.render() for a in m.atoms()) for m in duo.solve())
[['left'], ['right']]
The catch is that ASPAlchemy doesn’t check raw text as you write it, so a typo inside it only shows
up when clingo grounds the program. You do still get help there: clingo’s messages about raw text
point back to the line of Python that called raw_asp(). See
Clingo’s messages.
The predicates= seatbelt
ASPAlchemy can’t see inside raw text, so predicates= is how you tell it which predicates the text
uses. Every predicate in the text has to be declared somewhere, and this is checked when the program
is grounded. Declaring a class does two things: it adds a #show directive so the predicate appears
in models, and it tells ASPAlchemy which class to read those atoms back into.
If the text derives classically negated atoms, you
need to declare -P as well as P. Declaring just P is enough to read negated atoms back, but
only shows the positive ones, so the negated atoms would be missing from your models:
class Status(Predicate):
node: Field[str]
belt = ASPProgram()
belt.raw_asp('status("a"). -status("b").', predicates=[Status, -Status])
>>> print(belt.render())
% Generated by aspalchemy ...
status("a"). -status("b").
#show.
#show -status/1.
#show status/1.
>>> model = belt.solve().first()
>>> sorted(a.render() for a in model.atoms(Status))
['-status("b")', 'status("a")']
If you forget to declare a predicate, grounding fails with an error naming it:
>>> mystery = ASPProgram()
>>> mystery.raw_asp("hidden_gem(7).")
>>> mystery.solve().first()
Traceback (most recent call last):
...
ValueError: The grounded program contains predicates never declared to aspalchemy: hidden_gem/1. raw_asp blocks must declare every predicate occurring in their text via predicates=[...]; ...
To keep a declared predicate out of your models, define its class with show=False, or hide() it
on the program, as usual. This is also the fix if raw text produces atoms containing strings with
escape characters (like \" or \n): ASPAlchemy can’t read those back, so hide() that class and
the rest of the model reads normally.
Blocks are lexically self-contained
Each raw block has to stand on its own. If a block opens a %* block comment or a #script and
doesn’t close it, it is refused:
>>> scratch = ASPProgram()
>>> scratch.raw_asp("%* this comment never closes")
Traceback (most recent call last):
...
ValueError: raw_asp() text opens a %* block comment that never closes: every raw block must be lexically self-contained. ...
Clingo itself would let a comment opened in one block close in a later one, but everything in
between would quietly disappear, including statements you wrote with ASPAlchemy. For the same
reason, a #script block has to start and end inside a single raw_asp() call.
Blocked directives
Four directives are refused inside raw blocks, and each error tells you what to use instead:
#const: usedefine_constant()instead. Constants you define that way are always in the program, so raw text can use them freely.#programand#external: ASPAlchemy grounds the whole program in one go, so anything after a#programdirective would silently never be grounded. These belong to clingo’s incremental solving, which isn’t supported yet. If you want to switch an atom on and off between solves, make it a choice and fix it withassumptions=.#include: you’re writing Python, so read the file with Python and pass in its text.
>>> scratch.raw_asp("#const width = 9.")
Traceback (most recent call last):
...
ValueError: raw_asp() text contains #const — register it with define_constant() instead: ...
Calling Python during grounding
Clingo lets you call Python functions during grounding with @-terms, and you can use them in
raw_asp() text. An @f(X) term calls a method on a context object that you pass in as
ground(context=obj), so @double(N) calls obj.double(N). solve(), brave(), cautious() and
optimize() take context= as well.
import clingo
class Doubler:
def double(self, n: clingo.Symbol) -> clingo.Symbol:
return clingo.Number(n.number * 2)
class Base(Predicate, show=False):
n: Field[int]
class Doubled(Predicate):
n: Field[int]
twice: Field[int]
calls = ASPProgram()
calls.fact(Base(n=2), Base(n=21))
calls.raw_asp("doubled(N, @double(N)) :- base(N).", predicates=[Doubled])
@double(N) is called during grounding, once for each base fact:
>>> model = calls.ground(context=Doubler()).solve().first()
>>> sorted((d.n, d.twice) for d in model.atoms(Doubled))
[(2, 4), (21, 42)]
This is plain clingo, so there’s no typed layer here: your methods take and return clingo.Symbol
values, and it’s up to you to keep them in step with the text. You can also put the Python directly
in the ASP text with a #script block, as long as it fits inside one raw_asp() call.
One thing to watch out for: ground_text() and
aspif(), and the statement profile, all ground the
program again, using the same context. If your functions keep state and give different answers on
later calls, those may not match your original grounding.
Solver options and the raw Control
grounded.control is the underlying clingo.Control, which gives you everything in the clingo API:
configuration, externals, theory atoms, observers and so on. ASPAlchemy’s guarantees stop here, so
change things between solves, never during one, and use ASPAlchemy’s own methods where they exist.
This is where most of clingo’s command-line options go. For -t 4, --seed or
--configuration=jumpy, set the matching key on grounded.control.configuration after
ground() and before you solve. Clingo’s documentation lists
the options. --time-limit doesn’t need any of this: it’s timeout= on solve() and the other
verbs. The clingo map has the rest.
Two directives need a solver option before they do anything: #project does nothing unless
projection is turned on, and #heuristic does nothing unless the Domain heuristic is turned on.
Without the option, the directive is quietly ignored:
from aspalchemy import Choice
class Pick(Predicate):
x: Field[int]
class Audited(Predicate):
pass
projected = ASPProgram()
picks = Choice(Pick(x=1))
picks.add(Pick(x=2))
projected.choose(picks)
projected.choose(Choice(Audited()))
projected.raw_asp("#project pick/1.")
grounded = projected.ground()
>>> len(list(grounded.solve())) # audited doubles every pick set: #project is ignored
8
>>> grounded.control.configuration.solve.project = "project" # type: ignore[union-attr] # clingo types config knobs loosely
>>> len(list(grounded.solve())) # one model per pick set
4
>>> grounded.control.configuration.solver.heuristic = "Domain" # type: ignore[union-attr] # the #heuristic knob
With projection on, models that only differ in audited count as one, so eight models become four.
Clingo symbol interop
If you work with the clingo API directly (externals on the raw Control, your own on_model
callback, or another clingo library), you’ll need to convert between typed atoms and
clingo.Symbol objects. convert_predicate_to_symbol and convert_symbol_to_predicate do that,
including nested predicates and classical negation:
from aspalchemy import convert_predicate_to_symbol, convert_symbol_to_predicate
class Cell(Predicate):
row: Field[int]
col: Field[int]
>>> sym = convert_predicate_to_symbol(-Cell(row=1, col=2)) # a clingo.Symbol
>>> str(sym)
'-cell(1,2)'
>>> back = convert_symbol_to_predicate(sym, {("cell", 2): Cell})
>>> back == -Cell(row=1, col=2)
True
>>> assert isinstance(back, Cell) # the converter returns Predicate; narrow to read fields typed
>>> back.negated, back.row
(True, 1)
Going from a symbol back to a predicate needs a mapping from (name, arity) to the class to use.
Incremental-ish solving today
If you want to ask lots of questions of one program, use
ground() plus assumptions: ground once, then fix different
atoms true or false for each solve. Clingo’s full incremental solving, with #program parts, is
not supported yet.