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.
@@ -0,0 +1,5 @@
1
+ """Parse Goal Structuring Notation (.gsn) models."""
2
+
3
+ from gsn_model_parser.parser import GSNModel, GSNNode, get_gsn_nodes, list_solutions, load_model
4
+
5
+ __all__ = ['GSNModel', 'GSNNode', 'get_gsn_nodes', 'list_solutions', 'load_model']
@@ -0,0 +1,3 @@
1
+ from gsn_model_parser.parser import main
2
+
3
+ main()
@@ -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)