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
tensorcode/quantity.py ADDED
@@ -0,0 +1,311 @@
1
+ """Quantities with units, and arithmetic that refuses rather than guesses.
2
+
3
+ A number in a sentence is not a number: "3 sheep", "3 coins per sheep" and "3%" compose
4
+ differently, and adding the first two is not a small error but a category mistake. So a
5
+ quantity carries its unit, a unit carries its dimension, and every operation checks that
6
+ the dimensions line up. A mismatch returns :class:`~tensorcode.outcomes.Unknown` — the
7
+ same refusal the rest of the library uses — never a number that looks fine.
8
+
9
+ >>> sheep = Quantity(12, Unit.of("sheep"))
10
+ >>> price = Quantity(5, Unit.of("coin") / Unit.of("sheep"))
11
+ >>> mul(sheep, price)
12
+ Quantity(value=60.0, unit=Unit(coin))
13
+ >>> isinstance(add(sheep, price), Unknown)
14
+ True
15
+
16
+ Derivations are recorded, not just computed: :func:`derive` writes the result as a claim
17
+ whose evidence names the premises and the operation, so ``explain`` shows the working.
18
+ """
19
+
20
+ from __future__ import annotations
21
+
22
+ import math
23
+ from collections import Counter
24
+ from dataclasses import dataclass, field
25
+ from datetime import datetime, timezone
26
+ from typing import Any, Iterable, Mapping
27
+
28
+ from .outcomes import Score, Unknown
29
+ from .records import Claim, Evidence, Ref, Store
30
+
31
+ # --------------------------------------------------------------------------- units
32
+
33
+ #: base dimension per known unit symbol, with the factor into that dimension's base unit.
34
+ #: "item" is the dimension of anything counted; a bare count has no unit of its own.
35
+ BASE_UNITS: dict[str, tuple[str, float]] = {
36
+ "item": ("item", 1.0),
37
+ # currency
38
+ "coin": ("currency", 1.0), "dollar": ("currency", 1.0), "cent": ("currency", 0.01),
39
+ "euro": ("currency", 1.0), "pound_sterling": ("currency", 1.0),
40
+ # mass
41
+ "kilogram": ("mass", 1.0), "gram": ("mass", 0.001), "pound": ("mass", 0.45359237), "ounce": ("mass", 0.0283495),
42
+ # volume
43
+ "litre": ("volume", 1.0), "millilitre": ("volume", 0.001), "bushel": ("volume", 35.2391), "gallon": ("volume", 3.78541),
44
+ # length
45
+ "metre": ("length", 1.0), "centimetre": ("length", 0.01), "kilometre": ("length", 1000.0),
46
+ "inch": ("length", 0.0254), "foot": ("length", 0.3048), "mile": ("length", 1609.344),
47
+ # time
48
+ "second": ("time", 1.0), "minute": ("time", 60.0), "hour": ("time", 3600.0),
49
+ "day": ("time", 86400.0), "week": ("time", 604800.0), "year": ("time", 31557600.0),
50
+ }
51
+
52
+ #: surface spellings that mean a known unit. Plurals are stripped before lookup.
53
+ ALIASES: dict[str, str] = {
54
+ "$": "dollar", "usd": "dollar", "buck": "dollar", "€": "euro", "£": "pound_sterling", "penny": "cent", "pennies": "cent",
55
+ "kg": "kilogram", "g": "gram", "lb": "pound", "lbs": "pound", "oz": "ounce",
56
+ "l": "litre", "ml": "millilitre", "m": "metre", "cm": "centimetre", "km": "kilometre",
57
+ "ft": "foot", "feet": "foot", "in": "inch", "mi": "mile",
58
+ "s": "second", "sec": "second", "min": "minute", "hr": "hour", "hrs": "hour", "h": "hour",
59
+ "mins": "minute", "yr": "year",
60
+ }
61
+
62
+
63
+ def normalize_unit(word: str) -> str:
64
+ """A surface word to a unit symbol. Unknown words become their own count unit."""
65
+ w = word.strip().lower().rstrip(".")
66
+ w = ALIASES.get(w, w)
67
+ if w in BASE_UNITS:
68
+ return w
69
+ # try each way this could be a plural, and take the first that names a unit we know;
70
+ # "minutes" is minute (not "minut"), while an unknown word keeps its singular stem
71
+ if w.endswith(("us", "is", "ss")) or len(w) < 3:
72
+ return w # a singular that merely ends in s: bus, iris, glass — nothing to strip
73
+ stems = []
74
+ if w.endswith("ies") and len(w) > 4:
75
+ stems.append(w[:-3] + "y")
76
+ if w.endswith("es") and len(w) > 3:
77
+ stems += [w[:-1], w[:-2]]
78
+ if w.endswith("s") and len(w) > 2:
79
+ stems.append(w[:-1])
80
+ for stem in stems:
81
+ candidate = ALIASES.get(stem, stem)
82
+ if candidate in BASE_UNITS:
83
+ return candidate
84
+ if not stems:
85
+ return w
86
+ # An unknown count noun. "-es" is the plural marker after a sibilant ("boxes" -> box,
87
+ # "glasses" -> glass), but a word ending in silent e takes a bare "-s" ("houses" ->
88
+ # house). Testing for a doubled s keeps those apart: "hous" ends in one s and is
89
+ # rejected, "glass" in two and is kept. Getting this wrong would file a thing's
90
+ # singular and plural as different units and then refuse to add them together.
91
+ if w.endswith("es") and len(w) > 3:
92
+ stripped = w[:-2]
93
+ return stripped if stripped.endswith(("ss", "x", "z", "ch", "sh")) else w[:-1]
94
+ return stems[0]
95
+
96
+
97
+ @dataclass(frozen=True)
98
+ class Unit:
99
+ """A product of unit symbols with integer exponents. ``Unit()`` is dimensionless."""
100
+
101
+ powers: Mapping[str, int] = field(default_factory=dict)
102
+
103
+ def __post_init__(self) -> None:
104
+ object.__setattr__(self, "powers", {k: v for k, v in sorted(self.powers.items()) if v})
105
+
106
+ @classmethod
107
+ def of(cls, symbol: str, power: int = 1) -> "Unit":
108
+ return cls({normalize_unit(symbol): power}) if symbol else cls()
109
+
110
+ @property
111
+ def dimension(self) -> tuple[tuple[str, int], ...]:
112
+ """The dimension signature: what may be added to what."""
113
+ dims: Counter[str] = Counter()
114
+ for symbol, power in self.powers.items():
115
+ dims[BASE_UNITS.get(symbol, ("item", 1.0))[0] if symbol in BASE_UNITS else f"count:{symbol}"] += power
116
+ return tuple(sorted((d, p) for d, p in dims.items() if p))
117
+
118
+ @property
119
+ def dimensionless(self) -> bool:
120
+ return not self.powers
121
+
122
+ def factor(self) -> float:
123
+ """Scale into base units of each dimension, so comparable quantities compare."""
124
+ out = 1.0
125
+ for symbol, power in self.powers.items():
126
+ out *= BASE_UNITS.get(symbol, (None, 1.0))[1] ** power
127
+ return out
128
+
129
+ def __mul__(self, other: "Unit") -> "Unit":
130
+ merged = Counter(self.powers)
131
+ merged.update(other.powers)
132
+ return Unit(dict(merged))
133
+
134
+ def __truediv__(self, other: "Unit") -> "Unit":
135
+ merged = Counter(self.powers)
136
+ merged.subtract(other.powers)
137
+ return Unit(dict(merged))
138
+
139
+ def __pow__(self, n: int) -> "Unit":
140
+ return Unit({s: p * n for s, p in self.powers.items()})
141
+
142
+ def __str__(self) -> str:
143
+ if not self.powers:
144
+ return ""
145
+ parts = [s if p == 1 else f"{s}^{p}" for s, p in self.powers.items() if p > 0]
146
+ under = [s if p == -1 else f"{s}^{-p}" for s, p in self.powers.items() if p < 0]
147
+ head = "·".join(parts) or "1"
148
+ return head + ("/" + "·".join(under) if under else "")
149
+
150
+ def __repr__(self) -> str:
151
+ return f"Unit({self})"
152
+
153
+
154
+ @dataclass(frozen=True)
155
+ class Quantity:
156
+ """A measured value: how much, of what unit."""
157
+
158
+ value: float
159
+ unit: Unit = field(default_factory=Unit)
160
+
161
+ def __post_init__(self) -> None:
162
+ object.__setattr__(self, "value", float(self.value))
163
+
164
+ @classmethod
165
+ def parse(cls, value: float, unit_word: str | None = None, power: int = 1) -> "Quantity":
166
+ return cls(value, Unit.of(unit_word, power) if unit_word else Unit())
167
+
168
+ @property
169
+ def dimension(self) -> tuple[tuple[str, int], ...]:
170
+ return self.unit.dimension
171
+
172
+ def base(self) -> float:
173
+ """The value in base units, for comparison across spellings of one dimension."""
174
+ return self.value * self.unit.factor()
175
+
176
+ def comparable(self, other: "Quantity") -> bool:
177
+ return self.dimension == other.dimension
178
+
179
+ def __str__(self) -> str:
180
+ shown = f"{self.value:g}"
181
+ return f"{shown} {self.unit}".strip()
182
+
183
+ def __repr__(self) -> str:
184
+ return f"Quantity(value={self.value}, unit={self.unit!r})"
185
+
186
+
187
+ # ----------------------------------------------------------------- arithmetic
188
+
189
+ MISMATCH = "dimension_mismatch"
190
+
191
+
192
+ def _mismatch(op: str, a: Quantity, b: Quantity) -> Unknown:
193
+ return Unknown(MISMATCH, f"cannot {op} {a} and {b}: {_dim(a)} vs {_dim(b)}")
194
+
195
+
196
+ def _dim(q: Quantity) -> str:
197
+ return "·".join(f"{d}^{p}" if p != 1 else d for d, p in q.dimension) or "dimensionless"
198
+
199
+
200
+ def add(a: Quantity, b: Quantity) -> Quantity | Unknown:
201
+ if not a.comparable(b):
202
+ return _mismatch("add", a, b)
203
+ return Quantity(a.base() + b.base(), _base_unit(a.unit)) if a.unit != b.unit else Quantity(a.value + b.value, a.unit)
204
+
205
+
206
+ def sub(a: Quantity, b: Quantity) -> Quantity | Unknown:
207
+ if not a.comparable(b):
208
+ return _mismatch("subtract", a, b)
209
+ return Quantity(a.base() - b.base(), _base_unit(a.unit)) if a.unit != b.unit else Quantity(a.value - b.value, a.unit)
210
+
211
+
212
+ def mul(a: Quantity, b: Quantity) -> Quantity:
213
+ return Quantity(a.value * b.value, a.unit * b.unit)
214
+
215
+
216
+ def div(a: Quantity, b: Quantity) -> Quantity | Unknown:
217
+ if b.value == 0:
218
+ return Unknown("division_by_zero", f"cannot divide {a} by zero")
219
+ return Quantity(a.value / b.value, a.unit / b.unit)
220
+
221
+
222
+ def scale(a: Quantity, factor: float) -> Quantity:
223
+ return Quantity(a.value * factor, a.unit)
224
+
225
+
226
+ def ratio(a: Quantity, b: Quantity) -> Quantity | Unknown:
227
+ """A dimensionless ratio, only between comparable quantities."""
228
+ if not a.comparable(b):
229
+ return _mismatch("compare", a, b)
230
+ if b.base() == 0:
231
+ return Unknown("division_by_zero", f"cannot divide {a} by zero")
232
+ return Quantity(a.base() / b.base(), Unit())
233
+
234
+
235
+ def percent_of(part: Quantity, whole: Quantity) -> Quantity | Unknown:
236
+ got = ratio(part, whole)
237
+ return got if isinstance(got, Unknown) else Quantity(got.value * 100, Unit.of("percent"))
238
+
239
+
240
+ def _base_unit(unit: Unit) -> Unit:
241
+ """The base spelling of each dimension in a unit, used when two spellings are added."""
242
+ out: Counter[str] = Counter()
243
+ for symbol, power in unit.powers.items():
244
+ dim = BASE_UNITS.get(symbol, (None, None))[0]
245
+ base = next((s for s, (d, f) in BASE_UNITS.items() if d == dim and f == 1.0), symbol) if dim else symbol
246
+ out[base] += power
247
+ return Unit(dict(out))
248
+
249
+
250
+ OPS = {"add": add, "sub": sub, "mul": mul, "div": div, "ratio": ratio, "percent_of": percent_of}
251
+
252
+
253
+ def compare(a: Quantity, b: Quantity) -> str | Unknown:
254
+ """'greater', 'less' or 'equal' — or a refusal when the dimensions differ."""
255
+ if not a.comparable(b):
256
+ return _mismatch("compare", a, b)
257
+ x, y = a.base(), b.base()
258
+ if math.isclose(x, y, rel_tol=1e-9, abs_tol=1e-12):
259
+ return "equal"
260
+ return "greater" if x > y else "less"
261
+
262
+
263
+ # --------------------------------------------------------------------- claims
264
+
265
+
266
+ def tell_quantity(mind: Store, subject: Ref, predicate: str, quantity: Quantity, *, source: Ref,
267
+ observed_at: datetime | None = None, method: str = "quantity", confidence: Score | None = None) -> Claim:
268
+ """Record a quantity as a claim, keeping the unit with the number."""
269
+ claim = Claim(subject, predicate, quantity)
270
+ mind.tell(claim, Evidence(source=source, observed_at=observed_at or datetime.now(timezone.utc), method=method, confidence=confidence))
271
+ return claim
272
+
273
+
274
+ def derive(mind: Store, subject: Ref, predicate: str, op: str, premises: Iterable[Claim], *,
275
+ source: Ref | None = None, observed_at: datetime | None = None,
276
+ factor: float | None = None) -> Claim | Unknown:
277
+ """Compute ``op`` over the premises' quantities and record the result with its working.
278
+
279
+ The evidence names the operation and the premise claim ids, so ``explain`` shows the
280
+ arithmetic and retracting a premise withdraws the conclusion.
281
+ """
282
+ premises = list(premises)
283
+ values = [p.object for p in premises]
284
+ if not all(isinstance(v, Quantity) for v in values):
285
+ return Unknown("not_quantities", f"{op} needs quantities, got {[type(v).__name__ for v in values]}")
286
+ if op == "scale":
287
+ if factor is None or len(values) != 1:
288
+ return Unknown("bad_arity", "scale takes one quantity and a factor")
289
+ result: Any = scale(values[0], factor)
290
+ elif op in OPS:
291
+ if len(values) != 2:
292
+ return Unknown("bad_arity", f"{op} takes two quantities, got {len(values)}")
293
+ result = OPS[op](values[0], values[1])
294
+ elif op == "sum":
295
+ result = values[0]
296
+ for v in values[1:]:
297
+ result = add(result, v)
298
+ if isinstance(result, Unknown):
299
+ break
300
+ else:
301
+ return Unknown("unknown_operation", op)
302
+ if isinstance(result, Unknown):
303
+ return result
304
+ claim = Claim(subject, predicate, result)
305
+ mind.tell(claim, Evidence(
306
+ source=source or Ref("reasoning:arithmetic"),
307
+ observed_at=observed_at or datetime.now(timezone.utc),
308
+ method=f"arithmetic:{op}" + (f"×{factor:g}" if factor is not None else ""),
309
+ derived_from=tuple(p.id for p in premises),
310
+ ))
311
+ return claim