engineering-argument-language 3.2.3__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.
Files changed (72) hide show
  1. eal/__init__.py +3 -0
  2. eal/abstractions.py +144 -0
  3. eal/acquisition_coordination.py +80 -0
  4. eal/api_load_methods.py +77 -0
  5. eal/aspic.py +478 -0
  6. eal/aspic_compiler.py +445 -0
  7. eal/aspic_export.py +309 -0
  8. eal/builtin_methods.py +115 -0
  9. eal/catalogue.py +384 -0
  10. eal/cli.py +204 -0
  11. eal/client_transports.py +69 -0
  12. eal/collection_identity.py +28 -0
  13. eal/collection_scheduler.py +72 -0
  14. eal/command_process.py +118 -0
  15. eal/command_supervisor.py +112 -0
  16. eal/composition.py +531 -0
  17. eal/credentials.py +33 -0
  18. eal/dialectic.py +321 -0
  19. eal/discovery.py +127 -0
  20. eal/evaluator.py +619 -0
  21. eal/expressions.py +404 -0
  22. eal/extensions.py +38 -0
  23. eal/formatter.py +156 -0
  24. eal/generated/EALLexer.py +434 -0
  25. eal/generated/EALParser.py +6559 -0
  26. eal/generated/EALVisitor.py +393 -0
  27. eal/generated/__init__.py +1 -0
  28. eal/host.py +179 -0
  29. eal/host_redaction.py +51 -0
  30. eal/knowledge.py +96 -0
  31. eal/limits.py +88 -0
  32. eal/mcp_guard.py +56 -0
  33. eal/methods.py +463 -0
  34. eal/model.py +266 -0
  35. eal/model_context.py +55 -0
  36. eal/modes.py +121 -0
  37. eal/observation_reuse.py +128 -0
  38. eal/operation_contracts.py +25 -0
  39. eal/operations.py +155 -0
  40. eal/packets.py +604 -0
  41. eal/parser.py +407 -0
  42. eal/planning.py +136 -0
  43. eal/propositions.py +231 -0
  44. eal/reachability.py +85 -0
  45. eal/reasoning/__init__.py +25 -0
  46. eal/reasoning/abductive.py +45 -0
  47. eal/reasoning/analogical.py +41 -0
  48. eal/reasoning/causal.py +46 -0
  49. eal/reasoning/counterfactual.py +64 -0
  50. eal/reasoning/deductive.py +72 -0
  51. eal/reasoning/inductive.py +29 -0
  52. eal/reasoning/strategy.py +18 -0
  53. eal/reasoning/structured.py +11 -0
  54. eal/reasoning/temporal.py +47 -0
  55. eal/reasoning/validation.py +90 -0
  56. eal/registered_assessment.py +181 -0
  57. eal/runtime.py +454 -0
  58. eal/sampled_negative.py +131 -0
  59. eal/scope_transfer.py +59 -0
  60. eal/semantics.py +656 -0
  61. eal/server.py +79 -0
  62. eal/server_auth.py +37 -0
  63. eal/server_settings.py +258 -0
  64. eal/source_printer.py +129 -0
  65. eal/store.py +272 -0
  66. eal/tool_acquisition.py +383 -0
  67. engineering_argument_language-3.2.3.dist-info/METADATA +188 -0
  68. engineering_argument_language-3.2.3.dist-info/RECORD +72 -0
  69. engineering_argument_language-3.2.3.dist-info/WHEEL +5 -0
  70. engineering_argument_language-3.2.3.dist-info/entry_points.txt +4 -0
  71. engineering_argument_language-3.2.3.dist-info/licenses/LICENSE +24 -0
  72. engineering_argument_language-3.2.3.dist-info/top_level.txt +1 -0
