gsn-model-parser 0.16.10__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- gsn_model_parser/__init__.py +5 -0
- gsn_model_parser/__main__.py +3 -0
- gsn_model_parser/gsn.tx +218 -0
- gsn_model_parser/opinion.py +369 -0
- gsn_model_parser/parser.py +1099 -0
- gsn_model_parser-0.16.10.dist-info/METADATA +155 -0
- gsn_model_parser-0.16.10.dist-info/RECORD +11 -0
- gsn_model_parser-0.16.10.dist-info/WHEEL +5 -0
- gsn_model_parser-0.16.10.dist-info/entry_points.txt +2 -0
- gsn_model_parser-0.16.10.dist-info/licenses/LICENSE +21 -0
- gsn_model_parser-0.16.10.dist-info/top_level.txt +1 -0
gsn_model_parser/gsn.tx
ADDED
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
AssuranceModel:
|
|
2
|
+
assurancemodels += GSNDefinition*
|
|
3
|
+
;
|
|
4
|
+
|
|
5
|
+
GSNDefinition:
|
|
6
|
+
Goals|Strategies|Contexts|Assumptions|Justifications|Solutions
|
|
7
|
+
;
|
|
8
|
+
|
|
9
|
+
Goals:
|
|
10
|
+
'GOALS' name=ID '{' goals *= GoalNode '}'
|
|
11
|
+
;
|
|
12
|
+
|
|
13
|
+
Strategies:
|
|
14
|
+
'STRATEGIES' name=ID '{' strategies *= StrategyNode '}'
|
|
15
|
+
;
|
|
16
|
+
|
|
17
|
+
Contexts:
|
|
18
|
+
'CONTEXTS' name=ID '{' contexts *= ContextNode '}'
|
|
19
|
+
;
|
|
20
|
+
|
|
21
|
+
Assumptions:
|
|
22
|
+
'ASSUMPTIONS' name=ID '{' assumptions *= AssumptionNode '}'
|
|
23
|
+
;
|
|
24
|
+
|
|
25
|
+
Justifications:
|
|
26
|
+
'JUSTIFICATIONS' name=ID '{' justifications *= JustificationNode '}'
|
|
27
|
+
;
|
|
28
|
+
|
|
29
|
+
Solutions:
|
|
30
|
+
'SOLUTIONS' name=ID '{' solutions *= SolutionNode '}'
|
|
31
|
+
;
|
|
32
|
+
|
|
33
|
+
GoalSupportNodes:
|
|
34
|
+
GoalNode
|
|
35
|
+
| StrategyNode
|
|
36
|
+
| SolutionNode
|
|
37
|
+
| GoalNodeRef
|
|
38
|
+
| StrategyNodeRef
|
|
39
|
+
| SolutionNodeRef
|
|
40
|
+
;
|
|
41
|
+
|
|
42
|
+
StrategySupportNodes:
|
|
43
|
+
GoalNode
|
|
44
|
+
| StrategyNode
|
|
45
|
+
| GoalNodeRef
|
|
46
|
+
| StrategyNodeRef
|
|
47
|
+
;
|
|
48
|
+
|
|
49
|
+
ContextNodes:
|
|
50
|
+
ContextNode
|
|
51
|
+
| ContextNodeRef
|
|
52
|
+
;
|
|
53
|
+
|
|
54
|
+
AssumptionNodes:
|
|
55
|
+
AssumptionNode
|
|
56
|
+
| AssumptionNodeRef
|
|
57
|
+
;
|
|
58
|
+
|
|
59
|
+
JustificationNodes:
|
|
60
|
+
JustificationNode
|
|
61
|
+
| JustificationNodeRef
|
|
62
|
+
;
|
|
63
|
+
|
|
64
|
+
ALLNodes:
|
|
65
|
+
GoalNode
|
|
66
|
+
| StrategyNode
|
|
67
|
+
| SolutionNode
|
|
68
|
+
| ContextNode
|
|
69
|
+
| AssumptionNode
|
|
70
|
+
| JustificationNode
|
|
71
|
+
;
|
|
72
|
+
|
|
73
|
+
GoalNodeRef:
|
|
74
|
+
'ref_goal' ref=[GoalNode:FQN]
|
|
75
|
+
;
|
|
76
|
+
|
|
77
|
+
StrategyNodeRef:
|
|
78
|
+
'ref_strategy' ref=[StrategyNode:FQN]
|
|
79
|
+
;
|
|
80
|
+
|
|
81
|
+
SolutionNodeRef:
|
|
82
|
+
'ref_solution' ref=[SolutionNode:FQN]
|
|
83
|
+
;
|
|
84
|
+
|
|
85
|
+
ContextNodeRef:
|
|
86
|
+
'ref_context' ref=[ContextNode:FQN]
|
|
87
|
+
;
|
|
88
|
+
|
|
89
|
+
AssumptionNodeRef:
|
|
90
|
+
'ref_assumption' ref=[AssumptionNode:FQN]
|
|
91
|
+
;
|
|
92
|
+
|
|
93
|
+
JustificationNodeRef:
|
|
94
|
+
'ref_justification' ref=[JustificationNode:FQN]
|
|
95
|
+
;
|
|
96
|
+
|
|
97
|
+
Number:
|
|
98
|
+
FLOAT | INT
|
|
99
|
+
;
|
|
100
|
+
|
|
101
|
+
Opinion:
|
|
102
|
+
'(' b=Number ',' d=Number ',' u=Number (',' a=Number)? ')'
|
|
103
|
+
;
|
|
104
|
+
|
|
105
|
+
WarrantBlock:
|
|
106
|
+
'warrant' '{'
|
|
107
|
+
('givenTrue' givenTrue=Opinion)?
|
|
108
|
+
('givenFalse' givenFalse=Opinion)?
|
|
109
|
+
'}'
|
|
110
|
+
;
|
|
111
|
+
|
|
112
|
+
GoalChild:
|
|
113
|
+
GoalSupportNodes | ContextNodes | AssumptionNodes | JustificationNodes
|
|
114
|
+
;
|
|
115
|
+
|
|
116
|
+
StrategyChild:
|
|
117
|
+
StrategySupportNodes | ContextNodes | AssumptionNodes | JustificationNodes
|
|
118
|
+
;
|
|
119
|
+
|
|
120
|
+
GoalDetails:
|
|
121
|
+
child=GoalChild
|
|
122
|
+
(warrants=WarrantBlock)?
|
|
123
|
+
;
|
|
124
|
+
|
|
125
|
+
StrategyDetails:
|
|
126
|
+
child=StrategyChild
|
|
127
|
+
(warrants=WarrantBlock)?
|
|
128
|
+
;
|
|
129
|
+
|
|
130
|
+
GoalNode:
|
|
131
|
+
'goal' name=ID '{'
|
|
132
|
+
(details=BaseNode)?
|
|
133
|
+
('status' status=SolutionStatus)?
|
|
134
|
+
(nodedetails *= GoalDetails)
|
|
135
|
+
'}'
|
|
136
|
+
;
|
|
137
|
+
|
|
138
|
+
StrategyNode:
|
|
139
|
+
'strategy' name=ID '{'
|
|
140
|
+
(details=BaseNode)?
|
|
141
|
+
('argument' argumentType=ArgumentType)?
|
|
142
|
+
(nodedetails *= StrategyDetails)
|
|
143
|
+
'}'
|
|
144
|
+
;
|
|
145
|
+
|
|
146
|
+
ContextNode:
|
|
147
|
+
'context' name=ID '{'
|
|
148
|
+
(details=BaseNode)?
|
|
149
|
+
'}'
|
|
150
|
+
;
|
|
151
|
+
|
|
152
|
+
AssumptionNode:
|
|
153
|
+
'assumption' name=ID '{'
|
|
154
|
+
(details=BaseNode)?
|
|
155
|
+
('opinion' opinion=Opinion)?
|
|
156
|
+
'}'
|
|
157
|
+
;
|
|
158
|
+
|
|
159
|
+
JustificationNode:
|
|
160
|
+
'justification' name=ID '{'
|
|
161
|
+
(details=BaseNode)?
|
|
162
|
+
'}'
|
|
163
|
+
;
|
|
164
|
+
|
|
165
|
+
SolutionNode:
|
|
166
|
+
'solution' name=ID '{'
|
|
167
|
+
(details=BaseNode)?
|
|
168
|
+
('status' status=SolutionStatus)?
|
|
169
|
+
('opinion' opinion=Opinion)?
|
|
170
|
+
'}'
|
|
171
|
+
;
|
|
172
|
+
|
|
173
|
+
BaseNode:
|
|
174
|
+
(uuid=UUIDType)?
|
|
175
|
+
(summary=Summary)?
|
|
176
|
+
(info=Description)?
|
|
177
|
+
(labels *= LabelInfo)
|
|
178
|
+
(artifacts *= URI)
|
|
179
|
+
;
|
|
180
|
+
|
|
181
|
+
SolutionStatus:
|
|
182
|
+
'NotReviewed' | 'Approved' | 'Disapproved'
|
|
183
|
+
;
|
|
184
|
+
|
|
185
|
+
ArgumentType:
|
|
186
|
+
'CONJUNCTION' | 'DISJUNCTION' | 'FUSION' | 'DISCOUNT'
|
|
187
|
+
;
|
|
188
|
+
|
|
189
|
+
UUIDType:
|
|
190
|
+
'uuid' info=GUID
|
|
191
|
+
;
|
|
192
|
+
|
|
193
|
+
Summary:
|
|
194
|
+
'summary' info=MLString
|
|
195
|
+
;
|
|
196
|
+
|
|
197
|
+
Description:
|
|
198
|
+
'info' info=MLString
|
|
199
|
+
;
|
|
200
|
+
|
|
201
|
+
LabelInfo:
|
|
202
|
+
'label' name=ID
|
|
203
|
+
;
|
|
204
|
+
|
|
205
|
+
URI:
|
|
206
|
+
'artifact' uri=MLString
|
|
207
|
+
;
|
|
208
|
+
|
|
209
|
+
FQN:
|
|
210
|
+
ID+['.']
|
|
211
|
+
;
|
|
212
|
+
|
|
213
|
+
MLString: data=/(?ms)\"(.*?)\"/;
|
|
214
|
+
GUID: /\{?[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}\}?/;
|
|
215
|
+
|
|
216
|
+
Comment:
|
|
217
|
+
/\/\/.*$/
|
|
218
|
+
;
|
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
"""Binomial subjective logic opinions and GSN confidence propagation.
|
|
2
|
+
|
|
3
|
+
Names follow Herd et al., "Towards a compositional semantics for quantitative
|
|
4
|
+
confidence assessment in assurance arguments" (arXiv:2605.22213). The
|
|
5
|
+
operators match eSLIM++ (eslimpp) Opinion2d.
|
|
6
|
+
|
|
7
|
+
A stored opinion from the parser is a dict with ``b``, ``d``, ``u`` and
|
|
8
|
+
optional ``a``. Propagation always returns an :class:`Opinion`, which
|
|
9
|
+
:meth:`Opinion.as_dict` writes back in that shape.
|
|
10
|
+
"""
|
|
11
|
+
import math
|
|
12
|
+
import sys
|
|
13
|
+
from dataclasses import dataclass
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
# Match eSLIM++ EPS_v for double.
|
|
17
|
+
EPS = sys.float_info.epsilon
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass(frozen=True)
|
|
21
|
+
class Opinion:
|
|
22
|
+
"""Binomial opinion ω = (b, d, u, a) with u = 1 − b − d."""
|
|
23
|
+
|
|
24
|
+
belief: float
|
|
25
|
+
disbelief: float
|
|
26
|
+
uncertainty: float
|
|
27
|
+
base_rate: float
|
|
28
|
+
|
|
29
|
+
def as_dict(self):
|
|
30
|
+
"""Return the parser's ``b``, ``d``, ``u``, ``a`` dict."""
|
|
31
|
+
return {
|
|
32
|
+
'b': self.belief,
|
|
33
|
+
'd': self.disbelief,
|
|
34
|
+
'u': self.uncertainty,
|
|
35
|
+
'a': self.base_rate,
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def opinion(belief, disbelief, base_rate=0.5):
|
|
40
|
+
"""Build an opinion, deriving uncertainty as ``1 - belief - disbelief``."""
|
|
41
|
+
return Opinion(belief, disbelief, 1 - belief - disbelief, base_rate)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def vacuous(base_rate=0.5):
|
|
45
|
+
"""Opinion with no evidence, ω = (0, 0, 1, a)."""
|
|
46
|
+
return opinion(0, 0, base_rate)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def dogmatic_true(base_rate=0.5):
|
|
50
|
+
"""Certain true opinion, ω = (1, 0, 0, a)."""
|
|
51
|
+
return opinion(1, 0, base_rate)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def dogmatic_false(base_rate=0.5):
|
|
55
|
+
"""Certain false opinion, ω = (0, 1, 0, a)."""
|
|
56
|
+
return opinion(0, 1, base_rate)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
# Support-edge warrants that make conditional deduction the identity
|
|
60
|
+
# (conclusion = premise) for base rates in (0, 1).
|
|
61
|
+
# Masses: given true (1, 0, 0), given false (0, 1, 0). Deduction ignores
|
|
62
|
+
# conditional base rates when both uncertainties are 0. If one support
|
|
63
|
+
# warrant is saved, propagation fills the empty one with these opinions.
|
|
64
|
+
# If neither is saved, the child is passed through.
|
|
65
|
+
IDENTITY_GIVEN_TRUE = dogmatic_true(0.5)
|
|
66
|
+
IDENTITY_GIVEN_FALSE = dogmatic_false(0.5)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def projected_probability(value):
|
|
70
|
+
"""Projected probability P = b + a·u."""
|
|
71
|
+
return value.belief + value.base_rate * value.uncertainty
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _from_bd(belief, disbelief, base_rate):
|
|
75
|
+
return Opinion(belief, disbelief, 1 - belief - disbelief, base_rate)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _binom_proj(belief, uncertainty, base_rate):
|
|
79
|
+
return belief + uncertainty * base_rate
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _div(numerator, denominator):
|
|
83
|
+
"""Divide like IEEE JavaScript: 1/0 is inf and 0/0 is nan."""
|
|
84
|
+
try:
|
|
85
|
+
return numerator / denominator
|
|
86
|
+
except ZeroDivisionError:
|
|
87
|
+
if numerator == 0:
|
|
88
|
+
return math.nan
|
|
89
|
+
return math.copysign(math.inf, numerator)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def conjunction(left, right):
|
|
93
|
+
"""Conjunction of independent claims (ω_x · ω_y).
|
|
94
|
+
|
|
95
|
+
Use when all child claims must hold (divide-and-conquer).
|
|
96
|
+
"""
|
|
97
|
+
base_x = left.base_rate
|
|
98
|
+
base_y = right.base_rate
|
|
99
|
+
factor = (1 - base_x) * base_y * left.belief * right.uncertainty
|
|
100
|
+
factor += base_x * (1 - base_y) * left.uncertainty * right.belief
|
|
101
|
+
factor /= 1 - base_x * base_y
|
|
102
|
+
belief = left.belief * right.belief + factor
|
|
103
|
+
disbelief = left.disbelief + right.disbelief - left.disbelief * right.disbelief
|
|
104
|
+
return _from_bd(belief, disbelief, base_x * base_y)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def disjunction(left, right):
|
|
108
|
+
"""Disjunction of independent claims (ω_x ⊔ ω_y).
|
|
109
|
+
|
|
110
|
+
Use when any one child claim is enough (alternatives).
|
|
111
|
+
"""
|
|
112
|
+
base_x = left.base_rate
|
|
113
|
+
base_y = right.base_rate
|
|
114
|
+
factor = base_x * (1 - base_y) * left.disbelief * right.uncertainty
|
|
115
|
+
factor += (1 - base_x) * base_y * left.uncertainty * right.disbelief
|
|
116
|
+
factor /= base_x + base_y - base_x * base_y
|
|
117
|
+
disbelief = left.disbelief * right.disbelief + factor
|
|
118
|
+
belief = left.belief + right.belief - left.belief * right.belief
|
|
119
|
+
return _from_bd(belief, disbelief, base_x + base_y - base_x * base_y)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def cumulative_fusion(left, right):
|
|
123
|
+
"""Cumulative fusion ⊕ of independent opinions on the same claim.
|
|
124
|
+
|
|
125
|
+
Uncertainty typically shrinks as independent support is added.
|
|
126
|
+
"""
|
|
127
|
+
uncertainty_x = left.uncertainty
|
|
128
|
+
uncertainty_y = right.uncertainty
|
|
129
|
+
mass_denom = uncertainty_x + uncertainty_y - uncertainty_x * uncertainty_y
|
|
130
|
+
|
|
131
|
+
if abs(mass_denom) < EPS:
|
|
132
|
+
belief = (left.belief + right.belief) / 2
|
|
133
|
+
disbelief = (left.disbelief + right.disbelief) / 2
|
|
134
|
+
else:
|
|
135
|
+
belief = (left.belief * uncertainty_y + right.belief * uncertainty_x) / mass_denom
|
|
136
|
+
disbelief = (left.disbelief * uncertainty_y + right.disbelief * uncertainty_x) / mass_denom
|
|
137
|
+
|
|
138
|
+
prior_denom = uncertainty_x + uncertainty_y - 2 * uncertainty_x * uncertainty_y
|
|
139
|
+
if abs(prior_denom) < EPS:
|
|
140
|
+
base_rate = (left.base_rate + right.base_rate) / 2
|
|
141
|
+
else:
|
|
142
|
+
base_rate = (
|
|
143
|
+
left.base_rate * uncertainty_y
|
|
144
|
+
+ right.base_rate * uncertainty_x
|
|
145
|
+
- (left.base_rate + right.base_rate) * uncertainty_x * uncertainty_y
|
|
146
|
+
) / prior_denom
|
|
147
|
+
|
|
148
|
+
return _from_bd(belief, disbelief, base_rate)
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def trust_discount(trust, subject):
|
|
152
|
+
"""Trust discounting ⊗: ω = trust ⊗ subject.
|
|
153
|
+
|
|
154
|
+
Belief in the source scales the subject's belief and disbelief. Disbelief
|
|
155
|
+
and uncertainty about the source become uncertainty about the subject.
|
|
156
|
+
The subject's base rate is kept.
|
|
157
|
+
|
|
158
|
+
b = b_trust · b_subject
|
|
159
|
+
d = b_trust · d_subject
|
|
160
|
+
u = d_trust + u_trust + b_trust · u_subject
|
|
161
|
+
a = a_subject
|
|
162
|
+
"""
|
|
163
|
+
belief = trust.belief * subject.belief
|
|
164
|
+
disbelief = trust.belief * subject.disbelief
|
|
165
|
+
return _from_bd(belief, disbelief, subject.base_rate)
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def conditional_deduction(premise, given_true, given_false):
|
|
169
|
+
"""Conditional deduction ω_y||x = ω_x ⊚ (ω_{y|x}, ω_{y|¬x}).
|
|
170
|
+
|
|
171
|
+
Maps a support or context edge: the premise opinion plus the warrants
|
|
172
|
+
for the premise being true and false.
|
|
173
|
+
"""
|
|
174
|
+
base_x = premise.base_rate
|
|
175
|
+
|
|
176
|
+
# Apex base rate a_y (OpinionNoBase path with EPS guard for masses).
|
|
177
|
+
apex_nom = base_x * given_true.belief + (1 - base_x) * given_false.belief
|
|
178
|
+
apex_denom = 1 - (base_x * given_true.uncertainty + (1 - base_x) * given_false.uncertainty)
|
|
179
|
+
apex_mass = base_x
|
|
180
|
+
if abs(apex_denom) > EPS:
|
|
181
|
+
apex_mass = apex_nom / apex_denom
|
|
182
|
+
|
|
183
|
+
apex_belief = (
|
|
184
|
+
base_x * _binom_proj(given_true.belief, given_true.uncertainty, apex_mass)
|
|
185
|
+
+ (1 - base_x) * _binom_proj(given_false.belief, given_false.uncertainty, apex_mass)
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
uncertainty_apex = min(
|
|
189
|
+
_div(apex_belief - min(given_true.belief, given_false.belief), apex_mass),
|
|
190
|
+
_div(
|
|
191
|
+
1 - apex_belief - min(given_true.disbelief, given_false.disbelief),
|
|
192
|
+
1 - apex_mass,
|
|
193
|
+
),
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
uncertainty = uncertainty_apex - (
|
|
197
|
+
(uncertainty_apex - given_true.uncertainty) * premise.belief
|
|
198
|
+
+ (uncertainty_apex - given_false.uncertainty) * premise.disbelief
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
premise_projection = projected_probability(premise)
|
|
202
|
+
projection = (
|
|
203
|
+
_binom_proj(given_true.belief, given_true.uncertainty, apex_mass) * premise_projection
|
|
204
|
+
+ _binom_proj(given_false.belief, given_false.uncertainty, apex_mass)
|
|
205
|
+
* (1 - premise_projection)
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
belief = projection - apex_mass * uncertainty
|
|
209
|
+
disbelief = 1 - belief - uncertainty
|
|
210
|
+
|
|
211
|
+
# Prior update matches eSLIM++ Opinion::deduction_ (no EPS guard on denom).
|
|
212
|
+
prior_nom = (
|
|
213
|
+
premise.base_rate * given_true.belief
|
|
214
|
+
+ (1 - premise.base_rate) * given_false.belief
|
|
215
|
+
)
|
|
216
|
+
prior_denom = 1 - (
|
|
217
|
+
premise.base_rate * given_true.uncertainty
|
|
218
|
+
+ (1 - premise.base_rate) * given_false.uncertainty
|
|
219
|
+
)
|
|
220
|
+
return _from_bd(belief, disbelief, _div(prior_nom, prior_denom))
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def reduce_opinions(opinions, combine):
|
|
224
|
+
"""Reduce opinions with a pairwise operator, left to right. Empty is None."""
|
|
225
|
+
if not opinions:
|
|
226
|
+
return None
|
|
227
|
+
result = opinions[0]
|
|
228
|
+
for item in opinions[1:]:
|
|
229
|
+
result = combine(result, item)
|
|
230
|
+
return result
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def opinion_from_stored(stored):
|
|
234
|
+
"""Opinion used for a Solution or Assumption, including unset defaults.
|
|
235
|
+
|
|
236
|
+
``stored`` is None or a dict with any of ``b``, ``d``, ``u``, ``a``.
|
|
237
|
+
Missing masses follow the vacuous defaults. When belief, disbelief, and
|
|
238
|
+
uncertainty are all present, uncertainty is kept as stored.
|
|
239
|
+
"""
|
|
240
|
+
if not isinstance(stored, dict):
|
|
241
|
+
return vacuous(0.5)
|
|
242
|
+
|
|
243
|
+
present = [key for key in ('b', 'd', 'u', 'a') if isinstance(stored.get(key), (int, float))]
|
|
244
|
+
if not present:
|
|
245
|
+
return vacuous(0.5)
|
|
246
|
+
|
|
247
|
+
belief = float(stored['b']) if 'b' in present else 0.0
|
|
248
|
+
disbelief = float(stored['d']) if 'd' in present else 0.0
|
|
249
|
+
base_rate = float(stored['a']) if 'a' in present else 0.5
|
|
250
|
+
if 'u' in present and 'b' in present and 'd' in present:
|
|
251
|
+
return Opinion(belief, disbelief, float(stored['u']), base_rate)
|
|
252
|
+
return opinion(belief, disbelief, base_rate)
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def as_opinion(stored):
|
|
256
|
+
"""Return an opinion when ``stored`` has all four masses, else None."""
|
|
257
|
+
if not isinstance(stored, dict):
|
|
258
|
+
return None
|
|
259
|
+
if not all(isinstance(stored.get(key), (int, float)) for key in ('b', 'd', 'u', 'a')):
|
|
260
|
+
return None
|
|
261
|
+
return Opinion(float(stored['b']), float(stored['d']), float(stored['u']), float(stored['a']))
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def _has_stored_field(stored):
|
|
265
|
+
if not isinstance(stored, dict):
|
|
266
|
+
return False
|
|
267
|
+
return any(isinstance(stored.get(key), (int, float)) for key in ('b', 'd', 'u', 'a'))
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def deduce_solved_by_link(child, warrant):
|
|
271
|
+
"""Deduce one solvedBy link.
|
|
272
|
+
|
|
273
|
+
A complete warrant is used as stored. When the other warrant has no
|
|
274
|
+
stored fields, it defaults to the identity opinion. If neither warrant
|
|
275
|
+
is complete, or one is only partial, the child opinion is passed through.
|
|
276
|
+
"""
|
|
277
|
+
if not warrant:
|
|
278
|
+
return child
|
|
279
|
+
given_true = as_opinion(warrant.get('givenTrue'))
|
|
280
|
+
given_false = as_opinion(warrant.get('givenFalse'))
|
|
281
|
+
true_empty = not _has_stored_field(warrant.get('givenTrue'))
|
|
282
|
+
false_empty = not _has_stored_field(warrant.get('givenFalse'))
|
|
283
|
+
|
|
284
|
+
if given_true is not None and given_false is not None:
|
|
285
|
+
return conditional_deduction(child, given_true, given_false)
|
|
286
|
+
if given_true is not None and false_empty:
|
|
287
|
+
return conditional_deduction(child, given_true, IDENTITY_GIVEN_FALSE)
|
|
288
|
+
if given_false is not None and true_empty:
|
|
289
|
+
return conditional_deduction(child, IDENTITY_GIVEN_TRUE, given_false)
|
|
290
|
+
return child
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
def resolve_argument_type(node_type, argument):
|
|
294
|
+
"""How a node combines solvedBy children.
|
|
295
|
+
|
|
296
|
+
Goals always use conjunction. Strategies use their argument, or
|
|
297
|
+
conjunction when it is unset or unknown.
|
|
298
|
+
"""
|
|
299
|
+
if node_type == 'GoalNode':
|
|
300
|
+
return 'conjunction'
|
|
301
|
+
name = str(argument or '').lower()
|
|
302
|
+
if name in ('conjunction', 'disjunction', 'fusion', 'discount'):
|
|
303
|
+
return name
|
|
304
|
+
return 'conjunction'
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
def _combine_opinions(argument_type, opinions):
|
|
308
|
+
if not opinions:
|
|
309
|
+
return vacuous(0.5)
|
|
310
|
+
if len(opinions) == 1:
|
|
311
|
+
return opinions[0]
|
|
312
|
+
|
|
313
|
+
if argument_type == 'discount':
|
|
314
|
+
result = opinions[0]
|
|
315
|
+
for trust in opinions[1:]:
|
|
316
|
+
result = trust_discount(trust, result)
|
|
317
|
+
return result
|
|
318
|
+
|
|
319
|
+
if argument_type == 'disjunction':
|
|
320
|
+
combine = disjunction
|
|
321
|
+
elif argument_type == 'fusion':
|
|
322
|
+
combine = cumulative_fusion
|
|
323
|
+
else:
|
|
324
|
+
combine = conjunction
|
|
325
|
+
return reduce_opinions(opinions, combine) or vacuous(0.5)
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
def try_consume_assumption(support, assumption, warrant):
|
|
329
|
+
"""Unconditional context: ω = ω_A ⊚ (ω_{G|A}, ω_{G|¬A}).
|
|
330
|
+
|
|
331
|
+
ω_{G|¬A} must be the edge givenFalse. ω_{G|A} defaults to the current
|
|
332
|
+
support opinion; a complete edge givenTrue overrides it. Returns None
|
|
333
|
+
when this link should not consume the assumption.
|
|
334
|
+
"""
|
|
335
|
+
if not warrant:
|
|
336
|
+
return None
|
|
337
|
+
given_false = as_opinion(warrant.get('givenFalse'))
|
|
338
|
+
if given_false is None:
|
|
339
|
+
return None
|
|
340
|
+
given_true = as_opinion(warrant.get('givenTrue'))
|
|
341
|
+
if given_true is None:
|
|
342
|
+
given_true = support
|
|
343
|
+
return conditional_deduction(assumption, given_true, given_false)
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
def propagated_from_links(node_type, argument, stored, solved_by, in_context_of, child_opinion):
|
|
347
|
+
"""Derive one node's opinion from its direct links.
|
|
348
|
+
|
|
349
|
+
``solved_by`` and ``in_context_of`` are sequences of
|
|
350
|
+
``(child, child_type, warrant)``. ``child_opinion(child)`` returns the
|
|
351
|
+
child's propagated :class:`Opinion`. ``warrant`` is None or a dict with
|
|
352
|
+
``givenTrue`` and ``givenFalse`` opinion dicts.
|
|
353
|
+
"""
|
|
354
|
+
if node_type in ('SolutionNode', 'AssumptionNode'):
|
|
355
|
+
return opinion_from_stored(stored)
|
|
356
|
+
if node_type in ('GoalNode', 'StrategyNode'):
|
|
357
|
+
link_opinions = [
|
|
358
|
+
deduce_solved_by_link(child_opinion(child), warrant)
|
|
359
|
+
for child, _child_type, warrant in solved_by
|
|
360
|
+
]
|
|
361
|
+
result = _combine_opinions(resolve_argument_type(node_type, argument), link_opinions)
|
|
362
|
+
for child, child_type, warrant in in_context_of:
|
|
363
|
+
if child_type != 'AssumptionNode':
|
|
364
|
+
continue
|
|
365
|
+
consumed = try_consume_assumption(result, child_opinion(child), warrant)
|
|
366
|
+
if consumed is not None:
|
|
367
|
+
result = consumed
|
|
368
|
+
return result
|
|
369
|
+
return vacuous(0.5)
|