autonometry 0.1.0__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 (77) hide show
  1. autonometry/__init__.py +5 -0
  2. autonometry/checks.py +197 -0
  3. autonometry/cli.py +1787 -0
  4. autonometry/cli_g1.py +212 -0
  5. autonometry/cli_lerobot.py +317 -0
  6. autonometry/connectors/__init__.py +6 -0
  7. autonometry/connectors/autonometry_client.py +179 -0
  8. autonometry/connectors/g1.py +1741 -0
  9. autonometry/connectors/g1_dds.py +1162 -0
  10. autonometry/connectors/heartbeats.py +78 -0
  11. autonometry/connectors/lerobot.py +990 -0
  12. autonometry/connectors/lerobot_io.py +930 -0
  13. autonometry/connectors/names.py +37 -0
  14. autonometry/connectors/station.py +235 -0
  15. autonometry/connectors/tb4.py +1457 -0
  16. autonometry/demo/__init__.py +103 -0
  17. autonometry/demo/session/about.txt +12 -0
  18. autonometry/demo/session/devices.json +27 -0
  19. autonometry/demo/session/events.jsonl +765 -0
  20. autonometry/demo/session/plan.json +3 -0
  21. autonometry/demo/session/tasks.json +12 -0
  22. autonometry/display.py +38 -0
  23. autonometry/events.py +408 -0
  24. autonometry/fileio.py +76 -0
  25. autonometry/keys.py +134 -0
  26. autonometry/merkle.py +87 -0
  27. autonometry/paths.py +17 -0
  28. autonometry/py.typed +0 -0
  29. autonometry/reader.py +420 -0
  30. autonometry/recorder/__init__.py +7 -0
  31. autonometry/recorder/anchor.py +148 -0
  32. autonometry/recorder/clock.py +111 -0
  33. autonometry/recorder/config.py +320 -0
  34. autonometry/recorder/engine.py +465 -0
  35. autonometry/recorder/runtime.py +327 -0
  36. autonometry/recorder/service.py +281 -0
  37. autonometry/recorder/store.py +572 -0
  38. autonometry/recorder/wire.py +144 -0
  39. autonometry/registry.py +232 -0
  40. autonometry/report/__init__.py +10 -0
  41. autonometry/report/build.py +406 -0
  42. autonometry/report/census.py +62 -0
  43. autonometry/report/figures.py +267 -0
  44. autonometry/report/html.py +299 -0
  45. autonometry/report/invariants.py +212 -0
  46. autonometry/report/statements.py +173 -0
  47. autonometry/report/templates/report.html.j2 +309 -0
  48. autonometry/report/trust.py +173 -0
  49. autonometry/sealfile.py +208 -0
  50. autonometry/sealing.py +125 -0
  51. autonometry/signednote.py +311 -0
  52. autonometry/spec/README.md +32 -0
  53. autonometry/spec/v0.3/checkpoint.schema.json +38 -0
  54. autonometry/spec/v0.3/definitions.md +416 -0
  55. autonometry/spec/v0.3/device_registry.schema.json +65 -0
  56. autonometry/spec/v0.3/event.schema.json +243 -0
  57. autonometry/spec/v0.3/plan.schema.json +38 -0
  58. autonometry/spec/v0.3/rules.toml +149 -0
  59. autonometry/spec/v0.3/task_registry.schema.json +37 -0
  60. autonometry/spec.py +464 -0
  61. autonometry/stats/__init__.py +12 -0
  62. autonometry/stats/facts.py +311 -0
  63. autonometry/stats/intervals.py +661 -0
  64. autonometry/stats/metrics.py +955 -0
  65. autonometry/stats/planning.py +215 -0
  66. autonometry/stats/report.py +77 -0
  67. autonometry/strictjson.py +125 -0
  68. autonometry/timeline.py +1840 -0
  69. autonometry/timestamps.py +67 -0
  70. autonometry/tsa.py +294 -0
  71. autonometry/verify.py +452 -0
  72. autonometry/witness.py +829 -0
  73. autonometry-0.1.0.dist-info/METADATA +177 -0
  74. autonometry-0.1.0.dist-info/RECORD +77 -0
  75. autonometry-0.1.0.dist-info/WHEEL +4 -0
  76. autonometry-0.1.0.dist-info/entry_points.txt +2 -0
  77. autonometry-0.1.0.dist-info/licenses/LICENSE +202 -0
