Skip to content

Commit

Permalink
Supporting use of context.Context to time out execution of the solver
Browse files Browse the repository at this point in the history
  • Loading branch information
gnboorse committed Mar 20, 2022
1 parent 96119e4 commit 8244b8d
Show file tree
Hide file tree
Showing 8 changed files with 159 additions and 10 deletions.
28 changes: 28 additions & 0 deletions context.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package centipede

import (
"context"
"errors"
)

var (
ErrExecutionCanceled error = errors.New("execution canceled")
)

// RunWithContext accepts a context and a function that produces T
// The function will be run, and so long as the context is not done,
// its result will be returned. Otherwise an error will be returned.
func RunWithContext[T any](ctx context.Context, f func() T) (*T, error) {
ch := make(chan T, 1)
go func() {
r := f()
ch <- r
}()

select {
case res := <-ch:
return &res, nil
case <-ctx.Done():
return nil, ErrExecutionCanceled
}
}
12 changes: 10 additions & 2 deletions cspsolver.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@

package centipede

import "context"

// BackTrackingCSPSolver struct for holding solver state
type BackTrackingCSPSolver[T comparable] struct {
State CSPState[T]
Expand All @@ -30,8 +32,14 @@ func NewBackTrackingCSPSolverWithPropagation[T comparable](vars Variables[T], co
}

// Solve solves for values in the CSP
func (solver *BackTrackingCSPSolver[T]) Solve() bool {
return reduce(&solver.State)
func (solver *BackTrackingCSPSolver[T]) Solve(ctx context.Context) (bool, error) {
b, err := RunWithContext[bool](ctx, func() bool {
return reduce(&solver.State)
})
if b != nil && *b {
return true, nil
}
return false, err
}

// implements backtracking search
Expand Down
4 changes: 3 additions & 1 deletion integerconstraints_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
package centipede

import (
"context"
"testing"

"github.com/stretchr/testify/assert"
Expand Down Expand Up @@ -50,7 +51,8 @@ func TestIntegerConstraints(t *testing.T) {

// solve the problem
solver := NewBackTrackingCSPSolver(vars, constraints)
success := solver.Solve() // run the solution
success, err := solver.Solve(context.TODO()) // run the solution
assert.Nil(t, err)

assert.True(t, success)
values := map[string]int{}
Expand Down
29 changes: 27 additions & 2 deletions localconsistency.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package centipede

import (
"context"
"fmt"
)

Expand All @@ -11,7 +12,19 @@ import (
// mutually exclusive to it, i.e. if A != B and B = 2, remove 2
// from the domain of A.
// Use of this algorith is not recommended. Enforce arc consistency instead.
func (state *CSPState[T]) SimplifyPreAssignment() {
func (state *CSPState[T]) SimplifyPreAssignment(ctx context.Context) error {
_, err := RunWithContext(ctx, func() bool {
state.simplify()
return true
})
if err != nil {
return err
}

return nil
}

func (state *CSPState[T]) simplify() {

for _, variable := range state.Vars {
if !variable.Empty { // assigned to
Expand Down Expand Up @@ -57,7 +70,19 @@ func (state *CSPState[T]) SimplifyPreAssignment() {
// MakeArcConsistent algorithm based off of AC-3 used to make the
// given CSP fully arc consistent.
// https://en.wikipedia.org/wiki/AC-3_algorithm
func (state *CSPState[T]) MakeArcConsistent() {
func (state *CSPState[T]) MakeArcConsistent(ctx context.Context) error {
_, err := RunWithContext(ctx, func() bool {
state.arcConsistency()
return true
})
if err != nil {
return err
}

return nil
}

func (state *CSPState[T]) arcConsistency() {
// create queue of indices and fill it with constraints
queue := make([]int, 0)
for i := range state.Constraints {
Expand Down
4 changes: 3 additions & 1 deletion mapcoloringaustralia_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
package centipede

import (
"context"
"testing"

"github.com/stretchr/testify/assert"
Expand Down Expand Up @@ -52,7 +53,8 @@ func TestMapColoringAustralia(t *testing.T) {

// create the solver with a maximum depth of 500
solver := NewBackTrackingCSPSolver(vars, constraints)
success := solver.Solve() // run the solution
success, err := solver.Solve(context.TODO()) // run the solution
assert.Nil(t, err)

assert.True(t, success)
values := map[string]string{}
Expand Down
6 changes: 4 additions & 2 deletions sudoku_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
package centipede

import (
"context"
"strconv"
"testing"

Expand Down Expand Up @@ -129,8 +130,9 @@ func TestSudoku(t *testing.T) {
solver := NewBackTrackingCSPSolver(vars, constraints)

// simplify variable domains following initial assignment
solver.State.MakeArcConsistent()
success := solver.Solve() // run the solution
solver.State.MakeArcConsistent(context.TODO())
success, err := solver.Solve(context.TODO()) // run the solution
assert.Nil(t, err)

assert.True(t, success)

Expand Down
80 changes: 80 additions & 0 deletions timeout_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
// Copyright 2022 Gabriel Boorse

// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at

// http://www.apache.org/licenses/LICENSE-2.0

// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package centipede

import (
"context"
"testing"
"time"

"github.com/stretchr/testify/assert"
)

func TestTimeout(t *testing.T) {
vars := Variables[int]{
NewVariable("A", IntRange(1, 10)),
NewVariable("B", IntRange(1, 10)),
NewVariable("C", IntRange(1, 10)),
}

constraints := Constraints[int]{
Equals[int]("A", "B"), // A = B
Constraint[int]{Vars: VariableNames{"A", "B"},
ConstraintFunction: func(variables *Variables[int]) bool {
time.Sleep(10 * time.Millisecond)
if variables.Find("A").Empty || variables.Find("B").Empty {
return true
}
return variables.Find("A").Value > variables.Find("B").Value
}},
}

// solve the problem
solver := NewBackTrackingCSPSolver(vars, constraints)
d := time.Now().Add(20 * time.Millisecond)
ctx, cancel := context.WithDeadline(context.TODO(), d)
defer cancel()
_, err := solver.Solve(ctx)
assert.Equal(t, ErrExecutionCanceled, err)
}

func TestNoTimeout(t *testing.T) {
vars := Variables[int]{
NewVariable("A", IntRange(1, 10)),
NewVariable("B", IntRange(1, 10)),
NewVariable("C", IntRange(1, 10)),
}

constraints := Constraints[int]{
Equals[int]("A", "B"), // A = B
Constraint[int]{Vars: VariableNames{"A", "C"},
ConstraintFunction: func(variables *Variables[int]) bool {
time.Sleep(1 * time.Millisecond)
if variables.Find("A").Empty || variables.Find("C").Empty {
return true
}
return variables.Find("A").Value > variables.Find("C").Value
}},
}

// solve the problem
solver := NewBackTrackingCSPSolver(vars, constraints)
d := time.Now().Add(200 * time.Millisecond)
ctx, cancel := context.WithDeadline(context.TODO(), d)
defer cancel()
success, err := solver.Solve(ctx)
assert.Nil(t, err)
assert.True(t, success)
}
6 changes: 4 additions & 2 deletions zebra_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
package centipede

import (
"context"
"testing"

"github.com/stretchr/testify/assert"
Expand Down Expand Up @@ -87,8 +88,9 @@ func TestZebra(t *testing.T) {
solver := NewBackTrackingCSPSolver(vars, constraints)

// simplify variable domains following initial assignment
solver.State.MakeArcConsistent()
success := solver.Solve() // run the solution
solver.State.MakeArcConsistent(context.TODO())
success, err := solver.Solve(context.TODO()) // run the solution
assert.Nil(t, err)

assert.True(t, success)

Expand Down

0 comments on commit 8244b8d

Please sign in to comment.