potassco / clasp

⚙️ A conflict-driven nogood learning answer set solver

Home Page:https://potassco.org/clasp/

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Extend Potassco::Assignment to provide access to trail.

rkaminsk opened this issue · comments

In https://github.com/MaxOstrowski/clingo/blob/8ed27d12c0b282a6549170c352b57401ce9b2b95/examples/clingo/csp/csp.py#L1069 I implemented an initial propagation for a theory in Propagator::init. With the current interface, this requires traversing the whole assignment in each iteration. Being able to access the solver's trail would make this much nicer. Would it be difficult to extend the interface to access the trail?

This heavily depends on how this interface should look like and what information it should actually provide. At the moment, the clingo propagator in clasp only maintains a trail of watched literals. Hence, supporting a simple "span-based" interface for accessing this trail would be trivial. On the other hand, if you'd need the trail of all literals, we have to ask the clasp solver, which unfortunately maintains this information in a different format. So, one would have to either maintain a mapped copy of this trail (not a good idea) or come up with an interface for some "lazy sequence" that supports a "map" operation on element access. A trivial but rather ugly way would be something like:

class AbstractTrail {
public:
	virtual ~AbstractTrail();
	//! Returns the number of literals in the trail.
	virtual uint32_t size()         const = 0;
	//! Returns the i'th element in the trail.
	virtual Lit_t    at(uint32_t i) const = 0;
};

A trivial but rather ugly way would be something like: [...]

It would be what I need. The watched literals are not enough. One further function might be nice:

// offset of first literal with the given level in trail
virtual uint32_t offset(uint32_t level) const = 0;

A span based interface would be (a little) nicer but so far the assignment does not provide any spans anyway. So we might get away with what you are suggesting. Also, in the current interface, spans are used to pass around small blocks of memory. Since the trail is potentially large, the ugly interface might just make sense. Also, for C++ and Python I can hide this behind a nice interface easily.

@rkaminsk
I implemented trail access in https://github.com/potassco/clasp/compare/dev...clasp_49?expand=1
Let me know whether having these functions in the Assignment interface works for you or whether you'd prefer having a separate Trail interface.

Awesome. I'll make the interface available in clingo and give it a try asap.

EDIT: Is skimmed too quickly over your comment. Having this as part of the assignment is perfectly fine. IFor clingo's C interface, I will probably just extend the assignment as you did. For Python and C++ I'll probably do something nicer by providing an array like object to iterate over the trail.

Hi, just two things I noticed.

  1. Assignment::trailSize and Assignment::size are the same?
  2. Assignment::trailAt might report "invalid decision level"; invalid index might be better.

I also want to implement a trailEnd(uint32_t) method. For example, to be able to iterate over a decision level. It would be nice if I could implement this method simply by calling trailBegin(level+1). What do you think? Would it make sense to let trailBegin(level+1) return trailSize() if level==level() instead of return UINT32_MAX?

Maybe we should even let trailBegin(level) throw an exception if level > level()+1 to detect wrong calls right away.

Also I want to use the trail already during propagator initialization to obtain what has been propagated at this point and use it to simplify the data structures in the propagator. Currently, trailBegin(0) returns UINT32_MAX at this point. Can we change this? The whole idea of the example I linked in my issue was to have the trail during initialization.

EDIT: Maybe it is also just this function, which is problematic:

uint32_t ClingoAssignment::trailBegin(uint32_t dl) const {
  if (dl > solver_->decisionLevel() || trailSize() == 0)
    return UINT32_MAX;
  return dl == 0 ? 0 : solver_->levelStart(dl);
}

During initialization, the trail can very well be empty. Then the function should return 0 and not UINT32_MAX. If I add a clause during initialization that propagates something, then I get trailBegin(0)==0 as I would expect.

Here is how I wrapped the begin method so that it does what I need. Would be cool if we can get this into clasp.

extern "C" bool clingo_assignment_trail_begin(clingo_assignment_t const *assignment, uint32_t level, uint32_t *ret) {
    GRINGO_CLINGO_TRY {                     
        if (level > assignment->level() + 1) {
            throw std::runtime_error("invalid level");              
        }                                                           
        else if (assignment->trailSize() == 0) {                    
            *ret = 0;                                               
        }                                                           
        else if (level > assignment->level()) {                     
            *ret = assignment->trailSize();                         
        }                                                           
        else {                                                      
            *ret = assignment->trailBegin(level);                                
        }                                                               
    }                                                         
    GRINGO_CLINGO_CATCH;                                                                                                                                                           
}

