-
Notifications
You must be signed in to change notification settings - Fork 13
/
weighing.go
421 lines (382 loc) · 11.4 KB
/
weighing.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
// Copyright © 2015 The Carneades Authors
// This Source Code Form is subject to the terms of the
// Mozilla Public License, v. 2.0. If a copy of the MPL
// was not distributed with this file, You can obtain one
// at http://mozilla.org/MPL/2.0/.
// Carneades Argument Evaluation Structure (CAES)
// This version of CAES supports cyclic argument graphs,
// cumulative arguments and IBIS.
package caes
// types and procedures for weighing arguments
import (
"fmt"
"reflect"
"sort"
"time"
)
// for sorting arguments by property order
type ByProperties struct {
args []*Argument
order []PropertyOrder
}
type Criteria struct {
HardConstraints []int // premise indices of hard constraints
SoftConstraints map[string]SoftConstraint // role name to soft constraint
}
type Order int
const (
Descending Order = iota
Ascending
)
// PropertyOrder: orders the values of a property so
// that the highest-ranked values appear first when a
// sequence of values is sorted according to the order
// The Order field is ignored, if the order is specified
// explicitly, by providing an ordered slice of values
type PropertyOrder struct {
Property string
Order Order // implicit ordering (ascending or descending)
Values []string // explicit ordering, highest ranked values first
}
type SoftConstraint struct {
// Factor: relative weight of the constraint, in range of 0.00 to 1.00
Factor float64
// NormalizedValues: string to value in range of 0.0 to 1.0
NormalizedValues map[string]float64
}
var BasicWeighingFunctions = map[string]WeighingFunction{
"linked": LinkedWeighingFunction,
"convergent": ConvergentWeighingFunction,
"cumulative": CumulativeWeighingFunction,
"factorized": FactorizedWeighingFunction,
}
// Note: the names of the basic schemes are the same
// as the corresponding basic weighing functions.
var BasicSchemes = map[string]*Scheme{
"linked": &Scheme{Id: "linked", Weight: LinkedWeighingFunction},
"convergent": &Scheme{Id: "convergent", Weight: ConvergentWeighingFunction},
"cumulative": &Scheme{Id: "cumulative", Weight: CumulativeWeighingFunction},
"factorized": &Scheme{Id: "factorized", Weight: FactorizedWeighingFunction},
}
func IsBasicScheme(scheme *Scheme) bool {
for _, s := range BasicSchemes {
if scheme == s {
return true
}
}
return false
}
func LinkedWeighingFunction(arg *Argument, l Labelling) float64 {
for _, p := range arg.Premises {
if l[p.Stmt] != In {
return 0.0
}
}
return 1.0
}
func ConvergentWeighingFunction(arg *Argument, l Labelling) float64 {
for _, p := range arg.Premises {
if l[p.Stmt] == In {
return 1.0
}
}
return 0.0
}
func CumulativeWeighingFunction(arg *Argument, l Labelling) float64 {
n := len(arg.Premises)
m := 0
for _, p := range arg.Premises {
if l[p.Stmt] == In {
m++
}
}
return float64(m) / float64(n)
}
// Count the number of distinct premises for all arguments of an issue.
func premiseCount(issue *Issue) int {
m := make(map[string]bool)
for _, p := range issue.Positions {
for _, arg := range p.Args {
for _, pr := range arg.Premises {
m[pr.Stmt.Text] = true
}
}
}
return len(m)
}
// A factorized argument, like a linked argument, has no weight unless all
// of its premises are labelled In. If all the premises are in, the weight
// of the argument depends on the number of its premises, compared to
// other arguments about the same issue. The greater the number of premises,
// relative to the other arguments, the greater the weight of the argument.
// See the jogging example for an illustration of its use. Can be used
// to simulate HYPO-style case-based reasoning.
func FactorizedWeighingFunction(arg *Argument, l Labelling) float64 {
n := premiseCount(arg.Conclusion.Issue)
m := 0
for _, p := range arg.Premises {
switch l[p.Stmt] {
case In:
m++
case Out:
return 0.0
default:
continue
}
}
return float64(m) / float64(n)
}
// The constant weight function is derived from and generalizes the linked weighing
// function. Like the linked weighing function, if any of the
// premises is Out, the weight is 0.0. If all of the premises
// are In, the weight of the argument is the constant weight assigned by
// to the scheme. Whereas the linked weighing function always assigns the
// weight 1.0 if all the premises are In, the constant weighing function is
// more flexible and allows some other weight to be assigned when all the
// premises are In.
func ConstantWeighingFunction(w float64) WeighingFunction {
return func(arg *Argument, l Labelling) float64 {
for _, p := range arg.Premises {
if l[p.Stmt] != In {
return 0.0
}
}
return w
}
}
func CriteriaWeighingFunction(cs *Criteria) WeighingFunction {
return func(arg *Argument, l Labelling) float64 {
// check the hard constraints
for _, hc := range cs.HardConstraints {
for i, p := range arg.Premises {
if hc == i && l[p.Stmt] == Out {
return 0.0 // KO Criteria
}
}
}
// All the hard constraints are satisfied.
// Compute the weighted sum of the soft constraints
// factorSum is the sum of the factors of all soft constraints.
// Let f be the factor of some constraint. The relative weight
// of the constraint is f/factorSum .
factorSum := 0.0
for _, sc := range cs.SoftConstraints {
factorSum += sc.Factor
}
weight := 0.0
for property, sc := range cs.SoftConstraints {
v, ok := arg.PropertyValue(property, l)
if !ok {
// the argument does have a premise for the specified property
return 0.0
}
// If v is not one of the specified values of a soft constraint
// the normalized value will be 0.0
relativeWeight := sc.Factor / factorSum
weight = weight + (relativeWeight * sc.NormalizedValues[v.String()])
}
return weight
}
}
// Define the methods needed to make ByProperties match
// the sort.Interface interface.
func (s ByProperties) Len() int {
return len(s.args)
}
func (s ByProperties) Swap(i, j int) {
s.args[i], s.args[j] = s.args[j], s.args[i]
}
func (s ByProperties) Less(i, j int) bool {
ai := s.args[i]
aj := s.args[j]
// indexOf: returns the index of a string s in a list l
// or the length of l if s is not in l.
indexOf := func(s string, l []string) int {
for i, v := range l {
if s == v {
return i
}
}
return len(l) + 1
}
for _, p := range s.order {
aip := ai.Scheme.Metadata[p.Property]
ajp := aj.Scheme.Metadata[p.Property]
if reflect.TypeOf(aip) != reflect.TypeOf(ajp) {
// skip uncomparable values and try sorting by the next property
continue
}
switch aip.(type) {
case string:
if aip.(string) == ajp.(string) {
continue
}
// try interpreting the strings as times
ti, err1 := time.Parse("2006-01-02", aip.(string))
tj, err2 := time.Parse("2006-01-02", ajp.(string))
if err1 == nil && err2 == nil {
// both strings represent times in the supported format
switch p.Order {
case Ascending:
return ti.Before(tj)
case Descending:
return ti.After(tj)
}
}
if err1 == nil || err2 == nil {
// only one of the values represents time
continue
}
// neither string represents time
switch {
case len(p.Values) > 0:
if indexOf(aip.(string), p.Values) < indexOf(ajp.(string), p.Values) {
return true
} else {
continue
}
case aip.(string) < ajp.(string):
return true
default:
continue
}
case int:
if aip.(int) == ajp.(int) {
continue
}
switch p.Order {
case Ascending:
return aip.(int) < ajp.(int)
case Descending:
return aip.(int) > ajp.(int)
}
case float64:
if aip.(float64) == ajp.(float64) {
continue
}
switch p.Order {
case Ascending:
return aip.(float64) < ajp.(float64)
case Descending:
return aip.(float64) > ajp.(float64)
}
default:
continue
}
}
return false
}
func genEqualArgsFunction(o []PropertyOrder) func(*Argument, *Argument) bool {
return func(a1, a2 *Argument) bool {
for _, p := range o {
a1 := a1.Metadata[p.Property]
a2 := a2.Metadata[p.Property]
if reflect.TypeOf(a1) != reflect.TypeOf(a2) {
// skip uncomparable values and try sorting by the next property
continue
}
switch a1.(type) {
case string:
return a1.(string) == a2.(string)
case int:
return a1.(int) == a2.(int)
case float64:
return a1.(float64) == a2.(float64)
default:
continue
}
}
return false
}
}
// Debugging code
func printGroup(g []*Argument) {
fmt.Printf("group[")
for _, a := range g {
fmt.Printf("%v ", a.Id)
}
fmt.Printf("]\n")
}
// PreferenceWeighingFunction: Orders arguments by the metadata properties of
// the schemes instantiated by the arguments. Can be used to model, e.g.,
// Lex Superior and Lex Posterior. Can also be used to simulate value-based
// argumentation frameworks, using value properties and an ordering
// of values. If any premise of the argument is Out, the
// argument weights 0.0. If no premise is Out but
// the conclusion of the argument is not at issue, the argument weights 1.0.
// Otherwise all the arguments are the issue are ordered according to
// given PropertyOrder and assigned weights which respect this order.
// To do: Considering caching weights to improve efficiency, since
// currently the arguments are sorted multiple times, once for each
// argument being weighed. Problem: avoiding a memory leak when used the
// cache in a long running service
func PreferenceWeighingFunction(o []PropertyOrder) WeighingFunction {
return func(arg *Argument, l Labelling) float64 {
c := arg.Conclusion
issue := c.Issue
w := LinkedWeighingFunction(arg, l)
if issue == nil || w == 0.0 {
return w
}
// collect the arguments for all positions of the issue
args := []*Argument{}
for _, p := range issue.Positions {
for _, a := range p.Args {
args = append(args, a)
}
}
// Sort the arguments, so that the weakest arguments
// appear first in the args list (ascending order)
sort.Sort(ByProperties{args: args, order: o})
// fmt.Printf("sorted args:\n")
// for _, arg := range args {
// fmt.Printf("\t%v\n", arg.Id)
// }
// groups is in an ordered list of sets of arguments,
// representing a partial order. The groups are ordered
// by increasing strength (ascending order)
var groups [][]*Argument
group := []*Argument{}
equalArgs := genEqualArgsFunction(o)
for _, a := range args {
if len(group) == 0 {
// first arg in the group
group = append(group, a)
} else {
if equalArgs(a, group[0]) {
group = append(group, a)
printGroup(group)
} else {
// start a new group
groups = append(groups, group)
group = []*Argument{a}
}
}
}
groups = append(groups, group)
// Debugging, to see if the groups have been formed correctly
// for i, g := range groups {
// fmt.Printf("Group %v. ", i)
// printGroup(g)
// }
// The weight of an argument depends on its place in the partial
// order. All arguments in a group (equivalence class) have the
// same weight. Arguments in the last group will have the weight
// 1.0. All arguments have some weight greater than 0.0
// If there are ten groups, arguments in the first group
// will have the weight 0.1
// Find arg in the partial order and returns its weight.
n := float64(len(groups))
var weight float64
for i, group := range groups {
weight = ((float64(i) + 1.0) * 1.0) / n
for _, a := range group {
if arg.Id == a.Id {
// found arg
return weight
}
}
}
return 0.0 // The argument was not found in some group. Should not happen.
}
}