potassco / clasp

⚙️ A conflict-driven nogood learning answer set solver
https://potassco.org/clasp/
MIT License
116 stars 16 forks source link

Core conflicts. #59

Closed rkaminsk closed 4 years ago

rkaminsk commented 4 years ago

When a problem is unsatisfiable when solving under assumptions, it would be nice if there were an interface to get a conflict clause that is a subset of the assumptions literals. A trivial example would be something like the following:

Program:

:- a, b.
{c}.

Solve call:

ret = solve(assumptions=[a, b, c])
assert(ret.conflict == [a, b])

The above is not meant to be the final interface.

BenKaufmann commented 4 years ago

@rkaminsk While it would be quite straightforward to add this for single-threaded solving and for solving in competition mode (as long as we only talk about problems that are truly unsat under some given assumptions), I don't currently see an easy way to properly support this in splitting mode. Atm, clasp simply does not maintain enough information to extract such a final conflict clasue in that case. So, without extending parallel solve with the capability to store/resolve unsat paths, I see only two possibilities: a) always return the initial set of assumptions b) do a second solve() call in the end hoping that solvers learnt/kept enough crucial clauses to make it faster than the first call. Neither approach seems particular desirable.

rkaminsk commented 4 years ago

@rkaminsk While it would be quite straightforward to add this for single-threaded solving and for solving in competition mode (as long as we only talk about problems that are truly unsat under some given assumptions), I don't currently see an easy way to properly support this in splitting mode. Atm, clasp simply does not maintain enough information to extract such a final conflict clasue in that case. So, without extending parallel solve with the capability to store/resolve unsat paths, I see only two possibilities: a) always return the initial set of assumptions b) do a second solve() call in the end hoping that solvers learnt/kept enough crucial clauses to make it faster than the first call. Neither approach seems particular desirable.

I would think a) is fine because it is still "correct". We can document this as a caveat. I hope I am not wrong but I think splitting is not used very often anyway.

BenKaufmann commented 4 years ago

I implemented a first version in branch issue-59. However, I'm not convinced that this is really useful because without doing any subset minimization, the returned literals are probably just the initial assumptions.

rkaminsk commented 4 years ago

This is cool. I will put it into Clingo's SolveHandle class like you did. Having the possibility to minimize cores further would of course be nice, too. Having the ones that are already used in the uncore minimization would be nice. But one can also role one's own minimization strategy using multiple solve calls.

rkaminsk commented 4 years ago

I did a first throw at an implementation. Using the Python API one can write now:

>>> import clingo
>>>
>>> ctl = clingo.Control()
>>> ctl.add("base", [], "{a;b;c}. :- a, b.")
>>> ctl.ground([("base", [])])
>>> 
>>> assumptions = []
>>> for name in ("a", "b", "c"):
...     assumptions.append(ctl.symbolic_atoms[clingo.Function(name)].literal)
... 
>>> print(ctl.solve(assumptions=assumptions, on_core=lambda core: print(core)))
[1, 2]
UNSAT

There are still some issues though. It looks like for empty cores unsatCore returns a null pointer. I think it should really be an empty vector instead.

Another thing is that the core is returned in terms of solver literals. For the interface, I need program literals. I had to write quite some ugly code (that is not even fully working yet) to do the mapping. Would it be possible to get a little help from Clasp::Asp::LogicProgram to do the mapping?

Can you have a look at the feature/core branch in the clingo repository, please?

PS: I additionally added a callback because this is easier to use in Python. There is a SolveHandle.core method too.

BenKaufmann commented 4 years ago

There are still some issues though. It looks like for empty cores unsatCore returns a null pointer. I think it should really be an empty vector instead.

Ok. My idea was to return nullptr whenever the problem was not "unsat under assumptions" but I guess it makes more sense to only return a nullptr if the problem was not unsat at all.

Another thing is that the core is returned in terms of solver literals. For the interface, I need program literals. I had to write quite some ugly code (that is not even fully working yet) to do the mapping. Would it be possible to get a little help from Clasp::Asp::LogicProgram to do the mapping?

Probably, I'll look into that. At least for the assumptions, we'll probably always have or can find a corresponding program literal. This is not the case for unsat-core based minimization, though. There, unsat cores nearly completely consist of solver-local aux variables.

rkaminsk commented 4 years ago

Probably, I'll look into that. At least for the assumptions, we'll probably always have or can find a corresponding program literal. This is not the case for unsat-core based minimization, though. There, unsat cores nearly completely consist of solver-local aux variables.

Thanks. Yes, here it should really work because the clingo API only allows for specifying assumptions as program literals. Maybe with a lower level interface it would also make sense to get solver literals. For example, if we had something like the enumerator in the API.

BenKaufmann commented 4 years ago

@rkaminsk I pushed an update to the issue-59 branch. Let me know if you need anything more or if I can close the issue and merge the branch into dev.

rkaminsk commented 4 years ago

There is the corner case that someone might assume a fact to be false (or assume a false literal to be true). In this case I would expect to get a core with one literal but clasp reports an empty core. This happens, for example, in the snippet below. Can we fix this?

#script (python)

import sys
from clingo import Function

def main(prg):
    cores = []
    prg.ground([("base", [])])
    a = prg.symbolic_atoms[Function("a")].literal
    prg.solve(assumptions=[-a], on_core=lambda core: cores.append(core))
    assert cores[-1] == [-a]

#end.

a.
rkaminsk commented 4 years ago

That seems to do the trick. I did not thorough testing but my simple tests all go through now.

BenKaufmann commented 4 years ago

Merged to dev.