Skip to main content

Python wrapper around Clingo/Answer Set Programming

Project description


Handy python wrapper around Potassco's Clingo ASP solver.

Example

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 file

More examples are available in the unit tests.

Chaining

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))

Official Python API

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 :

You can activate the use of the clingo module by calling once clyngor.activate_clingo_module() or calling clyngor.solve with argument use_clingo_module set to True.

Python embedding

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.

Easy ASP functors

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 match

Without 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.

Generalist propagators

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.

Python constraint propagators

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.

Decoders

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), or any (exact meaning has to be found)
  • allow to raise an InvalidDecoder exception during decoder instanciation to get the instance discarded

Alternatives

Clyngor is basically the total rewriting of pyasp, which is now abandonned.

For an ORM approach, give a try to clorm.

Installation

pip install clyngor

You must have clingo in your path. Depending on your OS, it might be done with a system installation, or through downloading and (compilation and) manual installation.

You may also want to install the python clingo module, which is an optional dependancy.

Tips

Careful parsing

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).

Import/export

See the utils module and its tests, which provides high level routines to save and load answer sets.

Define the path to clingo binary

import clyngor
clyngor.CLINGO_BIN_PATH = 'path/to/clingo'

Note that it will be the very first parameter to subprocess.Popen. The solve function also support the clingo_bin_path parameter.

The third solution is to use the decorator with_clingo_bin, which modify the global variable during the execution of a specific function:

import clyngor

@clyngor.with_clingo_bin('clingo454')
def sequence():
    ...
    clyngor.solve(...)  # will use clingo454, not clingo, unless clingo_bin_path is given
    ...

clyngor.solve parameters

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))

FAQ

Dinopython support ?

No.

Contributions ?

Yes.

Why clyngor ?

No, it's pronounced clyngor.

Explain me again the thing with the official module

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.

Further ideas

Changelog

  • 0.4.0 (todo)
  • 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.add to add atoms to the TermSet
    • TermSet.union to 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_output for 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_answers is now embedded in clyngor API
  • 0.3.16
    • support for .by_arity, equivalent to .by_predicate but 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
  • 0.3.14
  • 0.3.10
    • support for .discard_quotes option (thanks to ArnaudBelcour)
    • bugfix: .atom_as_string and .first_arg_only collision
    • bugfix: more robust tempfile deletion and closing management
    • demonstration of the non-working Constraint type implementation
  • 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

from pyasp to clyngor

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 answers

And 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 answers

Thanks

To Arnaud Belcour for his works and frequent feedbacks.

To Domoritz for his questions and feedbacks.

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

clyngor-0.3.21.tar.gz (36.6 kB view details)

Uploaded Source

Built Distribution

clyngor-0.3.21-py3-none-any.whl (43.3 kB view details)

Uploaded Python 3

File details

Details for the file clyngor-0.3.21.tar.gz.

File metadata

  • Download URL: clyngor-0.3.21.tar.gz
  • Upload date:
  • Size: 36.6 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/1.11.0 pkginfo/1.4.2 requests/2.19.1 setuptools/39.2.0 requests-toolbelt/0.8.0 tqdm/4.23.4 CPython/3.6.6

File hashes

Hashes for clyngor-0.3.21.tar.gz
Algorithm Hash digest
SHA256 e3d36afa790d322314bda5e5919506c19dbca5f1a0921e48b0f1975084544157
MD5 b4476773f5a512e7fe4a83833f97e574
BLAKE2b-256 07bb83b69ca7b21f3b90cbe93f04e434ad876ea562fe3ad4087b3d7db44c5c11

See more details on using hashes here.

File details

Details for the file clyngor-0.3.21-py3-none-any.whl.

File metadata

  • Download URL: clyngor-0.3.21-py3-none-any.whl
  • Upload date:
  • Size: 43.3 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/1.11.0 pkginfo/1.4.2 requests/2.19.1 setuptools/39.2.0 requests-toolbelt/0.8.0 tqdm/4.23.4 CPython/3.6.6

File hashes

Hashes for clyngor-0.3.21-py3-none-any.whl
Algorithm Hash digest
SHA256 d0e99859bc3221767950a417259385236af20ec129c3ae57527b90c2eda6ca1a
MD5 fa17a7a518683955529e6893eb860819
BLAKE2b-256 70e27c1093c874d3cafff5b06d1c037aec352aa896e6174b08f58f3168be520c

See more details on using hashes here.

Supported by

AWS AWS Cloud computing and Security Sponsor Datadog Datadog Monitoring Fastly Fastly CDN Google Google Download Analytics Microsoft Microsoft PSF Sponsor Pingdom Pingdom Monitoring Sentry Sentry Error logging StatusPage StatusPage Status page