Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Core conflicts. #59

Closed
rkaminsk opened this issue Apr 6, 2020 · 11 comments
Closed

Core conflicts. #59

rkaminsk opened this issue Apr 6, 2020 · 11 comments

Comments

@rkaminsk
Copy link
Member

rkaminsk commented Apr 6, 2020

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
Copy link
Contributor

BenKaufmann commented Apr 10, 2020

@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
Copy link
Member Author

@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 added a commit that referenced this issue Apr 10, 2020
* Add SolveHandle::unsatCore(). When a problem is unsat when solving
  under assumptions, this function returns a subset of the initial
  assumption literals that together made the problem unsat.
@BenKaufmann
Copy link
Contributor

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
Copy link
Member Author

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
Copy link
Member Author

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
Copy link
Contributor

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
Copy link
Member Author

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 added a commit that referenced this issue Apr 25, 2020
* Let SolveAlgorithm::unsatCore() return an empty vector instead of
  a nullptr if problem is unconditionally unsat.

* Add LogicProgram::extractCore() for mapping an unsat core of solver
  literals back to the corresponding program literals.

* Add ClaspFacade::Summary::unsatCore().
@BenKaufmann
Copy link
Contributor

@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
Copy link
Member Author

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
Copy link
Member Author

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

BenKaufmann added a commit that referenced this issue Apr 26, 2020
* Add SolveHandle::unsatCore(). When a problem is unsat when solving
  under assumptions, this function returns a subset of the initial
  assumption literals that together made the problem unsat.
BenKaufmann added a commit that referenced this issue Apr 26, 2020
* Let SolveAlgorithm::unsatCore() return an empty vector instead of
  a nullptr if problem is unconditionally unsat.

* Add LogicProgram::extractCore() for mapping an unsat core of solver
  literals back to the corresponding program literals.

* Add ClaspFacade::Summary::unsatCore().
@BenKaufmann
Copy link
Contributor

Merged to dev.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants