Skip to content

Commit d65ca99

Browse files
Merge pull request #1632 from benluddy/atmost-constraint
Add an AtMost cardinality constraint to the solver package.
2 parents 3e6294b + 28a3d3b commit d65ca99

File tree

4 files changed

+102
-144
lines changed

4 files changed

+102
-144
lines changed

pkg/controller/registry/resolver/solver/constraints.go

Lines changed: 64 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -3,40 +3,16 @@ package solver
33
import (
44
"fmt"
55
"strings"
6-
)
7-
8-
// constrainer is a reusable accumulator of constraint clause terms.
9-
type constrainer struct {
10-
pos []Identifier
11-
neg []Identifier
12-
}
13-
14-
func (x *constrainer) Add(id Identifier) {
15-
x.pos = append(x.pos, id)
16-
}
17-
18-
func (x *constrainer) AddNot(id Identifier) {
19-
x.neg = append(x.neg, id)
20-
}
216

22-
// Reset clears the receiver's internal state so that it can be
23-
// reused.
24-
func (x *constrainer) Reset() {
25-
x.pos = x.pos[:0]
26-
x.neg = x.neg[:0]
27-
}
28-
29-
// Empty returns true if and only if the receiver has accumulated no
30-
// positive or negative terms.
31-
func (x *constrainer) Empty() bool {
32-
return len(x.pos) == 0 && len(x.neg) == 0
33-
}
7+
"github.com/irifrance/gini/logic"
8+
"github.com/irifrance/gini/z"
9+
)
3410

3511
// Constraint implementations limit the circumstances under which a
3612
// particular Installable can appear in a solution.
3713
type Constraint interface {
3814
String(subject Identifier) string
39-
apply(x *constrainer, subject Identifier)
15+
apply(c *logic.C, lm *litMapping, subject Identifier) z.Lit
4016
order() []Identifier
4117
}
4218

@@ -49,7 +25,8 @@ func (zeroConstraint) String(subject Identifier) string {
4925
return ""
5026
}
5127