I can also modify the clasp_49 branch. I just need to know if I am not missing something important.

And then there is one more small detail that comes to my mind. Literal 1 is propagated as true in the propagator. I think it makes sense to pretend that it is in the trail, too. We could reserve offset 0 for it. I also stumbled over this, when trying to iterate over all literals to extract the trail. It is not included in the assigned literals. We could also change Assignment::assigned() to include this literal.

The current state of the clingo API extension is in potassco/clingo#187. I played a bit with the indices to make the interface easier to use (at least in my propagator prototype).

I provided a sequence based interface for both the assignment and the trail in the C++ and Python APIs. I am happy with how it is working now but it would be nice if we can move some of the index tricks into clasp.

I was also thinking to remove clingo_assignment_max_size() and change clingo_assignment_size() to return Assignment::size(). Now that we have the trail, this looks natural to me. Of course it would break backward compatibility but this should be rarely used functions.

Hi, just two things I noticed.

1. `Assignment::trailSize` and `Assignment::size` are the same?

No. Assignment::size() returns the number of variables in the assignment, counting both assigned and unassigned variables, while trailSize() returns the number of true literals in the trail (basically Assignment::size() - Assignment::unassigned())

2. `Assignment::trailAt` might report "invalid decision level"; invalid index might be better.

Yes, copy and paste error :)

Regarding the indexes/sizes: In clasp, variable 0 is used as a special sentinel-variable that is always true but never counted - neither in the assignment nor in the trail. While this is important internally, we could of course change this on the clingo API level. That would mean:

  • Assignment::size() would never be 0
  • Assignment::trailSize() would never be 0
  • Assignment::trailBegin(0) would always return 0
  • Assignment::trailAt(0) would always return lit(1)

No. Assignment::size() returns the number of variables in the assignment, counting both assigned and unassigned variables, while trailSize() returns the number of true literals in the trail (basically Assignment::size() - Assignment::unassigned())

In clasp yes, but in clingo I implemented it differently (which I regret now and am considering to change). This was about changing the clingo not clasp the implementation. Sorry for the confusion.

[...] we could of course change this on the clingo API level. That would mean [...]

I think this would make the API easier to use and more consistent. Because we are already pretending that there is a literal 1 during propagation.

@rkaminsk
I updated the branch accordingly. I also added a trailEnd() function and strengthened the precondition of trailBegin()/trailEnd(). I.e. the parameter dl now has to be 0 <= dl <= level(). This is not exactly what you asked for but I think with the new trailEnd() function that makes the most sense.

Thanks. I think we should also add the trailOffset to ClingoAssignment::size() even if it is a "breaking" change.

uint32_t ClingoAssignment::size() const {
    return solver_->assignment().numVars() + trailOffset;
}

Then every thing should fit together. We will get things like trail.size() + assignment.free() == assignment.size(). Note that we already get assignment.hasLit(1) and assignment.hasLit(-1).

Thanks. I think we should also add the trailOffset to ClingoAssignment::size() even if it is a "breaking" change.

uint32_t ClingoAssignment::size() const {
    return solver_->assignment().numVars() + trailOffset;
}

Maybe I'm missing something but I don't think this is necessary/correct. I deliberately changed this code from solver_->numVars() to solver_->assignment().numVars() in order to account for the always present true literal. For an empty clasp solver s, i.e. where s.numVars() == 0, ClingoAssignment::size() now already returns 1.

Maybe I'm missing something but I don't think this is necessary/correct. I deliberately changed this code from solver_->numVars() to solver_->assignment().numVars() in order to account for the always present true literal. For an empty clasp solver s, i.e. where s.numVars() == 0, ClingoAssignment::size() now already returns 1.

Great, then everything is fine. I could not quickly test it because adding even just one function is quite involved with 4 interfaces. I'll adjust my code to the new interface and then we can close this. Thanks for taking the time!

Looks like everything is working. I'll do some last sanity check tomorrow.

All the tests I came up with work as expected. We can merge and close this.