@@ -0,0 +1,5 @@
1
+ """Autonometry: measure how much of a robot's work is done by AI and how much by humans."""
2
+
3
+ __version__ = "0.1.0"
4
+
5
+ __all__ = ["__version__"]
autonometry/checks.py ADDED
@@ -0,0 +1,197 @@
1
+ """Structural checks of one session: the loader flags (definitions.md sections 1 to 3, 5 and 8).
2
+
3
+ Pure functions over validated events in ``seq`` order. A flag is a warning: the event stays and
4
+ later stages turn the problem into ``unknown`` time where the spec says so.
5
+ """
6
+
7
+ import math
8
+ from collections.abc import Iterator, Mapping
9
+ from itertools import pairwise
10
+ from typing import Any, cast
11
+
12
+ from autonometry.events import Event, Flag, FlagCode, Session
13
+ from autonometry.spec import Rules
14
+ from autonometry.timestamps import ns_to_seconds, seconds_to_ns, ts_to_ns
15
+
16
+
17
+ def check_session(session: Session, rules: Rules) -> list[Flag]:
18
+ """Return the loader flags of one session in the order found.
19
+
20
+ Implements: ``no_session_start``, ``no_session_end``, ``repeated_session_marker`` (one flag
21
+ per repeated marker) and ``event_outside_session`` (one flag per side with the count; the first
22
+ ``session_start`` and ``session_end`` bound the session) (definitions.md 1, 2 rule 4);
23
+ ``missing_initial_state`` (2 rule 1); ``undeclared_scope`` (1, control scope); ``seq_gap``,
24
+ ``time_gap`` (2 rule 2; silence longer than the session's ``heartbeat_interval_s`` plus
25
+ ``gap_tolerance_s``, in seconds); ``ts_regression`` (3, ``ts``); ``nested_task``,
26
+ ``task_end_without_start``, ``task_not_closed`` (1, task; 5); ``distance_regression`` (8,
27
+ ``distance_m`` in metres); ``ts_reconstructed`` (8, Time) and ``attribution_inferred`` (6.12),
28
+ one flag per session with the count; ``clock_unsynced`` (8, Time; one flag per session).
29
+ """
30
+
31
+ def flag(seq: int, code: FlagCode, detail: str) -> Flag:
32
+ return Flag(session.robot_id, session.session_id, seq, code, detail)
33
+
34
+ events = session.events
35
+ start = next((e for e in events if e["type"] == "session_start"), None)
36
+ flags: list[Flag] = []
37
+ if start is None:
38
+ flags.append(flag(events[0]["seq"], "no_session_start", "no session_start event"))
39
+ if not any(e["type"] == "session_end" for e in events):
40
+ flags.append(flag(events[-1]["seq"], "no_session_end", "no session_end event"))
41
+ if start is not None:
42
+ flags += [flag(start["seq"], code, detail) for code, detail in _declarations(start)]
43
+ heartbeat_s = rules.params.heartbeat_interval_s
44
+ if start is not None:
45
+ heartbeat_s = start.get("heartbeat_interval_s", heartbeat_s)
46
+ max_silence_ns = seconds_to_ns(heartbeat_s + rules.params.gap_tolerance_s)
47
+ scopes = frozenset(start.get("scopes", ())) if start is not None else None
48
+ flags += [flag(seq, code, detail) for seq, code, detail in _markers(events)]
49
+ flags += [flag(seq, code, detail) for seq, code, detail in _sequence(events, max_silence_ns)]
50
+ flags += [flag(seq, code, detail) for seq, code, detail in _per_event(events, scopes)]
51
+ flags += [flag(seq, code, detail) for seq, code, detail in _tasks(events)]
52
+ flags += [flag(seq, code, detail) for seq, code, detail in _marked(events)]
53
+ clock = _clock(events, rules.sealing.clock_tolerance_s)
54
+ if clock is not None:
55
+ flags.append(flag(*clock))
56
+ return flags
57
+
58
+
59
+ _MARKERS: tuple[tuple[str, FlagCode], ...] = (
60
+ ("x_ts_reconstructed", "ts_reconstructed"),
61
+ ("x_attribution_inferred", "attribution_inferred"),
62
+ )
63
+
64
+ _UNSYNCED_SOURCES = frozenset({"none", "unknown"})
65
+
66
+
67
+ def _marked(events: tuple[Event, ...]) -> Iterator[tuple[int, FlagCode, str]]:
68
+ """Events rebuilt after the fact (definitions.md 8, Time) or whose source was inferred
69
+ (6.12): one flag per marker at its first event, with the count."""
70
+ for field, code in _MARKERS:
71
+ found = [e["seq"] for e in events if _extension(e, field) is True]
72
+ if found:
73
+ yield found[0], code, f"{_events(len(found))} with {field}: true"
74
+
75
+
76
+ def _clock(events: tuple[Event, ...], tolerance_s: float) -> tuple[int, FlagCode, str] | None:
77
+ """``clock_unsynced`` when any heartbeat reports ``clock_source`` ``none`` or ``unknown``,
78
+ reports no source, or reports ``clock_max_error_s`` above ``tolerance_s`` seconds or not at
79
+ all; also when no heartbeat reports the clock (definitions.md 8, Time)."""
80
+ heartbeats = [e for e in events if e["type"] == "heartbeat"]
81
+ if not heartbeats:
82
+ return events[0]["seq"], "clock_unsynced", "no heartbeat reports the clock status"
83
+ unsynced = [
84
+ e["seq"]
85
+ for e in heartbeats
86
+ if e.get("clock_source", "unknown") in _UNSYNCED_SOURCES
87
+ or e.get("clock_max_error_s", math.inf) > tolerance_s
88
+ ]
89
+ if not unsynced:
90
+ return None
91
+ detail = (
92
+ f"{len(unsynced)} of {len(heartbeats)} heartbeat(s) without a synchronised clock "
93
+ f"(clock_source none, unknown or missing, or clock_max_error_s missing or above "
94
+ f"{tolerance_s:g} s)"
95
+ )
96
+ return unsynced[0], "clock_unsynced", detail
97
+
98
+
99
+ def _events(count: int) -> str:
100
+ return f"{count} event{'s' if count != 1 else ''}"
101
+
102
+
103
+ def _extension(event: Event, name: str) -> Any:
104
+ return cast(Mapping[str, Any], event).get(name)
105
+
106
+
107
+ def _declarations(start: Event) -> Iterator[tuple[FlagCode, str]]:
108
+ """Scopes declared at session_start against its initial_states and scope_weights."""
109
+ scopes = set(start.get("scopes", ()))
110
+ initial = set(start.get("initial_states", {}))
111
+ for scope in sorted(scopes - initial):
112
+ yield "missing_initial_state", f"scope {scope!r} has no initial state"
113
+ for scope in sorted(initial - scopes):
114
+ yield "undeclared_scope", f"initial_states names undeclared scope {scope!r}"
115
+ for scope in sorted(set(start.get("scope_weights", {})) - scopes):
116
+ yield "undeclared_scope", f"scope_weights names undeclared scope {scope!r}"
117
+
118
+
119
+ def _markers(events: tuple[Event, ...]) -> Iterator[tuple[int, FlagCode, str]]:
120
+ """Repeated session_start or session_end, and events before the first session_start or after
121
+ the first session_end."""
122
+ first: dict[str, int] = {}
123
+ for event in events:
124
+ kind, seq = event["type"], event["seq"]
125
+ if kind in ("session_start", "session_end"):
126
+ if kind in first:
127
+ yield seq, "repeated_session_marker", f"{kind} repeats the one at seq {first[kind]}"
128
+ else:
129
+ first[kind] = seq
130
+ seqs = [event["seq"] for event in events]
131
+ start, end = first.get("session_start"), first.get("session_end")
132
+ outside = {
133
+ "before session_start": [s for s in seqs if start is not None and s < start],
134
+ "after session_end": [s for s in seqs if end is not None and s > end],
135
+ }
136
+ for where, found in outside.items():
137
+ if found:
138
+ yield found[0], "event_outside_session", f"{_events(len(found))} {where}"
139
+
140
+
141
+ def _sequence(
142
+ events: tuple[Event, ...], max_silence_ns: int
143
+ ) -> Iterator[tuple[int, FlagCode, str]]:
144
+ """Gaps in seq, ts going backwards and silences between consecutive events."""
145
+ limit = f"{ns_to_seconds(max_silence_ns):g} s"
146
+ times_ns = [ts_to_ns(event["ts"]) for event in events]
147
+ for (previous, event), (before_ns, now_ns) in zip(
148
+ pairwise(events), pairwise(times_ns), strict=True
149
+ ):
150
+ before, seq = previous["seq"], event["seq"]
151
+ if seq > before + 1:
152
+ yield seq, "seq_gap", f"seq jumps from {before} to {seq}"
153
+ elapsed = f"{ns_to_seconds(abs(now_ns - before_ns)):.3f} s"
154
+ if now_ns < before_ns:
155
+ yield seq, "ts_regression", f"ts is {elapsed} earlier than at seq {before}"
156
+ elif now_ns - before_ns > max_silence_ns:
157
+ yield seq, "time_gap", f"{elapsed} of silence after seq {before} (limit {limit})"
158
+
159
+
160
+ def _per_event(
161
+ events: tuple[Event, ...], scopes: frozenset[str] | None
162
+ ) -> Iterator[tuple[int, FlagCode, str]]:
163
+ """Scopes not declared at session_start and cumulative distance going backwards."""
164
+ last_distance: float | None = None
165
+ for event in events:
166
+ seq = event["seq"]
167
+ scope = event.get("scope")
168
+ if scopes is not None and scope is not None and scope not in scopes:
169
+ yield seq, "undeclared_scope", f"scope {scope!r} is not declared at session_start"
170
+ distance = event.get("distance_m")
171
+ if distance is None:
172
+ continue
173
+ if last_distance is not None and distance < last_distance:
174
+ yield seq, "distance_regression", f"distance_m falls from {last_distance} to {distance}"
175
+ last_distance = distance
176
+
177
+
178
+ def _tasks(events: tuple[Event, ...]) -> Iterator[tuple[int, FlagCode, str]]:
179
+ """Open tasks, derived only from task_start and task_end."""
180
+ open_tasks: dict[str | None, int] = {}
181
+ for event in events:
182
+ seq, task_id = event["seq"], event["task_id"]
183
+ if event["type"] == "task_start":
184
+ if open_tasks:
185
+ yield (
186
+ seq,
187
+ "nested_task",
188
+ f"task_start while the task opened at seq {min(open_tasks.values())} is open",
189
+ )
190
+ open_tasks.setdefault(task_id, seq)
191
+ elif event["type"] == "task_end":
192
+ if task_id in open_tasks:
193
+ del open_tasks[task_id]
194
+ else:
195
+ yield seq, "task_end_without_start", "task_end for a task that is not open"
196
+ for start_seq in sorted(open_tasks.values()):
197
+ yield start_seq, "task_not_closed", "task_start has no task_end"