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.
Files changed (53) hide show
  1. tensorcode/__init__.py +84 -0
  2. tensorcode/actions.py +137 -0
  3. tensorcode/answer_type.py +222 -0
  4. tensorcode/awareness.py +344 -0
  5. tensorcode/backends/__init__.py +0 -0
  6. tensorcode/backends/builtin.py +167 -0
  7. tensorcode/backends/hf_local.py +89 -0
  8. tensorcode/backends/linear.py +133 -0
  9. tensorcode/backends/neural.py +361 -0
  10. tensorcode/causal.py +262 -0
  11. tensorcode/change.py +566 -0
  12. tensorcode/chunking.py +195 -0
  13. tensorcode/cognition.py +311 -0
  14. tensorcode/context.py +97 -0
  15. tensorcode/control.py +291 -0
  16. tensorcode/cues.py +192 -0
  17. tensorcode/expectation.py +270 -0
  18. tensorcode/frames.py +232 -0
  19. tensorcode/language/__init__.py +36 -0
  20. tensorcode/language/chart.py +558 -0
  21. tensorcode/language/discourse.py +132 -0
  22. tensorcode/language/domains/__init__.py +0 -0
  23. tensorcode/language/domains/desktop.py +552 -0
  24. tensorcode/language/english.py +459 -0
  25. tensorcode/language/features.py +112 -0
  26. tensorcode/language/generate.py +574 -0
  27. tensorcode/language/grammar.py +893 -0
  28. tensorcode/language/semantics.py +349 -0
  29. tensorcode/learning/__init__.py +30 -0
  30. tensorcode/learning/certificate.py +148 -0
  31. tensorcode/learning/induce.py +304 -0
  32. tensorcode/learning/library.py +217 -0
  33. tensorcode/learning/literals.py +126 -0
  34. tensorcode/learning/verify.py +253 -0
  35. tensorcode/memory.py +303 -0
  36. tensorcode/metacognition.py +351 -0
  37. tensorcode/ops.py +207 -0
  38. tensorcode/outcomes.py +99 -0
  39. tensorcode/permanence.py +376 -0
  40. tensorcode/priming.py +191 -0
  41. tensorcode/py.typed +0 -0
  42. tensorcode/quantity.py +311 -0
  43. tensorcode/records.py +728 -0
  44. tensorcode/relation.py +771 -0
  45. tensorcode/runtime.py +471 -0
  46. tensorcode/semantics_bridge.py +308 -0
  47. tensorcode/social.py +380 -0
  48. tensorcode/temporal.py +189 -0
  49. tensorcode/wants.py +185 -0
  50. tensorcode-0.1.0a1.dist-info/METADATA +196 -0
  51. tensorcode-0.1.0a1.dist-info/RECORD +53 -0
  52. tensorcode-0.1.0a1.dist-info/WHEEL +4 -0
  53. 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
+ ]