52-
func (zeroConstraint) apply(x *constrainer, subject Identifier) {
28+
func (zeroConstraint) apply(c *logic.C, lm *litMapping, subject Identifier) z.Lit {
29+
return z.LitNull
5330
}
5431

5532
func (zeroConstraint) order() []Identifier {
@@ -71,15 +48,15 @@ func (a AppliedConstraint) String() string {
7148

7249
type mandatory struct{}
7350

74-
func (c mandatory) String(subject Identifier) string {
51+
func (constraint mandatory) String(subject Identifier) string {
7552
return fmt.Sprintf("%s is mandatory", subject)
7653
}
7754

78-
func (c mandatory) apply(x *constrainer, subject Identifier) {
79-
x.Add(subject)
55+
func (constraint mandatory) apply(_ *logic.C, lm *litMapping, subject Identifier) z.Lit {
56+
return lm.LitOf(subject)
8057
}
8158

82-
func (c mandatory) order() []Identifier {
59+
func (constraint mandatory) order() []Identifier {
8360
return nil
8461
}
8562

@@ -91,15 +68,15 @@ func Mandatory() Constraint {
9168

9269
type prohibited struct{}
9370

94-
func (c prohibited) String(subject Identifier) string {
71+
func (constraint prohibited) String(subject Identifier) string {
9572
return fmt.Sprintf("%s is prohibited", subject)
9673
}
9774

98-
func (c prohibited) apply(x *constrainer, subject Identifier) {
99-
x.AddNot(subject)
75+
func (constraint prohibited) apply(c *logic.C, lm *litMapping, subject Identifier) z.Lit {
76+
return lm.LitOf(subject).Not()
10077
}
10178

102-
func (c prohibited) order() []Identifier {
79+
func (constraint prohibited) order() []Identifier {
10380
return nil
10481
}
10582

@@ -113,26 +90,24 @@ func Prohibited() Constraint {
11390

11491
type dependency []Identifier
11592

116-
func (c dependency) String(subject Identifier) string {
117-
s := make([]string, len(c))
118-
for i, each := range c {
93+
func (constraint dependency) String(subject Identifier) string {
94+
s := make([]string, len(constraint))
95+
for i, each := range constraint {
11996
s[i] = string(each)
12097
}
12198
return fmt.Sprintf("%s requires at least one of %s", subject, strings.Join(s, ", "))
12299
}
123100

124-
func (c dependency) apply(x *constrainer, subject Identifier) {
125-
if len(c) == 0 {
126-
return
127-
}
128-
x.AddNot(subject)
129-
for _, each := range c {
130-
x.Add(each)
101+
func (constraint dependency) apply(c *logic.C, lm *litMapping, subject Identifier) z.Lit {
102+
m := lm.LitOf(subject).Not()
103+
for _, each := range constraint {
104+
m = c.Or(m, lm.LitOf(each))
131105
}
106+
return m
132107
}
133108

134-
func (c dependency) order() []Identifier {
135-
return []Identifier(c)
109+
func (constraint dependency) order() []Identifier {
110+
return constraint
136111
}
137112

138113
// Dependency returns a Constraint that will only permit solutions
@@ -146,16 +121,15 @@ func Dependency(ids ...Identifier) Constraint {
146121

147122
type conflict Identifier
148123

149-
func (c conflict) String(subject Identifier) string {
150-
return fmt.Sprintf("%s conflicts with %s", subject, c)
124+
func (constraint conflict) String(subject Identifier) string {
125+
return fmt.Sprintf("%s conflicts with %s", subject, constraint)
151126
}
152127

153-
func (c conflict) apply(x *constrainer, subject Identifier) {
154-
x.AddNot(subject)
155-
x.AddNot(Identifier(c))
128+
func (constraint conflict) apply(c *logic.C, lm *litMapping, subject Identifier) z.Lit {
129+
return c.Or(lm.LitOf(subject).Not(), lm.LitOf(Identifier(constraint)).Not())
156130
}
157131

158-
func (c conflict) order() []Identifier {
132+
func (constraint conflict) order() []Identifier {
159133
return nil
160134
}
161135

@@ -165,3 +139,38 @@ func (c conflict) order() []Identifier {
165139
func Conflict(id Identifier) Constraint {
166140
return conflict(id)
167141
}
142+
143+
type leq struct {
144+
ids []Identifier
145+
n int
146+
}
147+
148+
func (constraint leq) String(subject Identifier) string {
149+
s := make([]string, len(constraint.ids))
150+
for i, each := range constraint.ids {
151+
s[i] = string(each)
152+
}
153+
return fmt.Sprintf("%s permits at most %d of %s", subject, constraint.n, strings.Join(s, ", "))
154+
}
155+
156+
func (constraint leq) apply(c *logic.C, lm *litMapping, subject Identifier) z.Lit {
157+
ms := make([]z.Lit, len(constraint.ids))
158+
for i, each := range constraint.ids {
159+
ms[i] = lm.LitOf(each)
160+
}
161+
return c.CardSort(ms).Leq(constraint.n)
162+
}
163+
164+
func (constraint leq) order() []Identifier {
165+
return nil
166+
}
167+
168+
// AtMost returns a Constraint that forbids solutions that contain
169+
// more than n of the Installables identified by the given
170+
// Identifiers.
171+
func AtMost(n int, ids ...Identifier) Constraint {
172+
return leq{
173+
ids: ids,
174+
n: n,
175+
}
176+
}

pkg/controller/registry/resolver/solver/constraints_test.go

Lines changed: 0 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -1,87 +1,11 @@
11
package solver
22

33
import (
4-
"sort"
54
"testing"
65

76
"github.com/stretchr/testify/assert"
87
)
98

10-
func TestConstraints(t *testing.T) {
11-
type tc struct {
12-
Name string
13-
Constraint Constraint
14-
Subject Identifier
15-
Expected constrainer
16-
}
17-
18-
for _, tt := range []tc{
19-
{
20-
Name: "mandatory",
21-
Constraint: Mandatory(),
22-
Subject: "a",
23-
Expected: constrainer{
24-
pos: []Identifier{"a"},
25-
},
26-
},
27-
{
28-
Name: "prohibited",
29-
Constraint: Prohibited(),
30-
Subject: "a",
31-
Expected: constrainer{
32-
neg: []Identifier{"a"},
33-
},
34-
},
35-
{
36-
Name: "empty dependency",
37-
Constraint: Dependency(),
38-
Subject: "a",
39-
Expected: constrainer{},
40-
},
41-
{
42-
Name: "single dependency",
43-
Constraint: Dependency("b"),
44-
Subject: "a",
45-
Expected: constrainer{
46-
pos: []Identifier{"b"},
47-
neg: []Identifier{"a"},
48-
},
49-
},
50-
{
51-
Name: "multiple dependency",
52-
Constraint: Dependency("x", "y", "z"),
53-
Subject: "a",
54-
Expected: constrainer{
55-
pos: []Identifier{"x", "y", "z"},
56-
neg: []Identifier{"a"},
57-
},
58-
},
59-
{
60-
Name: "conflict",
61-
Constraint: Conflict("b"),
62-
Subject: "a",
63-
Expected: constrainer{
64-
neg: []Identifier{"a", "b"},
65-
},
66-
},
67-
} {
68-
t.Run(tt.Name, func(t *testing.T) {
69-
var x constrainer
70-
tt.Constraint.apply(&x, tt.Subject)
71-
72-
// Literals in lexically increasing order:
73-
sort.Slice(x.pos, func(i, j int) bool {
74-
return x.pos[i] < x.pos[j]
75-
})
76-
sort.Slice(x.neg, func(i, j int) bool {
77-
return x.neg[i] < x.neg[j]
78-
})
79-
80-
assert.Equal(t, tt.Expected, x)
81-
})
82-
}
83-
}
84-
859
func TestOrder(t *testing.T) {
8610
type tc struct {
8711
Name string

pkg/controller/registry/resolver/solver/lit_mapping.go

Lines changed: 2 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -48,27 +48,16 @@ func newLitMapping(installables []Installable) *litMapping {
4848
d.installables[im] = installable
4949
}
5050

51-
var x constrainer
5251
for _, installable := range installables {
5352
for _, constraint := range installable.Constraints() {
54-
x.Reset()
55-
constraint.apply(&x, installable.Identifier())
56-
if x.Empty() {
53+
m := constraint.apply(d.c, &d, installable.Identifier())
54+
if m == z.LitNull {
5755
// This constraint doesn't have a
5856
// useful representation in the SAT
5957
// inputs.
6058
continue
6159
}
6260

63-
d.buf = d.buf[:0]
64-
for _, p := range x.pos {
65-
d.buf = append(d.buf, d.LitOf(p))
66-
}
67-
for _, n := range x.neg {
68-
d.buf = append(d.buf, d.LitOf(n).Not())
69-
}
70-
m := d.c.Ors(d.buf...)
71-
7261
d.constraints[m] = AppliedConstraint{
7362
Installable: installable,
7463
Constraint: constraint,

pkg/controller/registry/resolver/solver/solve_test.go

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -227,6 +227,42 @@ func TestSolve(t *testing.T) {
227227
installable("y"),
228228
},
229229
},
230+
{
231+
Name: "cardinality constraint prevents resolution",
232+
Installables: []Installable{
233+
installable("a", Mandatory(), Dependency("x", "y"), AtMost(1, "x", "y")),
234+
installable("x", Mandatory()),
235+
installable("y", Mandatory()),
236+
},
237+
Error: NotSatisfiable{
238+
{
239+
Installable: installable("a", Mandatory(), Dependency("x", "y"), AtMost(1, "x", "y")),
240+
Constraint: AtMost(1, "x", "y"),
241+
},
242+
{
243+
Installable: installable("x", Mandatory()),
244+
Constraint: Mandatory(),
245+
},
246+
{
247+
Installable: installable("y", Mandatory()),
248+
Constraint: Mandatory(),
249+
},
250+
},
251+
},
252+
{
253+
Name: "cardinality constraint forces alternative",
254+
Installables: []Installable{
255+
installable("a", Mandatory(), Dependency("x", "y"), AtMost(1, "x", "y")),
256+
installable("b", Mandatory(), Dependency("y")),
257+
installable("x"),
258+
installable("y"),
259+
},
260+
Installed: []Installable{
261+
installable("a", Mandatory(), Dependency("x", "y"), AtMost(1, "x", "y")),
262+
installable("b", Mandatory(), Dependency("y")),
263+
installable("y"),
264+
},
265+
},
230266
{
231267
Name: "two dependencies satisfied by one installable",
232268
Installables: []Installable{

0 commit comments

Comments
 (0)