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.
- autonometry/__init__.py +5 -0
- autonometry/checks.py +197 -0
- autonometry/cli.py +1787 -0
- autonometry/cli_g1.py +212 -0
- autonometry/cli_lerobot.py +317 -0
- autonometry/connectors/__init__.py +6 -0
- autonometry/connectors/autonometry_client.py +179 -0
- autonometry/connectors/g1.py +1741 -0
- autonometry/connectors/g1_dds.py +1162 -0
- autonometry/connectors/heartbeats.py +78 -0
- autonometry/connectors/lerobot.py +990 -0
- autonometry/connectors/lerobot_io.py +930 -0
- autonometry/connectors/names.py +37 -0
- autonometry/connectors/station.py +235 -0
- autonometry/connectors/tb4.py +1457 -0
- autonometry/demo/__init__.py +103 -0
- autonometry/demo/session/about.txt +12 -0
- autonometry/demo/session/devices.json +27 -0
- autonometry/demo/session/events.jsonl +765 -0
- autonometry/demo/session/plan.json +3 -0
- autonometry/demo/session/tasks.json +12 -0
- autonometry/display.py +38 -0
- autonometry/events.py +408 -0
- autonometry/fileio.py +76 -0
- autonometry/keys.py +134 -0
- autonometry/merkle.py +87 -0
- autonometry/paths.py +17 -0
- autonometry/py.typed +0 -0
- autonometry/reader.py +420 -0
- autonometry/recorder/__init__.py +7 -0
- autonometry/recorder/anchor.py +148 -0
- autonometry/recorder/clock.py +111 -0
- autonometry/recorder/config.py +320 -0
- autonometry/recorder/engine.py +465 -0
- autonometry/recorder/runtime.py +327 -0
- autonometry/recorder/service.py +281 -0
- autonometry/recorder/store.py +572 -0
- autonometry/recorder/wire.py +144 -0
- autonometry/registry.py +232 -0
- autonometry/report/__init__.py +10 -0
- autonometry/report/build.py +406 -0
- autonometry/report/census.py +62 -0
- autonometry/report/figures.py +267 -0
- autonometry/report/html.py +299 -0
- autonometry/report/invariants.py +212 -0
- autonometry/report/statements.py +173 -0
- autonometry/report/templates/report.html.j2 +309 -0
- autonometry/report/trust.py +173 -0
- autonometry/sealfile.py +208 -0
- autonometry/sealing.py +125 -0
- autonometry/signednote.py +311 -0
- autonometry/spec/README.md +32 -0
- autonometry/spec/v0.3/checkpoint.schema.json +38 -0
- autonometry/spec/v0.3/definitions.md +416 -0
- autonometry/spec/v0.3/device_registry.schema.json +65 -0
- autonometry/spec/v0.3/event.schema.json +243 -0
- autonometry/spec/v0.3/plan.schema.json +38 -0
- autonometry/spec/v0.3/rules.toml +149 -0
- autonometry/spec/v0.3/task_registry.schema.json +37 -0
- autonometry/spec.py +464 -0
- autonometry/stats/__init__.py +12 -0
- autonometry/stats/facts.py +311 -0
- autonometry/stats/intervals.py +661 -0
- autonometry/stats/metrics.py +955 -0
- autonometry/stats/planning.py +215 -0
- autonometry/stats/report.py +77 -0
- autonometry/strictjson.py +125 -0
- autonometry/timeline.py +1840 -0
- autonometry/timestamps.py +67 -0
- autonometry/tsa.py +294 -0
- autonometry/verify.py +452 -0
- autonometry/witness.py +829 -0
- autonometry-0.1.0.dist-info/METADATA +177 -0
- autonometry-0.1.0.dist-info/RECORD +77 -0
- autonometry-0.1.0.dist-info/WHEEL +4 -0
- autonometry-0.1.0.dist-info/entry_points.txt +2 -0
- autonometry-0.1.0.dist-info/licenses/LICENSE +202 -0
autonometry/__init__.py
ADDED
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"
|