-
Notifications
You must be signed in to change notification settings - Fork 4
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
Added Assume() + companion methods #12
Conversation
Thanks! If it's a little while before I merge this, it's not because I've forgotten about it. It's just that my next few weeks are pretty busy. I'll definitely get to it though. |
That's alright. I have 1 or 2 more merge requests to put together anyways, On Mon, 23 Feb 2015 6:13 PM William Schwartz notifications@github.com
|
FYI, I'm working on a pull request for your branch (i.e., merge from my |
Awesome. Glad to see this is still being considered. On Wed, Apr 15, 2015 at 8:46 AM William Schwartz notifications@github.com
|
This reverts commit 39cdc69.
Curious if this will ever get merged. I was thinking of open sourcing some old package resolving code, which would be making use of this. |
Yes, I'm sorry for the delay, but it will be merged. Thanks for the Thanks for your patience and sorry I've neglected this package all year. |
Still haven't forgotten about this! I've cleared my schedule for tomorrow to work on this and your other PR. |
// Returns a list of failed assumption in the last call to Solve(). It only | ||
// makes sense if the last call to Solve() returned Unsatisfiable. | ||
func (p *Pigosat) FailedAssumptions() []Literal { | ||
defer p.ready(true)() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(Moving discussion of your comment to here to keep the whole PR together.)
The reason why we should lock before checking the last result is to prevent the race condition in which one thread calls FailedAssumption
while another thread kicks off a new Solve
. The reason calling Res
from inside FailedAssumption
doesn't deadlock is that they both acquire a read lock and neither acquires a write lock. There is not limit on the number of threads holding a read lock.
The only advantage I can see to avoiding calling a public (i.e., read-lock-acquiring) method such as Res
here is speed, or avoiding a situation where in the future we want to switch Res
to acquire a write lock (not sure why we would want to do the latter). In that case, I think it's fine to create an internal method res
and have all the public methods, including Res
and FailedAssumption
call res
. That change would be cheap, at least, because it's just a couple lines of code.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry if my lack of a reply was holding this up at all. Yea I agree that we shouldn't hold up this ticket with out-of-scope improvements. I do feel that exported methods that lock should not call other exported methods that lock, and that there would be unexported methods which do not lock. Because if that method that is being called decides to change the way it locks it could then cause bugs/deadlocking in the callers which anticipated the implementation. But all good. I would much prefer we get this merged over anything else.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we're on the same page. Would you mind pushing a commit that replaces the internal calls to Res with direct calls to C? Once we get this commit merged, we'll fix it with #20.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sure. I can do that.
This is consistent with style elsewhere in Pigosat
header := (*reflect.SliceHeader)(unsafe.Pointer(&cints)) | ||
header.Cap = size | ||
header.Len = size | ||
header.Data = uintptr(unsafe.Pointer(litPtr)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Proposal to make the signature like the following so litArrayToSlice
is a function instead of a method. Instead of looping over the litPtr
array using a Go slice, what if we did this:
func litArrayToSlice(litPtr *C.int) []Literal {
if litPtr == nil {
return nil
}
lits := []Literal{}
for *litPtr != 0 {
lits = append(lits, Literal(*litPtr))
}
return lits
}
Is there any risk that we'll never get to *litPtr != 0
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In this example, how do you advance the litPtr
? The previous approach wraps the *C.int
in a slice to be able to range over it. Would we have to do some unsafe
pointer arith?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Omitting the pointer arithmetic was a typo on my part, but I did forget about using unsafe
(got my C/Go cross-combobulated). I guess it would look like this?
for *litPtr != 0 {
lits = append(lits, Literal(*litPtr))
litPtr += unsafe.Sizeof(C.int)
}
What's the "recommended" way of dealing with this in Cgo? No doubt there's a convention or semiofficial FAQ or something. Have a link or seen this done in the wild?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That would be the cgo way, since cgo is unsafe by nature. I suppose the only danger is if the literal was not 0-terminated and we loop forever. Whereas the other approach is limiting it to a max size when wrapping it in a slice header
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We could pass in a maxLen int
argument and bail or panic (I think I prefer panic) if we don't hit *litPtr == 0
before len(lits) >= maxLen
. Or we could keep calling picosat_variables
from within the function.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I prefer panic as well, as it would be a library error for the private method to iterate beyond an expected max
// Does not acquire internal locks. | ||
func (p *Pigosat) litArrayToSlice(litPtr *C.int) []Literal { | ||
if litPtr == nil || *litPtr == 0 { | ||
return []Literal{} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If litPtr == nil
why should we return a non-nil result here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We could return nil
but we would need to update the docstrings on the exported functions that return the result of litArrayToSlice
. They should say a nil slice could be returned. I assume in actual practice (the usage in the exported functions) that we just want to return valid empty Literals, for consistency? But I know they can be treated the same with len() and append() operations, so maybe it doesn't matter.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
From the way I read the documentation for picosat_failed_assumptions
:
/* Returns a zero terminated list of failed assumption in the last call to
* 'picosat_sat'. The pointer is valid until the next call to
* 'picosat_sat' or 'picosat_failed_assumptions'. It only makes sense if the
* last call to 'picosat_sat' returned 'PICOSAT_UNSATISFIABLE'.
*/
I think it would take an internal picosat error for picosat_failed_assumptions
to return a NULL pointer. Perhaps it makes more sense to panic when litPtr == nil
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
And, like I said in the other thread, I have no problem changing the API since the last stable release of Pigosat was version <1. After merging this and #15, the API will be pretty different anyway.
// first case, before returning, a blocking clause is added, that rules out | ||
// the result for the next call. | ||
// | ||
// NOTE: adding the blocking clause changes the CNF. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does this mean we should implement blocksol
differently, or not automatically call blocksol
from Solve
? (Disclaimer: I have not used the assumption interface before.)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
TBH I didn't look to much into the blocksol
stuff too closely. I see now that its a Pigosat addition that implicitly blocks on Solves. But if I understand this correctly, this shouldn't impact what we have done in our go Solve()
method, right? This will call into C, which will use the C solve and then block the solution. That would mean the only custom logic that has been layered in Pigosat is the blocking being done from our own Solve
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Correct. Pigosat.Solve
calls blocksol
before returning, and blocksol
just adds a clause to the formula that negates the solution just found. This way subsequent calls to solve can return any solution except the ones already returned. Based off Pycosat.
I wonder if this messes up the use of Assume(), et al. Do changes to the formula make Picosat forget about the last set of assumptions? If this is the case, perhaps we should make blocksol
a public method that users have to call themselves if they intend to find more than one solution. (I have no problem with changing the API if it makes sense. I had put Pigosat at version <1 for a reason.)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
According to the docs, the blocksol
would invalidate any current assumptions, since it would be adding more literals:
- More precisely, assumptions actually remain valid even after the next
- call to 'picosat_sat' has returned. Valid means they remain 'assumed'
- internally until a call to 'picosat_add', 'picosat_assume', or a second
- 'picosat_sat', following the first 'picosat_sat'. The reason for keeping
- them valid is to allow 'picosat_failed_assumption' to return correct
- values.
I think I see what you mean, and I was too focused on the location of this comment in the source. Because we are doing a block solution at the end of OUR solve, we are breaking the ability to check for failed assumptions afterwards. Yea we should probably remove the super secret blocking operations and make it user-explicit.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Whoops, both of us forgot about Pigosat.BlockSolution
already being a public method. This is good. We just need to update the documentation about using Solve
like an iterator and remove the call to blocksol
. I think it'll break some tests though, so I'm not going to try to do this tonight.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sounds good
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I can't get any tests to fail to demonstrate this problem. I think the reason why we don't need to worry about the call to blocksol
is because all the assumption-related Picosat functions look at failed literals after Picosat derives UNSAT. PiGosat only calls blocksol
upon SAT. Anyway, it's probably best not to do anything that seems "super secret" as you put it, so I've made #22, which I'll do after merging this PR.
This is hardly the final word on these docstrings. Just trying to simplify them for people less familiar with SAT solvers, and bring the style more in line with Go conventions. Also fixed some spelling/typo issues.
Just a heads up: It's going to be the end of the month before I can turn back to this. Swamped at work. |
I've pushed up my work to github today, since it didn't end up becoming a project, internally at my company. https://github.com/justinfx/pakr Its still pointing at my fork of pigosat because it is dependent on this branch getting merged. |
Remove dependency on the reflect library and use pointer arithmetic instead.
The last ten commits reduce the length of the assumption tests by a third. This is entirely for readability. The only reason for the separate commits was because that's how I tested the changes. |
This will simplify writing an ExamplePigosat_Assume
In particular, the assertion this adds to TestAssumptionsFailing is the only thing testing the check in Pigosat.MaxSatisfiableAssumptions of C.picosat_inconsistent.
For consistency with MaxSatisfiableAssumptions
Definitely need to resolve crash in |
If you call Assume with a satisfiable assumption after Solve returns UNSAT then FailedAssumption(s) crashes.
Go wasn’t running TestlitArrayToSlice because the l in lit wasn’t capitalized. Once I capitalized it and actually saw the results of the test, I realized there was a fence-post problem in detecting when arrays are too long. I also converted the repeated tests into for-loops.
Can’t have blank lines in “// Output:” comment of example tests.
…Solve Dealing with #22 in the assumptions branch because it’s mostly an issue when you’re trying to use NextMaxSatisfiableAssumptions.
I think all that remains at this point is fixing #22, and doing a final |
lits := []Literal{} | ||
for *litPtr != 0 { | ||
if len(lits) >= maxLen { | ||
panic("Array not zero-terminated") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why would it be considered "not zero-terminated" if there are more elements in lits
than maxLen? Does maxLen
imply that you know the length up front, and if so, does being 0-terminated even matter at that point?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
"Array not zero-terminated"
isn't the best error message there. My idea is this, keeping in mind that litArrayToSlice
is an internal utility function. We only use litArrayToSlice
on zero-terminated arrays coming from PicoSAT. In general, these arrays are zero-terminated. In case for whatever reason they are not, we can pass in maxLen
, which we known in general is equal to p.Variables()
, so that we don't loop forever here. In other words, maxLen
is purely a safety control and we don't know how long the array actually is until we hit the zero terminator.
Thoughts on a better error message?
…sumption Generalize the test for this behavior in all the assumption-invalidating methods except Solve, which I can’t figure out how to test, since it resets whether the Picosat object is UNSAT/SAT directly.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this is ready. Will require a version bump once merged into master because of #22.
@justinfx Thanks again for your work on this PR. I merged it without giving you a chance to review it one last time, but as always, feel free to leave a note here or open a new issue or PR if you notice anything amiss. I'm hoping to finish the other PRs and issues to get PiGoSAT to version 1.0. |
I've added the
Assume()
method, along with the checks for failed assumptions, and the max subsets of satisfiable assumptions.