eal/propositions.py ADDED
@@ -0,0 +1,231 @@
1
+ """EAL/3 scalar propositions and versioned method/input correspondence.
2
+
3
+ Metadata asserts an engineering interpretation. Checks establish correspondence
4
+ within that interpretation; they cannot authenticate a sensor or interpret prose.
5
+ """
6
+ from __future__ import annotations
7
+
8
+ from dataclasses import asdict, replace
9
+ from copy import deepcopy
10
+ from fractions import Fraction
11
+ import hashlib
12
+ import json
13
+ import math
14
+ import operator
15
+ import re
16
+
17
+ from .model import Proposition, Predicate
18
+ from .expressions import expression_errors, predicate_paths, evaluate_expression, format_expression
19
+ from .builtin_methods import BUILTIN_SPECS
20
+
21
+ SCHEMA = 'EAL/typed-input/1'
22
+ # Unit scales are exact rational multiples of the declared base unit. Absolute
23
+ # temperatures and offset conversions are deliberately outside this fragment.
24
+ UNITS = {
25
+ '1': ('dimensionless', '1'),
26
+ 'Pa': ('pressure', '1'), 'kPa': ('pressure', '1000'), 'MPa': ('pressure', '1000000'),
27
+ 's': ('time', '1'), 'ms': ('time', '0.001'), 'us': ('time', '0.000001'),
28
+ 'ns': ('time', '0.000000001'), 'min': ('time', '60'), 'h': ('time', '3600'),
29
+ 'm': ('length', '1'), 'mm': ('length', '0.001'), 'cm': ('length', '0.01'), 'km': ('length', '1000'),
30
+ 'kg': ('mass', '1'), 'g': ('mass', '0.001'),
31
+ 'K': ('temperature_difference', '1'), 'delta_degC': ('temperature_difference', '1'),
32
+ 'm/s': ('velocity', '1'), 'km/h': ('velocity', '5/18'),
33
+ 'm3/s': ('volumetric_flow', '1'), 'L/s': ('volumetric_flow', '0.001'),
34
+ }
35
+ QUANTITIES = {name: name for name in ('pressure', 'time', 'length', 'mass',
36
+ 'temperature_difference', 'velocity', 'volumetric_flow', 'dimensionless')}
37
+ QUANTITIES.update(probability='dimensionless', proposition='dimensionless')
38
+ # Each path denotes a specified statistic, not an arbitrary result field. basis
39
+ # means the unit of the measured quantity; boolean results have no numeric unit.
40
+ # These read-only discovery views derive from the same versioned specifications
41
+ # used to construct the registered MethodContracts.
42
+ OUTPUTS = {mode: dict(spec.outputs) for mode, spec in BUILTIN_SPECS.items() if spec.outputs}
43
+ QUERY_FIELDS = {mode: spec.query_fields for mode, spec in BUILTIN_SPECS.items() if spec.outputs}
44
+ _COMPARISONS = {'==': operator.eq, '!=': operator.ne, '<': operator.lt,
45
+ '<=': operator.le, '>': operator.gt, '>=': operator.ge}
46
+
47
+
48
+ def describe_bindings(registry=None):
49
+ """Machine-readable, versioned discovery of the implemented closed fragment."""
50
+ from .methods import default_registry
51
+ registry = registry or default_registry()
52
+ return {'schema': SCHEMA, 'registry_fingerprint': registry.fingerprint, 'quantities': dict(QUANTITIES),
53
+ 'units': {name: {'dimension': dim, 'base_scale': scale} for name, (dim, scale) in UNITS.items()},
54
+ 'methods': registry.describe(),
55
+ 'envelope_fields': ['schema', 'method', 'subject', 'quantity', 'unit', 'scope',
56
+ 'valid_from', 'valid_until', 'payload'],
57
+ 'interval': 'half-open [valid_from, valid_until); input must contain claim interval',
58
+ 'interpretation': 'metadata correspondence; physical relevance and prose correspondence remain asserted'}
59
+
60
+
61
+ def _contract(method, registry=None):
62
+ from .methods import default_registry
63
+ return (registry or default_registry()).get(method)
64
+
65
+
66
+ def _output_type(method, path, registry=None):
67
+ contract = _contract(method, registry)
68
+ return contract.output_type(path) if contract else None
69
+
70
+
71
+ def proposition_errors(proposition: Proposition, method: str | None = None, registry=None):
72
+ """Static shape/type errors without reading observations."""
73
+ from .semantics import parse_time, _check_json_resources
74
+ errors = []
75
+ for key in ('subject', 'scope'):
76
+ value = getattr(proposition, key)
77
+ if not value.strip() or len(value) > 4096:
78
+ errors.append(f'{key} must be nonblank and at most 4096 characters')
79
+ if proposition.quantity not in QUANTITIES:
80
+ errors.append(f'Unknown quantity {proposition.quantity!r}')
81
+ if proposition.unit not in UNITS:
82
+ errors.append(f'Unknown unit {proposition.unit!r}')
83
+ elif proposition.quantity in QUANTITIES and UNITS[proposition.unit][0] != QUANTITIES[proposition.quantity]:
84
+ errors.append(f'Unit {proposition.unit!r} does not measure quantity {proposition.quantity!r}')
85
+ try:
86
+ if parse_time(proposition.valid_from) >= parse_time(proposition.valid_until):
87
+ errors.append('Proposition valid_from must precede valid_until')
88
+ except (ValueError, TypeError):
89
+ errors.append('Proposition interval requires ISO-8601 timestamps with timezones')
90
+ expected = proposition.result.expected
91
+ if proposition.result.expression is None and (not isinstance(expected, (bool, int, float)) or isinstance(expected, float) and not math.isfinite(expected)):
92
+ errors.append('A scalar proposition result must compare a finite number or boolean')
93
+ if isinstance(expected, bool) and proposition.result.operator not in ('==', '!='):
94
+ errors.append('Boolean proposition results support only == and !=')
95
+ try:
96
+ _check_json_resources(proposition.query)
97
+ json.dumps(proposition.query, allow_nan=False).encode('utf-8')
98
+ except (ValueError, TypeError, UnicodeError, RecursionError):
99
+ errors.append('Proposition query must be finite JSON')
100
+ if method is not None:
101
+ from .methods import schema_errors
102
+ contract = _contract(method, registry)
103
+ if contract is None:
104
+ errors.append(f'Unknown registered method {method!r}')
105
+ return errors
106
+ errors.extend(schema_errors(proposition.query, contract.query_schema, 'query'))
107
+ if proposition.result.expression is not None:
108
+ errors.extend(expression_errors(proposition.result.expression, contract.output_schema))
109
+ paths = predicate_paths(proposition.result)
110
+ if not paths:
111
+ errors.append('A proposition result must depend on a declared method output')
112
+ for path in paths:
113
+ if contract.output_type(path) is None:
114
+ errors.append(f'Method {method!r} has no typed result {path!r}')
115
+ else:
116
+ output = contract.output_type(proposition.result.path)
117
+ if output is None:
118
+ errors.append(f'Method {method!r} has no typed result {proposition.result.path!r}')
119
+ elif (output == 'boolean') != isinstance(expected, bool):
120
+ errors.append('Proposition result type does not match the method output type')
121
+ if proposition.quantity not in contract.quantities:
122
+ errors.append(f'Method {method!r} does not accept quantity {proposition.quantity!r}')
123
+ return errors
124
+
125
+
126
+ def prepare_binding(proposition: Proposition, method: str, evidence_id: str, value, registry=None):
127
+ """Check complete input correspondence before passing a payload to a method."""
128
+ from .semantics import parse_time
129
+ contract = _contract(method, registry)
130
+ if contract is None:
131
+ return None, {"status": "unsupported", "reasons": [f"Unknown registered method {method!r}"]}
132
+ fields = {'schema', 'method', 'subject', 'quantity', 'unit', 'scope', 'valid_from', 'valid_until', 'payload'}
133
+ trace = {'evidence_id': evidence_id, 'method': contract.identifier, 'proposition': asdict(proposition),
134
+ 'method_contract': contract.describe(),
135
+ 'prose_verified': False, 'physical_interpretation_verified': False}
136
+ reasons = []
137
+ if not isinstance(value, dict) or set(value) != fields:
138
+ return None, {**trace, 'status': 'unsupported', 'reasons': ['Typed input requires exactly the declared envelope fields']}
139
+ for key, expected in (('schema', SCHEMA), ('method', contract.identifier), ('subject', proposition.subject),
140
+ ('quantity', proposition.quantity), ('scope', proposition.scope)):
141
+ if value.get(key) != expected:
142
+ reasons.append(f'Typed input {key} does not match the proposition or method')
143
+ unit = value.get('unit')
144
+ if not isinstance(unit, str) or unit not in UNITS or UNITS[unit][0] != QUANTITIES[proposition.quantity]:
145
+ reasons.append('Typed input unit is incompatible with the proposition quantity')
146
+ if contract.exact_unit and unit != proposition.unit:
147
+ reasons.append('This method embeds dimensional query constants and requires the exact proposition unit')
148
+ try:
149
+ start, end = parse_time(value['valid_from']), parse_time(value['valid_until'])
150
+ if start >= end or start > parse_time(proposition.valid_from) or end < parse_time(proposition.valid_until):
151
+ reasons.append('Typed input interval does not contain the whole proposition interval')
152
+ except (ValueError, TypeError, OverflowError):
153
+ reasons.append('Typed input interval requires valid timezone-aware timestamps')
154
+ # Match the intended mathematical question independently of measured values.
155
+ # JSON canonicalisation distinguishes booleans from numbers and retains the
156
+ # whole formula/model, including unused premises and zero coefficients.
157
+ payload = value['payload']
158
+ if not isinstance(payload, dict):
159
+ reasons.append('Typed input payload must be an object')
160
+ else:
161
+ for key in contract.query_fields:
162
+ expected = json.dumps(proposition.query.get(key), sort_keys=True, separators=(',', ':'), allow_nan=False)
163
+ try:
164
+ actual = json.dumps(payload[key], sort_keys=True, separators=(',', ':'), allow_nan=False)
165
+ except (KeyError, ValueError, TypeError, RecursionError):
166
+ actual = None
167
+ if actual != expected:
168
+ reasons.append(f'Typed input query field {key!r} does not match the declared proposition query')
169
+ # Hash the entire formal problem, rather than only its numerical answer.
170
+ try:
171
+ encoded = json.dumps(value['payload'], sort_keys=True, separators=(',', ':'), ensure_ascii=False, allow_nan=False).encode('utf-8')
172
+ trace.update(input_payload_digest=hashlib.sha256(encoded).hexdigest(), formal_query=value['payload'], input_unit=unit)
173
+ except (ValueError, TypeError, UnicodeError, RecursionError):
174
+ reasons.append('Typed input payload must be finite JSON')
175
+ trace.update(status='unsupported' if reasons else 'bound', reasons=reasons)
176
+ return (None if reasons else value['payload']), trace
177
+
178
+
179
+ def check_result(proposition: Proposition, method: str, details: dict, trace: dict, registry=None):
180
+ """Evaluate the formal result predicate, converting only physical statistics."""
181
+ if proposition.result.expression is not None:
182
+ converted = deepcopy(details)
183
+ observations = {}
184
+ for path in predicate_paths(proposition.result):
185
+ output = _output_type(method, path, registry)
186
+ check = check_result(replace(proposition, result=Predicate(path, '==', False if output == 'boolean' else 0)),
187
+ method, details, trace, registry)
188
+ if 'actual' not in check:
189
+ return check
190
+ observations[path] = check['actual']
191
+ destination = converted
192
+ parts = path.split('.')
193
+ for field in parts[:-1]:
194
+ destination = destination[field]
195
+ destination[parts[-1]] = check['actual']
196
+ checked = evaluate_expression(proposition.result.expression, converted)
197
+ if not checked.known or type(checked.value) is not bool:
198
+ return {**trace, 'status': 'unsupported', 'reasons': [checked.reason or 'Result requires a Boolean']}
199
+ return {**trace, 'status': 'supported' if checked.value else 'unsupported', 'holds': checked.value,
200
+ 'actual': observations, 'output_unit': proposition.unit,
201
+ 'reasons': [format_expression(proposition.result.expression) + (' holds' if checked.value else ' does not hold')]}
202
+ actual = details
203
+ for field in proposition.result.path.split('.'):
204
+ if not isinstance(actual, dict) or field not in actual:
205
+ return {**trace, 'status': 'unsupported', 'reasons': ['Bound method output is missing']}
206
+ actual = actual[field]
207
+ output = _output_type(method, proposition.result.path, registry)
208
+ if output is None:
209
+ return {**trace, 'status': 'unsupported', 'reasons': ['Bound method output is not declared by the method contract']}
210
+ is_numeric = type(actual) in (int, float) and (not isinstance(actual, float) or math.isfinite(actual))
211
+ if output == 'boolean' and type(actual) is not bool or output != 'boolean' and not is_numeric:
212
+ return {**trace, 'status': 'unsupported', 'reasons': ['Bound method output has an incompatible type']}
213
+ output_unit = None if output == 'boolean' else '1'
214
+ if output == 'basis':
215
+ ratio = Fraction(UNITS[trace['input_unit']][1]) / Fraction(UNITS[proposition.unit][1])
216
+ if ratio != 1:
217
+ exact = Fraction(str(actual)) * ratio
218
+ try:
219
+ actual = float(exact)
220
+ except OverflowError:
221
+ return {**trace, 'status': 'unsupported', 'reasons': ['Unit conversion exceeds the finite numeric range']}
222
+ if exact != 0 and actual == 0:
223
+ return {**trace, 'status': 'unsupported', 'reasons': ['Unit conversion underflow would erase a nonzero result']}
224
+ output_unit = proposition.unit
225
+ if type(actual) is float and not math.isfinite(actual):
226
+ return {**trace, 'status': 'unsupported', 'reasons': ['Converted result is not finite']}
227
+ holds = _COMPARISONS[proposition.result.operator](actual, proposition.result.expected)
228
+ return {**trace, 'status': 'supported' if holds else 'unsupported', 'holds': holds,
229
+ 'actual': actual, 'output_unit': output_unit,
230
+ 'reasons': [f'Bound result {proposition.result.path!r} {proposition.result.operator} '
231
+ f'{proposition.result.expected!r} {"holds" if holds else "does not hold"}']}
eal/reachability.py ADDED
@@ -0,0 +1,85 @@
1
+ """An optional, bounded finite-state model checker for EAL/3.
2
+
3
+ The host installs this method explicitly. An EAL source cannot load Python code.
4
+ It decides reachability in the *supplied graph*, not whether that graph describes
5
+ an actual device. The path is a checkable counterexample when one exists.
6
+ """
7
+ from __future__ import annotations
8
+
9
+ from collections import deque
10
+
11
+ from .methods import MethodContract, default_registry
12
+
13
+
14
+ def finite_reachability(payload: dict) -> dict:
15
+ """Find a shortest path to a forbidden state within the inclusive horizon."""
16
+ count = payload["node_count"]
17
+ start = payload["start"]
18
+ forbidden = payload["forbidden"]
19
+ horizon = payload["horizon"]
20
+ edges = payload["edges"]
21
+ if start >= count or any(node >= count for node in forbidden):
22
+ raise ValueError("Start and forbidden states must belong to the graph")
23
+ if len(set(forbidden)) != len(forbidden) or len({tuple(edge) for edge in edges}) != len(edges):
24
+ raise ValueError("Duplicate forbidden states or transition edges")
25
+ if any(source >= count or target >= count for source, target in edges):
26
+ raise ValueError("A transition endpoint is outside the graph")
27
+ adjacency = [[] for _ in range(count)]
28
+ for source, target in edges:
29
+ adjacency[source].append(target)
30
+ for successors in adjacency:
31
+ successors.sort()
32
+ queue = deque([(start,)])
33
+ seen = {start}
34
+ forbidden_set = set(forbidden)
35
+ while queue:
36
+ path = queue.popleft()
37
+ if path[-1] in forbidden_set:
38
+ return {"reachable": True, "shortest_length": len(path) - 1,
39
+ "counterexample": list(path), "visited": len(seen)}
40
+ if len(path) - 1 == horizon:
41
+ continue
42
+ for successor in adjacency[path[-1]]:
43
+ if successor not in seen:
44
+ seen.add(successor)
45
+ queue.append((*path, successor))
46
+ return {"reachable": False, "shortest_length": -1,
47
+ "counterexample": [], "visited": len(seen)}
48
+
49
+
50
+ NODE = {"type": "integer", "minimum": 0, "maximum": 7}
51
+ COUNT = {"type": "integer", "minimum": 1, "maximum": 8}
52
+ HORIZON = {"type": "integer", "minimum": 0, "maximum": 8}
53
+ FORBIDDEN = {"type": "array", "items": NODE, "minItems": 1, "maxItems": 8}
54
+ EDGES = {"type": "array", "items": {"type": "array", "items": NODE,
55
+ "minItems": 2, "maxItems": 2}, "maxItems": 64}
56
+
57
+ REACHABILITY_CONTRACT = MethodContract(
58
+ identifier="engineering/reachability/1", evidence_kind="finite_graph",
59
+ input_schema={"type": "object", "properties": {
60
+ "node_count": COUNT, "edges": EDGES, "start": NODE,
61
+ "forbidden": FORBIDDEN, "horizon": HORIZON},
62
+ "required": ["node_count", "edges", "start", "forbidden", "horizon"],
63
+ "additionalProperties": False},
64
+ query_schema={"type": "object", "properties": {
65
+ "start": NODE, "forbidden": FORBIDDEN, "horizon": HORIZON},
66
+ "required": ["start", "forbidden", "horizon"],
67
+ "additionalProperties": False},
68
+ output_schema={"type": "object", "properties": {
69
+ "reachable": {"type": "boolean"},
70
+ "shortest_length": {"type": "integer", "minimum": -1, "maximum": 8},
71
+ "counterexample": {"type": "array", "items": NODE, "maxItems": 9},
72
+ "visited": {"type": "integer", "minimum": 1, "maximum": 8}},
73
+ "required": ["reachable", "shortest_length", "counterexample", "visited"],
74
+ "additionalProperties": False},
75
+ outputs={"reachable": "boolean", "shortest_length": "dimensionless",
76
+ "visited": "dimensionless"},
77
+ quantities=("proposition",), exact_unit=True,
78
+ implementation=finite_reachability, implementation_version="finite-graph-bfs-1",
79
+ timeout_seconds=2.0,
80
+ )
81
+
82
+
83
+ def reachability_registry():
84
+ """Operator factory: ``--methods eal.reachability:reachability_registry``."""
85
+ return default_registry().with_method(REACHABILITY_CONTRACT)
@@ -0,0 +1,25 @@
1
+ """Select the stateless built-in strategies; algorithms live in their modules."""
2
+ from __future__ import annotations
3
+
4
+ from .structured import STRATEGY as STRUCTURED
5
+ from .deductive import STRATEGY as DEDUCTIVE
6
+ from .inductive import STRATEGY as INDUCTIVE
7
+ from .abductive import STRATEGY as ABDUCTIVE
8
+ from .causal import STRATEGY as CAUSAL
9
+ from .counterfactual import STRATEGY as COUNTERFACTUAL
10
+ from .analogical import STRATEGY as ANALOGICAL
11
+ from .temporal import STRATEGY as TEMPORAL
12
+
13
+
14
+ BUILTIN_STRATEGIES = {
15
+ strategy.mode: strategy for strategy in (
16
+ STRUCTURED,
17
+ DEDUCTIVE,
18
+ INDUCTIVE,
19
+ ABDUCTIVE,
20
+ CAUSAL,
21
+ COUNTERFACTUAL,
22
+ ANALOGICAL,
23
+ TEMPORAL,
24
+ )
25
+ }
@@ -0,0 +1,45 @@
1
+ """Condition a supplied finite hypothesis distribution on its likelihoods."""
2
+ from __future__ import annotations
3
+
4
+ import math
5
+
6
+ from .strategy import BuiltinStrategy
7
+ from .validation import _list, _name, _object, _probability, _result, _text
8
+
9
+
10
+ def _abductive(value):
11
+ _object(value, ("observed", "candidates"), "hypotheses")
12
+ _text(value["observed"], "observed evidence description")
13
+ candidates = _list(value["candidates"], "candidates", minimum=2, maximum=128)
14
+ priors, log_weights = {}, {}
15
+ for item in candidates:
16
+ _object(item, ("name", "prior", "likelihood"), "hypothesis")
17
+ name = _name(item["name"], "hypothesis name")
18
+ if name in priors:
19
+ raise ValueError("Hypothesis names must be unique")
20
+ prior = _probability(item["prior"], "prior")
21
+ likelihood = _probability(item["likelihood"], "likelihood")
22
+ priors[name] = prior
23
+ log_weights[name] = (math.log(prior) + math.log(likelihood)
24
+ if prior > 0 and likelihood > 0 else -math.inf)
25
+ total_prior = math.fsum(priors.values())
26
+ if not math.isclose(total_prior, 1.0, rel_tol=0.0, abs_tol=1e-9):
27
+ raise ValueError("Prior probabilities must sum to 1 within absolute tolerance 1e-9")
28
+ maximum = max(log_weights.values())
29
+ if maximum == -math.inf:
30
+ return _result(False, "The supplied hypotheses assign zero probability to the observed evidence",
31
+ posterior={}, best=None, ties=[], assumptions_verified=False)
32
+ weights = {name: math.exp(weight - maximum) for name, weight in log_weights.items()}
33
+ normaliser = math.fsum(weights.values())
34
+ posterior = {name: weight / normaliser for name, weight in weights.items()}
35
+ best_probability = max(posterior.values())
36
+ ties = sorted(name for name, probability in posterior.items()
37
+ if math.isclose(probability, best_probability, rel_tol=0.0, abs_tol=1e-12))
38
+ return _result(True, "Posterior probabilities computed within the supplied finite hypothesis model",
39
+ posterior=posterior, best=ties[0] if len(ties) == 1 else None,
40
+ best_posterior=best_probability, ties=ties if len(ties) > 1 else [],
41
+ log_evidence_probability=maximum + math.log(normaliser) - math.log(total_prior),
42
+ assumptions_verified=False)
43
+
44
+
45
+ STRATEGY = BuiltinStrategy("abductive", _abductive)
@@ -0,0 +1,41 @@
1
+ """Compare exactly the declared scalar feature correspondences."""
2
+ from __future__ import annotations
3
+
4
+
5
+ from .strategy import BuiltinStrategy
6
+ from .validation import _list, _name, _object, _result
7
+
8
+
9
+ def _analogical(value):
10
+ _object(value, ("relevant_features", "source", "target"), "analogy")
11
+ features = [_name(feature, "feature") for feature in
12
+ _list(value["relevant_features"], "relevant_features", maximum=256)]
13
+ if len(set(features)) != len(features):
14
+ raise ValueError("Relevant feature names must be unique")
15
+ source, target = value["source"], value["target"]
16
+ if not isinstance(source, dict) or not isinstance(target, dict):
17
+ raise ValueError("Source and target must be feature maps")
18
+ missing = sorted(feature for feature in features if feature not in source or feature not in target)
19
+ if missing:
20
+ return _result(False, "The relevant-feature mapping is incomplete", complete=False,
21
+ missing=missing, match_fraction=None, mismatches=[], assumptions_verified=False)
22
+
23
+ def scalar(item):
24
+ if isinstance(item, (dict, list)):
25
+ raise ValueError("Mapped feature values must be JSON scalars")
26
+ return item
27
+
28
+ def equal(left, right):
29
+ scalar(left)
30
+ scalar(right)
31
+ numeric = lambda x: isinstance(x, (float, int)) and not isinstance(x, bool)
32
+ return (type(left) is type(right) or numeric(left) and numeric(right)) and left == right
33
+
34
+ mismatches = [feature for feature in features if not equal(source[feature], target[feature])]
35
+ return _result(True, "Declared features compared exactly; feature relevance and conclusion transfer remain authored reasoning",
36
+ complete=True, feature_count=len(features), matched=len(features) - len(mismatches),
37
+ match_fraction=(len(features) - len(mismatches)) / len(features), mismatches=mismatches,
38
+ missing=[], interpretation="feature_correspondence", assumptions_verified=False)
39
+
40
+
41
+ STRATEGY = BuiltinStrategy("analogical", _analogical)
@@ -0,0 +1,46 @@
1
+ """Calculate the independent-group mean contrast and standard error."""
2
+ from __future__ import annotations
3
+
4
+ import math
5
+ from fractions import Fraction
6
+ from statistics import stdev
7
+
8
+ from .strategy import BuiltinStrategy
9
+ from .validation import _list, _number, _object, _result
10
+
11
+
12
+ def _causal(value):
13
+ _object(value, ("assignment", "treatment", "control"), "experiment")
14
+ if value["assignment"] != "randomised":
15
+ raise ValueError("This causal calculation requires declared randomised assignment")
16
+ groups = []
17
+ for name in ("treatment", "control"):
18
+ groups.append([_number(item, name) for item in _list(value[name], name, minimum=2)])
19
+ treatment, control = groups
20
+ # Subtract the represented means before rounding: two large, neighbouring
21
+ # integer means must not collapse to the same binary64 value and erase an
22
+ # observed treatment effect. Fraction also retains each supplied float's
23
+ # exact binary value; it does not infer additional measurement precision.
24
+ mt = sum(map(Fraction, treatment), Fraction()) / len(treatment)
25
+ mc = sum(map(Fraction, control), Fraction()) / len(control)
26
+ def numeric(value):
27
+ rounded = value.numerator if value.denominator == 1 else float(value)
28
+ if value and rounded == 0:
29
+ raise ValueError("Computed mean or contrast underflows the representable number range")
30
+ return rounded
31
+
32
+ # Compute standard deviations before combining their contributions. Forming
33
+ # floating variances first can underflow even when the final square root is
34
+ # representable (for example, a standard error of 1e-200).
35
+ se = math.hypot(stdev(treatment) / math.sqrt(len(treatment)),
36
+ stdev(control) / math.sqrt(len(control)))
37
+ if se == 0 and any(any(item != group[0] for item in group) for group in groups):
38
+ raise ValueError("Computed standard error underflows the representable number range")
39
+ return _result(True, "Randomised two-group mean contrast computed; assignment and causal assumptions require evidence",
40
+ estimate=numeric(mt - mc), standard_error=se,
41
+ treatment_mean=numeric(mt), control_mean=numeric(mc),
42
+ treatment_size=len(treatment), control_size=len(control),
43
+ sample_size=len(treatment) + len(control), assumptions_verified=False)
44
+
45
+
46
+ STRATEGY = BuiltinStrategy("causal", _causal)
@@ -0,0 +1,64 @@
1
+ """Evaluate a single intervention in an acyclic affine causal model."""
2
+ from __future__ import annotations
3
+
4
+ import math
5
+
6
+ from .strategy import BuiltinStrategy
7
+ from .validation import _name, _number, _object, _result
8
+
9
+
10
+ def _counterfactual(value):
11
+ _object(value, ("variables", "intervention", "outcome"), "causal_model")
12
+ variables = value["variables"]
13
+ if not isinstance(variables, dict) or not 1 <= len(variables) <= 32:
14
+ raise ValueError("variables must contain 1..32 structural equations")
15
+ equations = {}
16
+ for name, equation in variables.items():
17
+ _name(name, "variable name")
18
+ _object(equation, ("intercept", "coefficients", "noise"), "structural equation")
19
+ coefficients = equation["coefficients"]
20
+ if not isinstance(coefficients, dict) or len(coefficients) > 32:
21
+ raise ValueError("coefficients must map at most 32 variables to numbers")
22
+ if any(parent not in variables for parent in coefficients):
23
+ raise ValueError("Structural equation references an unknown parent variable")
24
+ equations[name] = (_number(equation["intercept"], "intercept"),
25
+ {parent: _number(c, "coefficient") for parent, c in coefficients.items()},
26
+ _number(equation["noise"], "noise"))
27
+ _object(value["intervention"], ("variable", "value"), "intervention")
28
+ changed = _name(value["intervention"]["variable"], "intervention variable")
29
+ outcome = _name(value["outcome"], "outcome")
30
+ if changed not in equations or outcome not in equations:
31
+ raise ValueError("Intervention and outcome must name declared variables")
32
+ setting = _number(value["intervention"]["value"], "intervention value")
33
+ order = []
34
+ remaining = dict(equations)
35
+ while remaining:
36
+ ready = sorted(name for name, (_, parents, _) in remaining.items()
37
+ if all(parent in order for parent in parents))
38
+ if not ready:
39
+ raise ValueError("Structural equations must be acyclic, including zero-coefficient references")
40
+ for name in ready:
41
+ order.append(name)
42
+ del remaining[name]
43
+
44
+ def solve(intervene):
45
+ result = {}
46
+ for name in order:
47
+ intercept, parents, noise = equations[name]
48
+ if intervene and name == changed:
49
+ result[name] = setting
50
+ else:
51
+ terms = [intercept, noise, *(c * result[parent] for parent, c in parents.items())]
52
+ result[name] = sum(terms) if all(type(term) is int for term in terms) else math.fsum(terms)
53
+ _number(result[name], "Computed structural value")
54
+ return result
55
+
56
+ factual, counterfactual = solve(False), solve(True)
57
+ return _result(True, "Intervention evaluated in an acyclic affine model with the same supplied exogenous values",
58
+ factual=factual[outcome], counterfactual=counterfactual[outcome],
59
+ difference=counterfactual[outcome] - factual[outcome],
60
+ factual_values=factual, counterfactual_values=counterfactual,
61
+ outcome=outcome, evaluation_order=order, assumptions_verified=False)
62
+
63
+
64
+ STRATEGY = BuiltinStrategy("counterfactual", _counterfactual)
@@ -0,0 +1,72 @@
1
+ """Check bounded propositional entailment and retain counterexamples."""
2
+ from __future__ import annotations
3
+
4
+ import itertools
5
+
6
+ MAX_ATOMS = 12
7
+ MAX_FORMULA_NODES = 512
8
+
9
+ from .strategy import BuiltinStrategy
10
+ from .validation import _list, _name, _object, _result
11
+
12
+
13
+ def _deductive(value):
14
+ _object(value, ("premises", "conclusion"), "logical_case")
15
+ premises = _list(value["premises"], "Logical premises", minimum=0, maximum=128)
16
+ atoms, budget = set(), [MAX_FORMULA_NODES]
17
+
18
+ def check(formula, depth=0):
19
+ budget[0] -= 1
20
+ if budget[0] < 0 or depth > 32:
21
+ raise ValueError("Logical formulas exceed 512 total nodes or depth 32")
22
+ if isinstance(formula, str):
23
+ atoms.add(_name(formula, "Propositional atom"))
24
+ return
25
+ if not isinstance(formula, dict) or len(formula) != 1:
26
+ raise ValueError("A formula is an atom or one not/and/or/implies object")
27
+ op, operand = next(iter(formula.items()))
28
+ if op == "not":
29
+ check(operand, depth + 1)
30
+ elif op in ("and", "or", "implies"):
31
+ _list(operand, f"{op} operands", minimum=2, maximum=2 if op == "implies" else 128)
32
+ for child in operand:
33
+ check(child, depth + 1)
34
+ else:
35
+ raise ValueError(f"Unsupported logical operator {op!r}")
36
+
37
+ for formula in [*premises, value["conclusion"]]:
38
+ check(formula)
39
+ if len(atoms) > MAX_ATOMS:
40
+ raise ValueError("Finite deduction supports at most 12 distinct atoms")
41
+
42
+ def truth(formula, assignment):
43
+ if isinstance(formula, str):
44
+ return assignment[formula]
45
+ op, operand = next(iter(formula.items()))
46
+ if op == "not":
47
+ return not truth(operand, assignment)
48
+ if op == "and":
49
+ return all(truth(item, assignment) for item in operand)
50
+ if op == "or":
51
+ return any(truth(item, assignment) for item in operand)
52
+ return not truth(operand[0], assignment) or truth(operand[1], assignment)
53
+
54
+ atoms = sorted(atoms)
55
+ satisfying = 0
56
+ counterexample = None
57
+ for values in itertools.product((False, True), repeat=len(atoms)):
58
+ assignment = dict(zip(atoms, values))
59
+ if all(truth(formula, assignment) for formula in premises):
60
+ satisfying += 1
61
+ if counterexample is None and not truth(value["conclusion"], assignment):
62
+ counterexample = assignment
63
+ consistent = satisfying > 0
64
+ entailed = counterexample is None
65
+ return _result(consistent,
66
+ "Finite entailment established with satisfiable premises" if consistent and entailed
67
+ else "Premises are inconsistent" if not consistent else "A counterexample refutes entailment",
68
+ entailed=entailed, consistent_premises=consistent, counterexample=counterexample,
69
+ atoms=atoms, valuations=2 ** len(atoms), satisfying_premise_valuations=satisfying)
70
+
71
+
72
+ STRATEGY = BuiltinStrategy("deductive", _deductive)
@@ -0,0 +1,29 @@
1
+ """Estimate a binomial proportion using the Wilson score interval."""
2
+ from __future__ import annotations
3
+
4
+ import math
5
+ from statistics import NormalDist
6
+
7
+ from .strategy import BuiltinStrategy
8
+ from .validation import _integer, _number, _object, _result
9
+
10
+
11
+ def _inductive(value):
12
+ _object(value, ("successes", "trials", "confidence"), "sample")
13
+ n = _integer(value["trials"], "trials", minimum=1)
14
+ k = _integer(value["successes"], "successes", maximum=n)
15
+ confidence = _number(value["confidence"], "confidence")
16
+ if not 0.001 <= confidence <= 0.999999:
17
+ raise ValueError("confidence must be in [0.001, 0.999999]")
18
+ z = NormalDist().inv_cdf((1 + confidence) / 2)
19
+ p = k / n
20
+ denominator = 1 + z * z / n
21
+ centre = (p + z * z / (2 * n)) / denominator
22
+ half = z * math.sqrt(p * (1 - p) / n + z * z / (4 * n * n)) / denominator
23
+ return _result(True, "Wilson interval computed under the stated binomial sampling model",
24
+ estimate=p, lower=max(0.0, centre - half), upper=min(1.0, centre + half),
25
+ sample_size=n, successes=k, confidence=confidence, method="wilson_score",
26
+ interpretation="approximate_frequentist", assumptions_verified=False)
27
+
28
+
29
+ STRATEGY = BuiltinStrategy("inductive", _inductive)