tensorcode 0.1.0a1__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.
- tensorcode/__init__.py +84 -0
- tensorcode/actions.py +137 -0
- tensorcode/answer_type.py +222 -0
- tensorcode/awareness.py +344 -0
- tensorcode/backends/__init__.py +0 -0
- tensorcode/backends/builtin.py +167 -0
- tensorcode/backends/hf_local.py +89 -0
- tensorcode/backends/linear.py +133 -0
- tensorcode/backends/neural.py +361 -0
- tensorcode/causal.py +262 -0
- tensorcode/change.py +566 -0
- tensorcode/chunking.py +195 -0
- tensorcode/cognition.py +311 -0
- tensorcode/context.py +97 -0
- tensorcode/control.py +291 -0
- tensorcode/cues.py +192 -0
- tensorcode/expectation.py +270 -0
- tensorcode/frames.py +232 -0
- tensorcode/language/__init__.py +36 -0
- tensorcode/language/chart.py +558 -0
- tensorcode/language/discourse.py +132 -0
- tensorcode/language/domains/__init__.py +0 -0
- tensorcode/language/domains/desktop.py +552 -0
- tensorcode/language/english.py +459 -0
- tensorcode/language/features.py +112 -0
- tensorcode/language/generate.py +574 -0
- tensorcode/language/grammar.py +893 -0
- tensorcode/language/semantics.py +349 -0
- tensorcode/learning/__init__.py +30 -0
- tensorcode/learning/certificate.py +148 -0
- tensorcode/learning/induce.py +304 -0
- tensorcode/learning/library.py +217 -0
- tensorcode/learning/literals.py +126 -0
- tensorcode/learning/verify.py +253 -0
- tensorcode/memory.py +303 -0
- tensorcode/metacognition.py +351 -0
- tensorcode/ops.py +207 -0
- tensorcode/outcomes.py +99 -0
- tensorcode/permanence.py +376 -0
- tensorcode/priming.py +191 -0
- tensorcode/py.typed +0 -0
- tensorcode/quantity.py +311 -0
- tensorcode/records.py +728 -0
- tensorcode/relation.py +771 -0
- tensorcode/runtime.py +471 -0
- tensorcode/semantics_bridge.py +308 -0
- tensorcode/social.py +380 -0
- tensorcode/temporal.py +189 -0
- tensorcode/wants.py +185 -0
- tensorcode-0.1.0a1.dist-info/METADATA +196 -0
- tensorcode-0.1.0a1.dist-info/RECORD +53 -0
- tensorcode-0.1.0a1.dist-info/WHEEL +4 -0
- tensorcode-0.1.0a1.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
"""Expectations, and the prediction errors that follow from them.
|
|
2
|
+
|
|
3
|
+
A modality feature on a parsed frame ("an announcement *should* appear") is inert: it
|
|
4
|
+
records that someone said "should" and predicts nothing. An expectation here is a
|
|
5
|
+
commitment — a cue, what should then hold, and how sure — which means it can be *wrong*,
|
|
6
|
+
and being wrong is the useful part:
|
|
7
|
+
|
|
8
|
+
* :func:`check` compares what was expected against what was observed and, on a mismatch,
|
|
9
|
+
writes a :class:`Violation` claim naming both sides and their provenance;
|
|
10
|
+
* a violation is a claim like any other, so attention (``awareness``) can be seeded from
|
|
11
|
+
it and rules can fire on it — surprise becomes a first-class input rather than a log line;
|
|
12
|
+
* :class:`Predictor` learns the probability from its own record of hits and misses, so the
|
|
13
|
+
number on an expectation is a frequency with a named basis rather than a hand-set prior.
|
|
14
|
+
|
|
15
|
+
Nothing here guesses: an expectation with too little evidence reports
|
|
16
|
+
:class:`~tensorcode.outcomes.Unknown` rather than a made-up probability.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import hashlib
|
|
22
|
+
import json
|
|
23
|
+
from math import exp, log, log2
|
|
24
|
+
from collections import Counter, defaultdict
|
|
25
|
+
from dataclasses import dataclass, field
|
|
26
|
+
from datetime import datetime, timezone
|
|
27
|
+
from typing import Any, Iterable, Mapping, Sequence
|
|
28
|
+
|
|
29
|
+
from .outcomes import Score, Unknown, Verdict
|
|
30
|
+
from .records import Claim, Evidence, Ref, Store
|
|
31
|
+
|
|
32
|
+
MIN_TRIALS = 3 # below this, a frequency is not a probability worth reporting
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _digest(payload: Any) -> str:
|
|
36
|
+
return hashlib.sha256(json.dumps(payload, sort_keys=True, default=str).encode()).hexdigest()[:12]
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@dataclass(frozen=True)
|
|
40
|
+
class Expectation:
|
|
41
|
+
"""If ``cue`` happens, ``predicted`` should hold afterwards, with probability ``p``."""
|
|
42
|
+
|
|
43
|
+
cue: str # a description of the triggering act or event, e.g. "click:Send message"
|
|
44
|
+
predicted: tuple[tuple[str, Any], ...] # (aspect, value) pairs that should be observed
|
|
45
|
+
p: Score | None = None
|
|
46
|
+
scope: Ref | None = None
|
|
47
|
+
source: str = "stated" # "stated" (someone said so) | "learned" (from its own record)
|
|
48
|
+
|
|
49
|
+
@property
|
|
50
|
+
def ref(self) -> Ref:
|
|
51
|
+
return Ref(f"expectation:{_digest([self.cue, list(self.predicted), self.source])}")
|
|
52
|
+
|
|
53
|
+
def describe(self) -> str:
|
|
54
|
+
body = ", ".join(f"{a}={v!r}" for a, v in self.predicted)
|
|
55
|
+
odds = f" p={self.p.value:.2f}" if self.p else ""
|
|
56
|
+
return f"after {self.cue}: {body}{odds}"
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@dataclass(frozen=True)
|
|
60
|
+
class Violation:
|
|
61
|
+
"""A prediction error: what was expected, what happened, and how surprising it was."""
|
|
62
|
+
|
|
63
|
+
expectation: Expectation
|
|
64
|
+
aspect: str
|
|
65
|
+
expected: Any
|
|
66
|
+
observed: Any
|
|
67
|
+
surprise: float # -log2(p assigned to what actually happened), 0 when unsurprising
|
|
68
|
+
|
|
69
|
+
@property
|
|
70
|
+
def ref(self) -> Ref:
|
|
71
|
+
return Ref(f"violation:{_digest([self.expectation.ref.id, self.aspect, str(self.expected), str(self.observed)])}")
|
|
72
|
+
|
|
73
|
+
def describe(self) -> str:
|
|
74
|
+
return f"{self.aspect}: expected {self.expected!r}, observed {self.observed!r} (surprise {self.surprise:.2f} bits)"
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def expect(mind: Store, expectation: Expectation, *, source: Ref, observed_at: datetime | None = None,
|
|
78
|
+
method: str = "expectation") -> Ref:
|
|
79
|
+
"""Record an expectation as claims, so it can be inspected, cited and retracted."""
|
|
80
|
+
at = observed_at or datetime.now(timezone.utc)
|
|
81
|
+
ref = expectation.ref
|
|
82
|
+
evidence = Evidence(source=source, observed_at=at, method=method, confidence=expectation.p)
|
|
83
|
+
mind.tell(Claim(ref, "is_a", "expectation", scope=expectation.scope), evidence)
|
|
84
|
+
mind.tell(Claim(ref, "cue", expectation.cue, scope=expectation.scope), evidence)
|
|
85
|
+
mind.tell(Claim(ref, "held_because", expectation.source, scope=expectation.scope), evidence)
|
|
86
|
+
for aspect, value in expectation.predicted:
|
|
87
|
+
mind.tell(Claim(ref, f"predicts:{aspect}", value, scope=expectation.scope), evidence)
|
|
88
|
+
return ref
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def check(mind: Store, expectation: Expectation, observed: Mapping[str, Any], *, source: Ref,
|
|
92
|
+
observed_at: datetime | None = None) -> tuple[Verdict, tuple[Violation, ...]]:
|
|
93
|
+
"""Compare an expectation against what was observed; record any prediction error.
|
|
94
|
+
|
|
95
|
+
Returns a verdict and the violations. An aspect the observation does not mention is
|
|
96
|
+
``unknown``, not a violation: unseen is not disconfirmed.
|
|
97
|
+
"""
|
|
98
|
+
at = observed_at or datetime.now(timezone.utc)
|
|
99
|
+
violations: list[Violation] = []
|
|
100
|
+
unseen: list[str] = []
|
|
101
|
+
for aspect, value in expectation.predicted:
|
|
102
|
+
if aspect not in observed:
|
|
103
|
+
unseen.append(aspect)
|
|
104
|
+
continue
|
|
105
|
+
got = observed[aspect]
|
|
106
|
+
if got != value:
|
|
107
|
+
p = expectation.p.value if expectation.p else 0.5
|
|
108
|
+
surprise = -log2(max(1e-6, 1.0 - p))
|
|
109
|
+
violations.append(Violation(expectation, aspect, value, got, surprise))
|
|
110
|
+
for violation in violations:
|
|
111
|
+
mind.tell(
|
|
112
|
+
Claim(violation.ref, "is_a", "violation"),
|
|
113
|
+
Evidence(source=source, observed_at=at, method="prediction-error", derived_from=()),
|
|
114
|
+
)
|
|
115
|
+
for predicate, value in (("of", expectation.ref), ("aspect", violation.aspect),
|
|
116
|
+
("expected", violation.expected), ("observed", violation.observed),
|
|
117
|
+
("surprise", violation.surprise)):
|
|
118
|
+
mind.tell(Claim(violation.ref, predicate, value), Evidence(source=source, observed_at=at, method="prediction-error"))
|
|
119
|
+
if violations:
|
|
120
|
+
return Verdict("fails", tuple(v.describe() for v in violations), (expectation.ref,)), tuple(violations)
|
|
121
|
+
if unseen and len(unseen) == len(expectation.predicted):
|
|
122
|
+
return Verdict("unknown", (f"nothing observed about {', '.join(unseen)}",), (expectation.ref,)), ()
|
|
123
|
+
return Verdict("holds", (expectation.describe(),), (expectation.ref,)), ()
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
# ------------------------------------------------------------------ learning
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
@dataclass
|
|
130
|
+
class Predictor:
|
|
131
|
+
"""Learns what follows a cue from its own hits and misses.
|
|
132
|
+
|
|
133
|
+
The probability it reports is a smoothed frequency over trials it actually saw, with
|
|
134
|
+
the basis naming the record it came from. Below :data:`MIN_TRIALS` it reports
|
|
135
|
+
``Unknown`` instead of a number, because three coin flips are not a calibration.
|
|
136
|
+
"""
|
|
137
|
+
|
|
138
|
+
name: str = "expectation/observed"
|
|
139
|
+
counts: dict[tuple[str, str], Counter] = field(default_factory=lambda: defaultdict(Counter))
|
|
140
|
+
trials: dict[tuple[str, str], int] = field(default_factory=lambda: defaultdict(int))
|
|
141
|
+
|
|
142
|
+
def observe(self, cue: str, aspect: str, value: Any) -> None:
|
|
143
|
+
key = (cue, aspect)
|
|
144
|
+
self.counts[key][_hashable(value)] += 1
|
|
145
|
+
self.trials[key] += 1
|
|
146
|
+
|
|
147
|
+
def predict(self, cue: str, aspect: str) -> tuple[Any, Score] | Unknown:
|
|
148
|
+
"""The most frequent outcome for this cue and aspect, with its frequency."""
|
|
149
|
+
key = (cue, aspect)
|
|
150
|
+
trials = self.trials.get(key, 0)
|
|
151
|
+
if trials < MIN_TRIALS:
|
|
152
|
+
return Unknown("too_few_trials", f"{trials} observations of {aspect} after {cue}")
|
|
153
|
+
value, hits = self.counts[key].most_common(1)[0]
|
|
154
|
+
# Laplace over the outcomes seen *and one that has not been*: with a single observed
|
|
155
|
+
# outcome the seen-only denominator cancels and twenty hits would report certainty,
|
|
156
|
+
# which is the failure this smoothing exists to prevent.
|
|
157
|
+
outcomes = max(2, len(self.counts[key]))
|
|
158
|
+
p = (hits + 1) / (trials + outcomes)
|
|
159
|
+
return value, Score(p, "probability", basis=f"{self.name}:{cue}/{aspect}@{trials}")
|
|
160
|
+
|
|
161
|
+
def expectation(self, cue: str, aspects: Iterable[str]) -> Expectation | Unknown:
|
|
162
|
+
"""An expectation over several aspects, at the weakest of their probabilities."""
|
|
163
|
+
predicted: list[tuple[str, Any]] = []
|
|
164
|
+
weakest: Score | None = None
|
|
165
|
+
for aspect in aspects:
|
|
166
|
+
got = self.predict(cue, aspect)
|
|
167
|
+
if isinstance(got, Unknown):
|
|
168
|
+
return got
|
|
169
|
+
value, score = got
|
|
170
|
+
predicted.append((aspect, value))
|
|
171
|
+
if weakest is None or score.value < weakest.value:
|
|
172
|
+
weakest = score
|
|
173
|
+
if not predicted:
|
|
174
|
+
return Unknown("nothing_to_predict", cue)
|
|
175
|
+
return Expectation(cue, tuple(predicted), weakest, source="learned")
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def _hashable(value: Any) -> Any:
|
|
179
|
+
if isinstance(value, (list, tuple)):
|
|
180
|
+
return tuple(_hashable(v) for v in value)
|
|
181
|
+
if isinstance(value, dict):
|
|
182
|
+
return tuple(sorted((k, _hashable(v)) for k, v in value.items()))
|
|
183
|
+
if isinstance(value, set):
|
|
184
|
+
return tuple(sorted(_hashable(v) for v in value))
|
|
185
|
+
return value
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
# -------------------------------------------------------------- probability
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def combine(scores: Sequence[Score], *, assume: str = "independent") -> Score | Unknown:
|
|
192
|
+
"""Pool several probabilities about one proposition, naming the assumption used.
|
|
193
|
+
|
|
194
|
+
Independent evidence pools in log-odds. That assumption is usually false — two sources
|
|
195
|
+
reading the same notice are one source — so it is written into the basis rather than
|
|
196
|
+
left implicit, and a caller that cannot justify it should not use the number.
|
|
197
|
+
|
|
198
|
+
Refuses when any input is not a calibrated probability: a similarity and a vote share do
|
|
199
|
+
not pool into a probability, and pretending they do is how a confident wrong number gets
|
|
200
|
+
made.
|
|
201
|
+
"""
|
|
202
|
+
scores = list(scores)
|
|
203
|
+
if not scores:
|
|
204
|
+
return Unknown("no_evidence", "nothing to combine")
|
|
205
|
+
wrong = [s.kind for s in scores if s.kind != "probability"]
|
|
206
|
+
if wrong:
|
|
207
|
+
return Unknown("not_probabilities", f"cannot pool {', '.join(sorted(set(wrong)))} into a probability")
|
|
208
|
+
if assume != "independent":
|
|
209
|
+
return Unknown("unsupported_assumption", f"only independent pooling is implemented, not {assume!r}")
|
|
210
|
+
total = 0.0
|
|
211
|
+
for score in scores:
|
|
212
|
+
p = min(max(score.value, 1e-6), 1 - 1e-6)
|
|
213
|
+
total += log(p / (1 - p))
|
|
214
|
+
pooled = 1 / (1 + exp(-total))
|
|
215
|
+
bases = "+".join(sorted({s.basis for s in scores if s.basis}))
|
|
216
|
+
return Score(pooled, "probability", basis=f"pooled(independent,n={len(scores)}):{bases}"[:200])
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def disagreement(scores: Sequence[Score]) -> float:
|
|
220
|
+
"""How far apart the evidence is, as the spread of its probabilities.
|
|
221
|
+
|
|
222
|
+
Pooling hides this: two sources at 0.1 and 0.9 pool to 0.5, and so do two at 0.5. A
|
|
223
|
+
caller that needs to know whether its belief is settled or contested needs the spread as
|
|
224
|
+
well as the pooled number.
|
|
225
|
+
"""
|
|
226
|
+
values = [s.value for s in scores]
|
|
227
|
+
return (max(values) - min(values)) if values else 0.0
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
@dataclass(frozen=True)
|
|
231
|
+
class Calibration:
|
|
232
|
+
"""How well stated probabilities matched what happened."""
|
|
233
|
+
|
|
234
|
+
bins: tuple[tuple[float, float, int], ...] # (stated, observed, n) per occupied bin
|
|
235
|
+
ece: float # expected calibration error: average gap, weighted by how often each bin was used
|
|
236
|
+
n: int
|
|
237
|
+
|
|
238
|
+
def describe(self) -> str:
|
|
239
|
+
rows = ", ".join(f"{stated:.2f}→{observed:.2f}({count})" for stated, observed, count in self.bins)
|
|
240
|
+
return f"ECE {self.ece:.3f} over {self.n}: {rows}"
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def calibration(pairs: Sequence[tuple[float, bool]], *, bins: int = 10) -> Calibration | Unknown:
|
|
244
|
+
"""Bin stated probabilities against observed frequencies."""
|
|
245
|
+
if not pairs:
|
|
246
|
+
return Unknown("no_predictions", "nothing to calibrate")
|
|
247
|
+
buckets: dict[int, list[tuple[float, bool]]] = defaultdict(list)
|
|
248
|
+
for p, hit in pairs:
|
|
249
|
+
buckets[min(bins - 1, int(p * bins))].append((p, hit))
|
|
250
|
+
rows: list[tuple[float, float, int]] = []
|
|
251
|
+
error = 0.0
|
|
252
|
+
for index in sorted(buckets):
|
|
253
|
+
group = buckets[index]
|
|
254
|
+
stated = sum(p for p, _ in group) / len(group)
|
|
255
|
+
observed = sum(1 for _, hit in group if hit) / len(group)
|
|
256
|
+
rows.append((round(stated, 4), round(observed, 4), len(group)))
|
|
257
|
+
error += len(group) * abs(stated - observed)
|
|
258
|
+
return Calibration(tuple(rows), round(error / len(pairs), 4), len(pairs))
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def surprises(mind: Store, *, since: datetime | None = None, least: float = 0.0) -> list[Claim]:
|
|
262
|
+
"""Violations on record, most surprising first — what attention should be drawn to."""
|
|
263
|
+
out: list[tuple[float, Claim]] = []
|
|
264
|
+
for record in mind.claims(predicate="surprise"):
|
|
265
|
+
if since is not None and all(e.observed_at < since for e in record.evidence):
|
|
266
|
+
continue
|
|
267
|
+
value = record.claim.object
|
|
268
|
+
if isinstance(value, (int, float)) and value >= least:
|
|
269
|
+
out.append((float(value), record.claim))
|
|
270
|
+
return [claim for _, claim in sorted(out, key=lambda pair: -pair[0])]
|
tensorcode/frames.py
ADDED
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
"""Three orientations over the same claims: what it is, where it is, how it was known.
|
|
2
|
+
|
|
3
|
+
The store indexes claims by subject, predicate and scope — all *semantic*. That is not
|
|
4
|
+
enough for a mind that has a body: "what is next to the pointer" and "what did I see with
|
|
5
|
+
my eyes rather than read in a tree" are ordinary questions, and answering them by scanning
|
|
6
|
+
everything is what makes them feel impossible.
|
|
7
|
+
|
|
8
|
+
semantic (subject, predicate) what it is about
|
|
9
|
+
spatial (window, region, box) where it is, relative to the screen and the pointer
|
|
10
|
+
modality (pixels, structure, text, hearsay) how it came to be known
|
|
11
|
+
|
|
12
|
+
The same claim sits in all three. That is the point: awareness spreading in one frame pulls
|
|
13
|
+
in neighbours from the others (``Frames.links`` feeds ``awareness.Awareness``), which is
|
|
14
|
+
what makes nucleation cross modalities. Where two modalities disagree about the same thing,
|
|
15
|
+
``disagreements`` reports it rather than letting the stronger index win silently.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import re
|
|
21
|
+
from dataclasses import dataclass
|
|
22
|
+
from typing import Any, Callable, Iterable
|
|
23
|
+
|
|
24
|
+
from .records import ClaimRecord, Ref, Store
|
|
25
|
+
|
|
26
|
+
#: how a claim came to be known, from its evidence's method and source
|
|
27
|
+
MODALITIES = ("pixels", "structure", "text", "hearsay", "inference", "other")
|
|
28
|
+
|
|
29
|
+
_MODALITY_PATTERNS = (
|
|
30
|
+
("pixels", re.compile(r"ocr|pixel|vision|screenshot|segment", re.I)),
|
|
31
|
+
("structure", re.compile(r"dom|scene|atspi|accessib|ax\b|uia|tree", re.I)),
|
|
32
|
+
("hearsay", re.compile(r"hearsay|heard|told|said|rumou?r", re.I)),
|
|
33
|
+
("inference", re.compile(r"derive|rule|infer", re.I)),
|
|
34
|
+
("text", re.compile(r"parse|text|read|grammar|utterance|note", re.I)),
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def modality_of(rec: ClaimRecord) -> tuple[str, ...]:
|
|
39
|
+
"""Which modalities this claim rests on, in a fixed order (a claim may have several)."""
|
|
40
|
+
found: set[str] = set()
|
|
41
|
+
for e in rec.evidence:
|
|
42
|
+
blob = f"{e.method or ''} {e.source.id}"
|
|
43
|
+
for name, pattern in _MODALITY_PATTERNS:
|
|
44
|
+
if pattern.search(blob):
|
|
45
|
+
found.add(name)
|
|
46
|
+
break
|
|
47
|
+
else:
|
|
48
|
+
found.add("other")
|
|
49
|
+
return tuple(m for m in MODALITIES if m in found)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@dataclass(frozen=True)
|
|
53
|
+
class Place:
|
|
54
|
+
"""Where something is, in screen terms. ``None`` fields are simply unknown."""
|
|
55
|
+
|
|
56
|
+
window: str | None = None
|
|
57
|
+
region: str | None = None
|
|
58
|
+
box: tuple[int, int, int, int] | None = None # x, y, w, h
|
|
59
|
+
|
|
60
|
+
@property
|
|
61
|
+
def center(self) -> tuple[float, float] | None:
|
|
62
|
+
if self.box is None:
|
|
63
|
+
return None
|
|
64
|
+
x, y, w, h = self.box
|
|
65
|
+
return (x + w / 2, y + h / 2)
|
|
66
|
+
|
|
67
|
+
def distance(self, other: Place | tuple[float, float]) -> float | None:
|
|
68
|
+
"""Pixels between centres, or ``None`` when either side has no box."""
|
|
69
|
+
here = self.center
|
|
70
|
+
there = other if isinstance(other, tuple) else other.center
|
|
71
|
+
if here is None or there is None:
|
|
72
|
+
return None
|
|
73
|
+
return ((here[0] - there[0]) ** 2 + (here[1] - there[1]) ** 2) ** 0.5
|
|
74
|
+
|
|
75
|
+
def near(self, other: Place | tuple[float, float], slack: float = 120.0) -> bool:
|
|
76
|
+
d = self.distance(other)
|
|
77
|
+
return d is not None and d <= slack
|
|
78
|
+
|
|
79
|
+
@property
|
|
80
|
+
def known(self) -> bool:
|
|
81
|
+
return self.window is not None or self.region is not None or self.box is not None
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def place_of(rec: ClaimRecord, mind: Store) -> Place:
|
|
85
|
+
"""Read a place off the claim's subject: its entity payload's geometry, or ``in``/``at`` claims."""
|
|
86
|
+
window = region = box = None
|
|
87
|
+
payload = mind.entities.get(rec.claim.subject)
|
|
88
|
+
if payload is not None:
|
|
89
|
+
raw = getattr(payload, "box", None)
|
|
90
|
+
if isinstance(raw, (list, tuple)) and len(raw) == 4 and all(isinstance(v, (int, float)) for v in raw):
|
|
91
|
+
box = tuple(int(v) for v in raw) # type: ignore[assignment]
|
|
92
|
+
section = getattr(payload, "section", None)
|
|
93
|
+
if isinstance(section, str) and section:
|
|
94
|
+
window = section
|
|
95
|
+
if rec.claim.predicate in ("in", "in_table") and isinstance(rec.claim.object, str):
|
|
96
|
+
region = rec.claim.object
|
|
97
|
+
if rec.claim.predicate == "at" and isinstance(rec.claim.object, (list, tuple)) and len(rec.claim.object) == 4:
|
|
98
|
+
box = tuple(int(v) for v in rec.claim.object) # type: ignore[assignment]
|
|
99
|
+
return Place(window, region, box)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
@dataclass(frozen=True)
|
|
103
|
+
class Binding:
|
|
104
|
+
"""One claim, seen from all three frames at once."""
|
|
105
|
+
|
|
106
|
+
claim_id: str
|
|
107
|
+
subject: Ref
|
|
108
|
+
predicate: str
|
|
109
|
+
place: Place
|
|
110
|
+
modality: tuple[str, ...]
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
@dataclass(frozen=True)
|
|
114
|
+
class Disagreement:
|
|
115
|
+
"""Two live claims about the same thing, from different modalities, with different objects."""
|
|
116
|
+
|
|
117
|
+
subject: Ref
|
|
118
|
+
predicate: str
|
|
119
|
+
readings: tuple[tuple[str, Any, tuple[str, ...]], ...] # (claim id, object, modalities)
|
|
120
|
+
|
|
121
|
+
def describe(self) -> str:
|
|
122
|
+
parts = ", ".join(f"{obj!r} via {'+'.join(mods) or 'unknown'}" for _, obj, mods in self.readings)
|
|
123
|
+
return f"{self.subject} {self.predicate}: {parts}"
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
class Frames:
|
|
127
|
+
"""Three indexes over one mind, rebuilt when the store's revision moves."""
|
|
128
|
+
|
|
129
|
+
def __init__(self, mind: Store, *, modality: Callable[[ClaimRecord], tuple[str, ...]] = modality_of,
|
|
130
|
+
place: Callable[[ClaimRecord, Store], Place] = place_of) -> None:
|
|
131
|
+
self.mind = mind
|
|
132
|
+
self._modality_of, self._place_of = modality, place
|
|
133
|
+
self._bindings: dict[str, Binding] = {}
|
|
134
|
+
self._by_modality: dict[str, set[str]] = {}
|
|
135
|
+
self._by_window: dict[str, set[str]] = {}
|
|
136
|
+
self._by_region: dict[str, set[str]] = {}
|
|
137
|
+
self._placed: list[tuple[str, Place]] = []
|
|
138
|
+
self._grid: dict[tuple[int, int], list[str]] = {} # coarse cells, so adjacency is a lookup
|
|
139
|
+
self._cell = 128
|
|
140
|
+
self._at = -1
|
|
141
|
+
|
|
142
|
+
def index(self) -> None:
|
|
143
|
+
if self._at == self.mind.revision:
|
|
144
|
+
return
|
|
145
|
+
self._bindings, self._by_modality, self._by_window, self._by_region, self._placed = {}, {}, {}, {}, []
|
|
146
|
+
self._grid = {}
|
|
147
|
+
for cid, rec in sorted(self.mind._claims.items()):
|
|
148
|
+
if rec.retracted:
|
|
149
|
+
continue
|
|
150
|
+
mods = self._modality_of(rec)
|
|
151
|
+
where = self._place_of(rec, self.mind)
|
|
152
|
+
self._bindings[cid] = Binding(cid, rec.claim.subject, rec.claim.predicate, where, mods)
|
|
153
|
+
for m in mods:
|
|
154
|
+
self._by_modality.setdefault(m, set()).add(cid)
|
|
155
|
+
if where.window:
|
|
156
|
+
self._by_window.setdefault(where.window, set()).add(cid)
|
|
157
|
+
if where.region:
|
|
158
|
+
self._by_region.setdefault(where.region, set()).add(cid)
|
|
159
|
+
if where.box is not None:
|
|
160
|
+
self._placed.append((cid, where))
|
|
161
|
+
cx, cy = where.center # type: ignore[misc]
|
|
162
|
+
self._grid.setdefault((int(cx // self._cell), int(cy // self._cell)), []).append(cid)
|
|
163
|
+
self._at = self.mind.revision
|
|
164
|
+
|
|
165
|
+
# -- the three frames
|
|
166
|
+
|
|
167
|
+
def binding(self, claim: Any) -> Binding | None:
|
|
168
|
+
self.index()
|
|
169
|
+
return self._bindings.get(claim if isinstance(claim, str) else claim.id)
|
|
170
|
+
|
|
171
|
+
def semantic(self, subject: Ref | None = None, predicate: str | None = None) -> list[ClaimRecord]:
|
|
172
|
+
return self.mind.claims(subject=subject, predicate=predicate)
|
|
173
|
+
|
|
174
|
+
def spatial(self, *, window: str | None = None, region: str | None = None,
|
|
175
|
+
near: Place | tuple[float, float] | None = None, slack: float = 120.0) -> list[ClaimRecord]:
|
|
176
|
+
"""Claims located in a window or region, or within ``slack`` pixels of a point."""
|
|
177
|
+
self.index()
|
|
178
|
+
ids: set[str] | None = None
|
|
179
|
+
if window is not None:
|
|
180
|
+
ids = set(self._by_window.get(window, ()))
|
|
181
|
+
if region is not None:
|
|
182
|
+
ids = set(self._by_region.get(region, ())) if ids is None else ids & self._by_region.get(region, set())
|
|
183
|
+
if near is not None:
|
|
184
|
+
close = {cid for cid, place in self._placed if place.near(near, slack)}
|
|
185
|
+
ids = close if ids is None else ids & close
|
|
186
|
+
chosen = sorted(ids if ids is not None else self._bindings)
|
|
187
|
+
return [self.mind._claims[cid] for cid in chosen if cid in self.mind._claims and not self.mind._claims[cid].retracted]
|
|
188
|
+
|
|
189
|
+
def modality(self, kind: str) -> list[ClaimRecord]:
|
|
190
|
+
self.index()
|
|
191
|
+
return [self.mind._claims[cid] for cid in sorted(self._by_modality.get(kind, ()))
|
|
192
|
+
if not self.mind._claims[cid].retracted]
|
|
193
|
+
|
|
194
|
+
# -- cross-frame
|
|
195
|
+
|
|
196
|
+
def disagreements(self) -> list[Disagreement]:
|
|
197
|
+
"""Where modalities conflict about one (subject, predicate). Nothing is resolved here."""
|
|
198
|
+
self.index()
|
|
199
|
+
groups: dict[tuple[Ref, str], list[tuple[str, Any, tuple[str, ...]]]] = {}
|
|
200
|
+
for cid, b in sorted(self._bindings.items()):
|
|
201
|
+
rec = self.mind._claims[cid]
|
|
202
|
+
groups.setdefault((b.subject, b.predicate), []).append((cid, rec.claim.object, b.modality))
|
|
203
|
+
out = []
|
|
204
|
+
for (subject, predicate), readings in sorted(groups.items(), key=lambda kv: (kv[0][0].id, kv[0][1])):
|
|
205
|
+
objects = {repr(obj) for _, obj, _ in readings}
|
|
206
|
+
modalities = {mods for _, _, mods in readings}
|
|
207
|
+
if len(objects) > 1 and len(modalities) > 1:
|
|
208
|
+
out.append(Disagreement(subject, predicate, tuple(readings)))
|
|
209
|
+
return out
|
|
210
|
+
|
|
211
|
+
def links(self, claim_id: str, mind: Store, *, slack: float = 80.0, spatial_weight: float = 0.6,
|
|
212
|
+
modality_weight: float = 0.25) -> Iterable[tuple[str, str, float]]:
|
|
213
|
+
"""Extra adjacency for awareness: what is beside this on screen, and what was seen with it."""
|
|
214
|
+
self.index()
|
|
215
|
+
b = self._bindings.get(claim_id)
|
|
216
|
+
if b is None:
|
|
217
|
+
return ()
|
|
218
|
+
out: list[tuple[str, str, float]] = []
|
|
219
|
+
if b.place.box is not None:
|
|
220
|
+
cx, cy = b.place.center # type: ignore[misc]
|
|
221
|
+
reach = int(slack // self._cell) + 1
|
|
222
|
+
cell_x, cell_y = int(cx // self._cell), int(cy // self._cell)
|
|
223
|
+
for dx in range(-reach, reach + 1):
|
|
224
|
+
for dy in range(-reach, reach + 1):
|
|
225
|
+
for cid in self._grid.get((cell_x + dx, cell_y + dy), ()):
|
|
226
|
+
if cid != claim_id and self._bindings[cid].place.near(b.place, slack):
|
|
227
|
+
out.append((cid, "spatial", spatial_weight))
|
|
228
|
+
if b.place.window:
|
|
229
|
+
for cid in sorted(self._by_window.get(b.place.window, ()))[:64]:
|
|
230
|
+
if cid != claim_id:
|
|
231
|
+
out.append((cid, "window", modality_weight))
|
|
232
|
+
return list(dict.fromkeys(out))
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""Symbolic language understanding and generation: one grammar, both directions.
|
|
2
|
+
|
|
3
|
+
from tensorcode.language import ENGLISH, understand, realize, Context, resolve, to_claims
|
|
4
|
+
|
|
5
|
+
got = understand(ENGLISH, "Anem said the north field failed")
|
|
6
|
+
got.meanings # (Frame('say', {speaker/subject, content: Frame('fail', ...)}),)
|
|
7
|
+
got.skipped # words no constituent claimed, reported not guessed
|
|
8
|
+
got.ambiguous # two readings of equal score survive as two readings
|
|
9
|
+
|
|
10
|
+
to_claims(frame, source=Ref("agent:anem")) # reported speech lands in its own scope
|
|
11
|
+
realize(ENGLISH, frame) # say it again with the same grammar
|
|
12
|
+
|
|
13
|
+
No model is called anywhere in this package, and it has no dependencies outside
|
|
14
|
+
the standard library.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from .chart import Chart, Node, Reading, Understanding, build_chart, cover, tokenize, understand, unquote
|
|
18
|
+
from .discourse import Context, resolve, unresolved
|
|
19
|
+
from .english import ENGLISH, ENGLISH_LEXICON
|
|
20
|
+
from .features import FVar, ground, unify
|
|
21
|
+
from .generate import realize, round_trip
|
|
22
|
+
from .grammar import (
|
|
23
|
+
Ask, Attach, Build, Cat, Coord, Ent, Entry, Grammar, Head, Lexicon, Lit, Locative, Merge, Order, Production,
|
|
24
|
+
Qualify, Terminal, inflect, production, words,
|
|
25
|
+
)
|
|
26
|
+
from .grammar import OpenClass, guess_entries
|
|
27
|
+
from .semantics import Entity, Frame, Question, Request, to_claims
|
|
28
|
+
|
|
29
|
+
__all__ = [
|
|
30
|
+
"Ask", "Attach", "Build", "Cat", "Chart", "Context", "Coord", "ENGLISH", "ENGLISH_LEXICON", "Ent", "Entity",
|
|
31
|
+
"Entry", "FVar", "Frame", "Grammar", "Head", "Lexicon", "Lit", "Locative", "Merge", "Node", "Order",
|
|
32
|
+
"OpenClass", "Production", "Qualify", "Question", "Reading", "Request", "Terminal", "Understanding",
|
|
33
|
+
"build_chart", "guess_entries",
|
|
34
|
+
"cover", "ground", "inflect", "production", "realize", "resolve", "round_trip", "to_claims", "tokenize",
|
|
35
|
+
"unify", "unquote", "unresolved", "understand", "words",
|
|
36
|
+
]
|