Handy python wrapper around Potassco's Clingo ASP solver.
Clyngor offers multiple interfaces. The followings are all equivalent. (they search for formal concepts)
from clyngor import ASP, solve
answers = ASP("""
rel(a,(c;d)). rel(b,(d;e)).
obj(X):- rel(X,_) ; rel(X,Y): att(Y).
att(Y):- rel(_,Y) ; rel(X,Y): obj(X).
:- not obj(X):obj(X).
:- not att(Y):att(Y).
""")
for answer in answers:
print(answer)The same, but with the lower level function expecting files:
answers = solve(inline="""
rel(a,(c;d)). rel(b,(d;e)).
obj(X):- rel(X,_) ; rel(X,Y): att(Y).
att(Y):- rel(_,Y) ; rel(X,Y): obj(X).
:- not obj(X):obj(X).
:- not att(Y):att(Y).
""")More traditional interface, using file containing the ASP source code:
answers = solve('concepts.lp'): # also accepts an iterable of fileMore examples are available in the unit tests.
Once you get your answers, clyngor allows you to specify the answer sets format using builtin methods:
for answer in answers.by_predicate.first_arg_only:
print('{' + ','.join(answer['obj']) + '} × {' + ','.join(answer['att']) + '}')And if you need a pyasp-like interface:
for answer in answers.as_pyasp:
print('{' + ','.join(a.args()[0] for a in answer if a.predicate == 'obj')
+ '} × {' + ','.join(a.args()[0] for a in answer if a.predicate == 'att') + '}')Currently, there is only one way to see all chaining operator available:
the source code of the Answers object.
(or help(clyngor.Answers))
clyngor can reach clingo in two ways: by running the binary as a subprocess and parsing its
output, or through the official clingo python module and its API objects.
Which one is used, and where the binary lives, is described by a Solver:
import clyngor
from clyngor import Solver
clyngor.solve('file.lp', solver=Solver(backend='module'))
clyngor.solve('file.lp', solver=Solver(binary_path='path/to/clingo'))A Solver is immutable — derive a new one with using() — and validates itself at construction,
rather than at solving time:
Solver().using(backend='module') # Solver(backend='module', binary_path='clingo')
Solver(backend='pyclingo') # ValueError: backend must be one of 'auto', ...Its backend is one of:
| backend | behavior |
|---|---|
'auto' (default) |
the binary if there is one, the clingo module otherwise |
'binary' |
the binary, raising SolverUnavailableError if it is unreachable |
'module' |
the clingo module, raising SolverUnavailableError if it is not installed |
'auto' means pip install clingo alone is a working setup: that package ships no executable,
and clyngor falls back to the module rather than failing.
When no solver is passed, clyngor uses a module-level default one, which you may read with
clyngor.default_solver(), replace with clyngor.set_default_solver(...), or swap for the
duration of a block:
with clyngor.using_solver(backend='module'):
clyngor.solve('file.lp') # goes through the clingo moduleNote that a default solver is process-wide state, so using_solver is not thread safe;
passing a Solver to solve() is.
If the used version of clingo is compiled with python, you can put python code into your ASP code as usual. But if you also have the clingo package installed and importable, clyngor can use it for various tasks.
Using the official API leads to the following changes :
- both robust and quick parsing, instead of the simple vs slow method
- some options are not supported : constants, time-limit, parsing error handling, decoupled grounding/solving
- statistics come as clingo's nested dicts, where the binary yields the flat mapping parsed out of its output
is_unsatisfiableandis_unknownare not reported
Passing use_clingo_module=False to clyngor.solve restricts it to the binary, whatever the solver says.
For users putting some python in their ASP, clyngor may help. The only condition is to have clingo compiled with python support, and having clyngor installed for the python used by clingo.
Clyngor provides converted_types function,
allowing one to avoid boilerplate code based on type annotation when
calling python from inside ASP code.
Example (see tests for more):
#script(python)
from clyngor.upapi import converted_types
@converted_types
def f(a:str, b:int):
yield a * b
yield len(a) * b
#end.
p(X):- (X)=@f("hello",2).
p(X):- (X)=@f(1,2). % ignored, because types do not matchWithout converted_types, user have to ensure that f is a function returning a list,
and that arguments are of the expected type.
Note that the incoming clingo version is leading to that flexibility regarding returned values.
Propagators are presented in this paper. They are basically active observers of the solving process, able for instance to modify truth assignment and invalidate models.
As shown in clyngor/test/test_propagator_class.py, a high-level propagator class built on top of the official API is available, useful in many typical use-cases.
As shown in examples/pyconstraint.lp, clyngor also exposes some helpers for users wanting to create propagators that implement an ASP constraint, but written in Python:
#script(python)
from clyngor import Constraint, Variable as V, Main
# Build the constraint on atom b
def formula(inputs) -> bool:
return inputs['b', (2,)]
constraint = Constraint(formula, {('b', (V,))})
# regular main function that register given propagator.
main = Main(propagators=constraint)
#end.
% ASP part, computing 3 models, b(1), b(2) and b(3).
1{b(1..3)}1.An idea coming from the JSON decoders, allowing user to specify how to decode/encode custom objects in JSON. With clyngor, you can do something alike for ASP (though very basic and only from ASP to Python):
import clyngor, itertools
ASP_LIST_CONCEPTS = """ % one model contains all concepts
concept(0).
extent(0,(a;b)). intent(0,(c;d)).
concept(1).
extent(1,(b;e)). intent(1,(f;g)).
concept(2).
extent(2,b). intent(2,(c;d;f;g)).
"""
class Concept:
"Decoder of concepts in ASP output"
def __init__(self, concept:1, extent:all, intent:all):
self.id = int(concept[0])
self.extent = frozenset(arg for nb, arg in extent if nb == self.id)
self.intent = frozenset(arg for nb, arg in intent if nb == self.id)
def __str__(self):
return f"<{self.id}: {{{','.join(sorted(self.extent))}}} × {{{','.join(sorted(self.intent))}}}>"
objects = clyngor.decode(inline=ASP_LIST_CONCEPTS, decoders=[Concept])
print('\t'.join(map(str, objects)))This code will print something like:
<2: {b} × {c,d,f,g}> <0: {a,b} × {c,d}> <1: {b,e} × {f,g}>
Note the use of annotations to declare that each concept must be associated to one instance,
and that all extent and intent must be sent to constructor for each object.
See tests for complete API example.
Remaining features for a good decoder support:
- encoding: try to more-or-less automatically build the python to ASP compiler
- more available annotations, for instance
(3, 5)(to ask for between 3 and 5 atoms to be associated with the instance), orany(exact meaning has to be found) - allow to raise an InvalidDecoder exception during decoder instanciation to get the instance discarded
Clyngor is basically the total rewriting of pyasp, which is now abandoned.
For an ORM approach, give a try to clorm.
pip install clyngor
You need clingo, in either of its two forms:
- the
clingobinary in your path — through a system installation depending on your OS, through downloading and (compilation and) manual installation, or withpip install clyngor-with-clingo, which ships one. - the python clingo module,
with
pip install clingo. That package ships no executable, so clyngor then solves through the module — see choosing how clingo is reached.
Having both is the most comfortable: clyngor prefers the binary, which supports options the module path rejects, and uses the module for what only it can do.
By default, clyngor uses a very simple parser (yeah, str.split) in order to achieve time efficiency in most time.
However, when asked to compute a particular output format (like parse_args) or an explicitely careful parsing,
clyngor will use a much more robust parser (made with an arpeggio grammar).
See the utils module and its tests,
which provides high level routines to save and load answer sets.
import clyngor
from clyngor import Solver
clyngor.solve('file.lp', solver=Solver(binary_path='path/to/clingo'))Note that it will be the very first parameter to subprocess.Popen.
The solve function also supports the clingo_bin_path parameter, which overrides the solver's.
To point every call of a whole block at a given binary:
with clyngor.using_solver(binary_path='clingo454'):
clyngor.solve(...) # will use clingo454, unless clingo_bin_path or solver is givenThe decorator with_clingo_bin does the same around a function:
import clyngor
@clyngor.with_clingo_bin('clingo454')
def sequence():
...
clyngor.solve(...) # will use clingo454, not clingo, unless clingo_bin_path is given
...The solve functions allow to pass explicitely some parameters to clingo
(including number of models to yield, time-limit, and constants).
Using the options parameter is just fine, but with the explicit parameters some verifications
are made against data (mostly about type).
Therefore, the two followings are equivalent ; but the first is more readable and will crash earlier with a better error message if n is not valid:
solve('file.lp', nb_model=n)
solve('file.lp', options='-n ' + str(n))No.
Yes.
No, it's pronounced clyngor.
Clyngor was designed to not require the official module, because it required a manual compilation and installation of clingo. However, because of the obvious interest in features and performances, the official module can be used by clyngor if it is available.
Since 1.0, it is also what clyngor falls back to when no binary is reachable, so the module alone is enough to solve. Which one is used is decided by the solver.
- timeout in addition to time-limit
- ASP source code debugging generator (started in clyngor-parser)
- bioinformatics, to encode biological pathway logic in pathmodel and Menetools, and for community detection.
- mathematics, to encode some FCA-related task such as AOC-poset generation or concept search, and graph compression or graph manipulation in the context of graph theory.
- visualization, with Draco, a formalization of visualization design knowledge as constraints, and biseau, an ASP-to-graph compiler.
- web applications, for a sudoku solver made with ASP.
- 1.0.0
-
how clingo is reached is now an explicit object,
clyngor.Solver, instead of module-level mutable state. See choosing how clingo is reached. -
clyngor falls back to the clingo module when no binary is reachable, instead of failing with a
FileNotFoundError.pip install clingoalone is now a working setup. -
an unreachable clingo raises
clyngor.SolverUnavailableError(aRuntimeError), naming what was looked for and how to fix it, where a bareFileNotFoundErrorused to escape fromsubprocess, or a probe used to answerFalse— conflating "no python support" with "no clingo at all". -
clingo_version()no longer raisesUnboundLocalErrorwhen the clingo module is the backend. -
with_clingo_binrestores the previous binary even when the decorated call raises. -
the pre-1.0 toggles still work, and warn. To migrate:
before after clyngor.use_clingo_module()clyngor.using_solver(backend='module')clyngor.use_clingo_binary(path)clyngor.using_solver(backend='binary', binary_path=path)clyngor.deactivate_clingo_module()clyngor.using_solver(backend='binary')clyngor.CLINGO_BIN_PATH = pathclyngor.using_solver(binary_path=path)clyngor.set_clingo_binary(path)clyngor.using_solver(binary_path=path)clyngor.have_clingo_module()clyngor.default_solver().uses_moduleclyngor.clingo_module_actived()clyngor.default_solver().uses_moduleclyngor.clingo_module_availableclyngor.default_solver().module_availableclyngor.load_clingo_module()nothing: the module is looked up when needed All of them move process-wide state. Passing
solver=Solver(...)toclyngor.solveneeds none of it, and is the thread-safe way to pick a backend. -
pyPEG2is no longer an install dependency: nothing in theclyngorpackage imports it, it belongs to the sibling clyngor-parser project.pytestmoved to thetestextra. -
tests run in CI, across python 3.9/3.11/3.13 and the three backend configurations.
-
- 0.4.0 (todo)
- see further ideas
- 0.3.28
- 0.3.25
- 80245b2a7: remove f-strings for 3.4 and 3.5 compat.
- 6efdb6ab0: fix combination of .as_pyasp and .parse_args, where atoms in args were not transformed as pyasp Atom objects.
- fe4107573: correctly parse atoms starting with underscores.
- d6507f17d: careful parsing is automatically set when answer set obviously needs it.
- f2c65e8ae: fixed bug when using clingo module and
.int_not_parsed.
- 0.3.24
- f92248e91:
#show 3.and#show "hello !".are now handled - 31375774c: when using clingo module, the models contains only the output atoms, not everything (thank you Arnaud)
- cc6021797: support for
.with_answer_number, giving model, optimization, optimality and answer number - c0c090c34: parsing and string reproduction of nested atoms such as
a((a("g(2,3)",(2)),))is now correctly handled and tested - 1840c36e3: fix the
models.commandoutput when clingo module is used - 2679d26a9: optimize memory usage of
opt_models_from_clyngor_answersby using yield and answer number, but is now a generator and loses (the useless)repeated_optimaloption
- f92248e91:
- 0.3.20
- fix #7
- improve testing cover, fix warning in recent versions of pytest
- more robust options parsing when solving with clingo module
- 0.3.19
- fix #16
- 0.3.18
- TermSet bugfix
TermSet.addto add atoms to the TermSetTermSet.unionto generate the union of multiple TermSet instances
- 0.3.17
- support for decoupled grounding and solving, as shown in dedicated example
- new parameter
return_raw_outputfor clyngor.solve, allowing to get stdout/stderr without treatments - new example showing how to retrieve all optimal models using clyngor, and…
- … the defined function
opt_models_from_clyngor_answersis now embedded in clyngor API
- 0.3.16
- support for
.by_arity, equivalent to.by_predicatebut with predicate and arity - decorator
with_clingo_bin, changing clingo binary path for encapsulated function - support for
.with_optimality, giving optimization and optimality along with the model
- support for
- 0.3.14
- decoders support, see
clyngor.decoderand doc
- decoders support, see
- 0.3.10
- support for
.discard_quotesoption (thanks to ArnaudBelcour) - bugfix:
.atom_as_stringand.first_arg_onlycollision - bugfix: more robust tempfile deletion and closing management
- demonstration of the non-working Constraint type implementation
- support for
- before
- predicat to know if python/lua are available with used clingo binary
- easy interface for most use cases using type hint for embedded python
- easy python constraints in ASP with Constraint type
- add support for propagators
- add support for clingo official python module
If you have a project that makes use of pyasp, but need clingo instead of gringo+clasp, one way to go is to use clyngor instead.
Here was my old code:
from pyasp import asp
def solving(comp, graph):
programs = [comp, graph]
clasp_options = ['--opt-mode=optN', '--parallel-mode=4', '--project']
solver = asp.Gringo4Clasp(clasp_options=clasp_options)
print("solver run as: `clingo {} {}`".format(' '.join(programs), clasp_options))
at_least_one_solution = False
for answerset in solver.run(programs, collapseAtoms=False):
yield answerset
def find_direct_inclusions(model) -> dict:
programs = [ASP_SRC_INCLUSION]
solver = asp.Gringo4Clasp()
add_atoms = ''.join(str(atom) + '.' for atom in model)
answers = tuple(solver.run(programs, collapseAtoms=False,
additionalProgramText=add_atoms))
return answersAnd here is the version using clyngor, that pass the exact same unit tests:
import clyngor
def solving(comp, graph):
programs = [comp, graph]
clasp_options = '--opt-mode=optN', '--parallel-mode=4', '--project'
answers = clyngor.solve(programs, options=clasp_options)
print("solver run as: `{}`".format(answers.command))
for answerset in answers.as_pyasp.parse_args.int_not_parsed:
yield answerset
def find_direct_inclusions(model) -> dict:
programs = [ASP_SRC_INCLUSION]
add_atoms = ''.join(str(atom) + '.' for atom in model)
answers = tuple(clyngor.solve(programs, inline=add_atoms).as_pyasp.parse_args)
return answersTo Arnaud Belcour for his works and frequent feedbacks.
To Domoritz for his questions and feedbacks.
