sequence-base 0.5.0__tar.gz → 0.6.0__tar.gz
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.
- {sequence_base-0.5.0 → sequence_base-0.6.0}/PKG-INFO +1 -1
- {sequence_base-0.5.0 → sequence_base-0.6.0}/pyproject.toml +1 -1
- {sequence_base-0.5.0 → sequence_base-0.6.0}/sequence_base/__init__.py +1 -3
- sequence_base-0.6.0/sequence_base/codec/compressed.py +174 -0
- {sequence_base-0.5.0 → sequence_base-0.6.0}/sequence_base/constants/limits.py +1 -1
- {sequence_base-0.5.0 → sequence_base-0.6.0}/sequence_base/constants/version.py +3 -1
- sequence_base-0.6.0/sequence_base/construct.py +173 -0
- {sequence_base-0.5.0 → sequence_base-0.6.0}/sequence_base/contract/__init__.py +27 -16
- {sequence_base-0.5.0 → sequence_base-0.6.0}/sequence_base/contract/benchmarks.py +48 -19
- {sequence_base-0.5.0 → sequence_base-0.6.0}/sequence_base/contract/connect.py +13 -22
- {sequence_base-0.5.0 → sequence_base-0.6.0}/sequence_base/contract/deployments.py +14 -12
- {sequence_base-0.5.0 → sequence_base-0.6.0}/sequence_base/contract/flags.py +2 -1
- {sequence_base-0.5.0 → sequence_base-0.6.0}/sequence_base/contract/observe.py +8 -3
- sequence_base-0.6.0/sequence_base/contract/own_io.py +97 -0
- sequence_base-0.6.0/sequence_base/contract/planner.py +234 -0
- {sequence_base-0.5.0 → sequence_base-0.6.0}/sequence_base/contract/policy.py +38 -25
- {sequence_base-0.5.0 → sequence_base-0.6.0}/sequence_base/schema_export.py +24 -24
- {sequence_base-0.5.0 → sequence_base-0.6.0}/sequence_base/schemas/ActRequest.json +1 -1
- {sequence_base-0.5.0 → sequence_base-0.6.0}/sequence_base/schemas/ActResponse.json +1 -1
- {sequence_base-0.5.0 → sequence_base-0.6.0}/sequence_base/schemas/ActionChunk.json +1 -1
- {sequence_base-0.5.0 → sequence_base-0.6.0}/sequence_base/schemas/ActionContract.json +1 -1
- {sequence_base-0.5.0 → sequence_base-0.6.0}/sequence_base/schemas/ActionStep.json +1 -1
- {sequence_base-0.5.0 → sequence_base-0.6.0}/sequence_base/schemas/AdapterPair.json +1 -1
- {sequence_base-0.5.0 → sequence_base-0.6.0}/sequence_base/schemas/AddLocal.json +1 -1
- {sequence_base-0.5.0 → sequence_base-0.6.0}/sequence_base/schemas/Autoscaler.json +1 -1
- {sequence_base-0.5.0 → sequence_base-0.6.0}/sequence_base/schemas/BenchmarkRegisterRequest.json +2 -2
- {sequence_base-0.5.0 → sequence_base-0.6.0}/sequence_base/schemas/BenchmarkSpec.json +50 -4
- {sequence_base-0.5.0 → sequence_base-0.6.0}/sequence_base/schemas/BenchmarkStatusResponse.json +1 -1
- {sequence_base-0.5.0 → sequence_base-0.6.0}/sequence_base/schemas/BootstrapResponse.json +1 -1
- {sequence_base-0.5.0 → sequence_base-0.6.0}/sequence_base/schemas/Camera.json +1 -1
- {sequence_base-0.5.0 → sequence_base-0.6.0}/sequence_base/schemas/CodeBundle.json +1 -1
- {sequence_base-0.5.0 → sequence_base-0.6.0}/sequence_base/schemas/Concurrency.json +1 -1
- {sequence_base-0.5.0 → sequence_base-0.6.0}/sequence_base/schemas/ConnectRequest.json +1 -27
- sequence_base-0.6.0/sequence_base/schemas/ConnectResponse.json +204 -0
- {sequence_base-0.5.0 → sequence_base-0.6.0}/sequence_base/schemas/DeploymentAccepted.json +1 -1
- sequence_base-0.6.0/sequence_base/schemas/DeploymentPolicy.json +332 -0
- {sequence_base-0.5.0 → sequence_base-0.6.0}/sequence_base/schemas/DeploymentRequest.json +28 -300
- {sequence_base-0.5.0 → sequence_base-0.6.0}/sequence_base/schemas/DeploymentStatusResponse.json +1 -1
- {sequence_base-0.5.0 → sequence_base-0.6.0}/sequence_base/schemas/ErrorBody.json +1 -1
- {sequence_base-0.5.0 → sequence_base-0.6.0}/sequence_base/schemas/ErrorCodes.json +1 -1
- {sequence_base-0.5.0 → sequence_base-0.6.0}/sequence_base/schemas/Estimate.json +1 -1
- {sequence_base-0.5.0 → sequence_base-0.6.0}/sequence_base/schemas/EvalDryRunResponse.json +1 -1
- {sequence_base-0.5.0 → sequence_base-0.6.0}/sequence_base/schemas/EvalReport.json +1 -1
- {sequence_base-0.5.0 → sequence_base-0.6.0}/sequence_base/schemas/EvalRequest.json +1 -1
- {sequence_base-0.5.0 → sequence_base-0.6.0}/sequence_base/schemas/EvalResponse.json +1 -1
- {sequence_base-0.5.0 → sequence_base-0.6.0}/sequence_base/schemas/ImageFrame.json +1 -1
- {sequence_base-0.5.0 → sequence_base-0.6.0}/sequence_base/schemas/ImageSpec.json +1 -1
- {sequence_base-0.5.0 → sequence_base-0.6.0}/sequence_base/schemas/ImageStepAddLocal.json +1 -1
- {sequence_base-0.5.0 → sequence_base-0.6.0}/sequence_base/schemas/ImageStepApt.json +1 -1
- {sequence_base-0.5.0 → sequence_base-0.6.0}/sequence_base/schemas/ImageStepEnv.json +1 -1
- {sequence_base-0.5.0 → sequence_base-0.6.0}/sequence_base/schemas/ImageStepProject.json +1 -1
- {sequence_base-0.5.0 → sequence_base-0.6.0}/sequence_base/schemas/ImageStepRun.json +1 -1
- {sequence_base-0.5.0 → sequence_base-0.6.0}/sequence_base/schemas/ImageStepTemplateFiles.json +1 -1
- {sequence_base-0.5.0 → sequence_base-0.6.0}/sequence_base/schemas/ImageStepUvPip.json +1 -1
- {sequence_base-0.5.0 → sequence_base-0.6.0}/sequence_base/schemas/InferParam.json +1 -1
- {sequence_base-0.5.0 → sequence_base-0.6.0}/sequence_base/schemas/Lease.json +1 -1
- {sequence_base-0.5.0 → sequence_base-0.6.0}/sequence_base/schemas/MetricAgg.json +1 -1
- {sequence_base-0.5.0 → sequence_base-0.6.0}/sequence_base/schemas/Observation.json +1 -1
- {sequence_base-0.5.0 → sequence_base-0.6.0}/sequence_base/schemas/ObservationContract.json +1 -1
- {sequence_base-0.5.0 → sequence_base-0.6.0}/sequence_base/schemas/ObserveRequest.json +1 -1
- {sequence_base-0.5.0 → sequence_base-0.6.0}/sequence_base/schemas/ObserveResponse.json +1 -1
- sequence_base-0.6.0/sequence_base/schemas/PlanAppend.json +13 -0
- {sequence_base-0.5.0 → sequence_base-0.6.0}/sequence_base/schemas/PlanAppended.json +1 -1
- sequence_base-0.6.0/sequence_base/schemas/PlannerMember.json +88 -0
- sequence_base-0.5.0/sequence_base/schemas/GroupObservation.json → sequence_base-0.6.0/sequence_base/schemas/PlannerObservation.json +4 -4
- sequence_base-0.6.0/sequence_base/schemas/PlannerObserve.json +13 -0
- sequence_base-0.6.0/sequence_base/schemas/PlannerObserved.json +16 -0
- sequence_base-0.6.0/sequence_base/schemas/PlannerPlanIn.json +33 -0
- sequence_base-0.6.0/sequence_base/schemas/PlannerPlans.json +66 -0
- sequence_base-0.5.0/sequence_base/schemas/TeamPlansWritten.json → sequence_base-0.6.0/sequence_base/schemas/PlannerPlansWritten.json +3 -3
- sequence_base-0.5.0/sequence_base/schemas/TeamMember.json → sequence_base-0.6.0/sequence_base/schemas/PlannerRobot.json +4 -9
- sequence_base-0.6.0/sequence_base/schemas/PlannerRound.json +200 -0
- sequence_base-0.5.0/sequence_base/schemas/TeamGroupsRequest.json → sequence_base-0.6.0/sequence_base/schemas/PlannerRoundRequest.json +3 -3
- sequence_base-0.6.0/sequence_base/schemas/PlannerSpec.json +22 -0
- {sequence_base-0.5.0 → sequence_base-0.6.0}/sequence_base/schemas/PolicyRegistration.json +21 -293
- {sequence_base-0.5.0 → sequence_base-0.6.0}/sequence_base/schemas/PolicySpec.json +19 -291
- {sequence_base-0.5.0 → sequence_base-0.6.0}/sequence_base/schemas/Project.json +1 -1
- {sequence_base-0.5.0 → sequence_base-0.6.0}/sequence_base/schemas/Proprioception.json +1 -1
- {sequence_base-0.5.0 → sequence_base-0.6.0}/sequence_base/schemas/ReplaySpec.json +1 -1
- {sequence_base-0.5.0 → sequence_base-0.6.0}/sequence_base/schemas/Resources.json +1 -1
- {sequence_base-0.5.0 → sequence_base-0.6.0}/sequence_base/schemas/RolloutResult.json +1 -1
- {sequence_base-0.5.0 → sequence_base-0.6.0}/sequence_base/schemas/SecretCreateRequest.json +1 -1
- {sequence_base-0.5.0 → sequence_base-0.6.0}/sequence_base/schemas/SecretInfo.json +1 -1
- {sequence_base-0.5.0 → sequence_base-0.6.0}/sequence_base/schemas/SecretRef.json +1 -1
- {sequence_base-0.5.0 → sequence_base-0.6.0}/sequence_base/schemas/SeqFlags.json +1 -1
- {sequence_base-0.5.0 → sequence_base-0.6.0}/sequence_base/schemas/SettingsChange.json +1 -1
- {sequence_base-0.5.0 → sequence_base-0.6.0}/sequence_base/schemas/SettingsChanged.json +1 -1
- {sequence_base-0.5.0 → sequence_base-0.6.0}/sequence_base/schemas/SettingsView.json +1 -1
- {sequence_base-0.5.0 → sequence_base-0.6.0}/sequence_base/schemas/Standby.json +1 -1
- {sequence_base-0.5.0 → sequence_base-0.6.0}/sequence_base/schemas/TaskAgg.json +1 -1
- {sequence_base-0.5.0 → sequence_base-0.6.0}/sequence_base/schemas/TemplateFiles.json +1 -1
- {sequence_base-0.5.0 → sequence_base-0.6.0}/sequence_base/schemas/Tensors.json +2 -2
- {sequence_base-0.5.0 → sequence_base-0.6.0}/sequence_base/schemas/Timeouts.json +1 -1
- {sequence_base-0.5.0 → sequence_base-0.6.0}/sequence_base/schemas/UploadTarget.json +1 -1
- {sequence_base-0.5.0 → sequence_base-0.6.0}/sequence_base/schemas/VolumeFile.json +1 -1
- {sequence_base-0.5.0 → sequence_base-0.6.0}/sequence_base/schemas/VolumeInfo.json +1 -1
- {sequence_base-0.5.0 → sequence_base-0.6.0}/sequence_base/schemas/VolumeMount.json +1 -1
- {sequence_base-0.5.0 → sequence_base-0.6.0}/sequence_base/schemas/VolumeRef.json +1 -1
- {sequence_base-0.5.0 → sequence_base-0.6.0}/sequence_base/schemas/VolumeUploadRequest.json +1 -1
- {sequence_base-0.5.0 → sequence_base-0.6.0}/sequence_base/schemas/VolumeUploadResponse.json +1 -1
- {sequence_base-0.5.0 → sequence_base-0.6.0}/sequence_base/schemas/WarmingError.json +1 -1
- sequence_base-0.5.0/sequence_base/construct.py +0 -124
- sequence_base-0.5.0/sequence_base/contract/obs_schedule.py +0 -170
- sequence_base-0.5.0/sequence_base/contract/own_io.py +0 -208
- sequence_base-0.5.0/sequence_base/contract/team.py +0 -185
- sequence_base-0.5.0/sequence_base/schemas/ConnectResponse.json +0 -461
- sequence_base-0.5.0/sequence_base/schemas/DeploymentPolicy.json +0 -604
- sequence_base-0.5.0/sequence_base/schemas/GroupObserve.json +0 -13
- sequence_base-0.5.0/sequence_base/schemas/GroupObserved.json +0 -16
- sequence_base-0.5.0/sequence_base/schemas/Observations.json +0 -264
- sequence_base-0.5.0/sequence_base/schemas/PlanAppend.json +0 -13
- sequence_base-0.5.0/sequence_base/schemas/Stream.json +0 -193
- sequence_base-0.5.0/sequence_base/schemas/TeamGroup.json +0 -120
- sequence_base-0.5.0/sequence_base/schemas/TeamGroups.json +0 -135
- sequence_base-0.5.0/sequence_base/schemas/TeamPlans.json +0 -51
- sequence_base-0.5.0/sequence_base/schemas/TeamSpec.json +0 -22
- {sequence_base-0.5.0 → sequence_base-0.6.0}/.gitignore +0 -0
- {sequence_base-0.5.0 → sequence_base-0.6.0}/README.md +0 -0
- {sequence_base-0.5.0 → sequence_base-0.6.0}/sequence_base/auth.py +0 -0
- {sequence_base-0.5.0 → sequence_base-0.6.0}/sequence_base/by_name.py +0 -0
- {sequence_base-0.5.0 → sequence_base-0.6.0}/sequence_base/codec/__init__.py +0 -0
- {sequence_base-0.5.0 → sequence_base-0.6.0}/sequence_base/codec/image.py +0 -0
- {sequence_base-0.5.0 → sequence_base-0.6.0}/sequence_base/codec/marked.py +0 -0
- {sequence_base-0.5.0 → sequence_base-0.6.0}/sequence_base/constants/__init__.py +0 -0
- {sequence_base-0.5.0 → sequence_base-0.6.0}/sequence_base/constants/cards.py +0 -0
- {sequence_base-0.5.0 → sequence_base-0.6.0}/sequence_base/constants/headers.py +0 -0
- {sequence_base-0.5.0 → sequence_base-0.6.0}/sequence_base/contract/accounts.py +0 -0
- {sequence_base-0.5.0 → sequence_base-0.6.0}/sequence_base/contract/act.py +0 -0
- {sequence_base-0.5.0 → sequence_base-0.6.0}/sequence_base/contract/adapters.py +0 -0
- {sequence_base-0.5.0 → sequence_base-0.6.0}/sequence_base/contract/errors.py +0 -0
- {sequence_base-0.5.0 → sequence_base-0.6.0}/sequence_base/contract/eval.py +0 -0
- {sequence_base-0.5.0 → sequence_base-0.6.0}/sequence_base/contract/io.py +0 -0
- {sequence_base-0.5.0 → sequence_base-0.6.0}/sequence_base/contract/retired.py +0 -0
- {sequence_base-0.5.0 → sequence_base-0.6.0}/sequence_base/contract/settings.py +0 -0
- {sequence_base-0.5.0 → sequence_base-0.6.0}/sequence_base/contract/templates.py +0 -0
- {sequence_base-0.5.0 → sequence_base-0.6.0}/sequence_base/contract/volumes.py +0 -0
- {sequence_base-0.5.0 → sequence_base-0.6.0}/sequence_base/contract/wire.py +0 -0
- {sequence_base-0.5.0 → sequence_base-0.6.0}/sequence_base/episode.py +0 -0
- {sequence_base-0.5.0 → sequence_base-0.6.0}/sequence_base/errors/__init__.py +0 -0
- {sequence_base-0.5.0 → sequence_base-0.6.0}/sequence_base/errors/transport.py +0 -0
- {sequence_base-0.5.0 → sequence_base-0.6.0}/sequence_base/msgpack_io.py +0 -0
- {sequence_base-0.5.0 → sequence_base-0.6.0}/sequence_base/obs/__init__.py +0 -0
- {sequence_base-0.5.0 → sequence_base-0.6.0}/sequence_base/obs/log.py +0 -0
- {sequence_base-0.5.0 → sequence_base-0.6.0}/sequence_base/obs/rid.py +0 -0
- {sequence_base-0.5.0 → sequence_base-0.6.0}/sequence_base/presets/__init__.py +0 -0
- {sequence_base-0.5.0 → sequence_base-0.6.0}/sequence_base/presets/frames.py +0 -0
- {sequence_base-0.5.0 → sequence_base-0.6.0}/sequence_base/presets/robot_arm.py +0 -0
- {sequence_base-0.5.0 → sequence_base-0.6.0}/sequence_base/telemetry/__init__.py +0 -0
- {sequence_base-0.5.0 → sequence_base-0.6.0}/sequence_base/telemetry/events.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: sequence-base
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.6.0
|
|
4
4
|
Summary: Shared foundation for the Sequences platform — wire contract, correlation, error taxonomy, constants. Used by the robot SDK, gateway, and container harness.
|
|
5
5
|
Project-URL: Changelog, https://generalsequences.com/docs/changelog/
|
|
6
6
|
Requires-Python: >=3.10
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "sequence-base"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.6.0"
|
|
4
4
|
description = "Shared foundation for the Sequences platform — wire contract, correlation, error taxonomy, constants. Used by the robot SDK, gateway, and container harness."
|
|
5
5
|
requires-python = ">=3.10"
|
|
6
6
|
dependencies = ["pydantic>=2.0"]
|
|
@@ -86,8 +86,6 @@ from sequence_base.contract import (
|
|
|
86
86
|
PolicyConstructor,
|
|
87
87
|
PolicyRegistration,
|
|
88
88
|
PolicySpec,
|
|
89
|
-
Observations,
|
|
90
|
-
Stream,
|
|
91
89
|
Tensors,
|
|
92
90
|
Proprioception,
|
|
93
91
|
ReplaySpec,
|
|
@@ -145,7 +143,7 @@ __all__ = [
|
|
|
145
143
|
"FROZEN_PATH", "TemplateFiles", "ImageStepTemplateFiles", "Project", "ImageStepProject", "ImageStep", "ImageSpec",
|
|
146
144
|
"VolumeMount", "VolumeRef", "Concurrency", "ActionContract", "Camera", "ObservationContract",
|
|
147
145
|
"PolicySpec", "PolicyRegistration", "PolicyConstructor",
|
|
148
|
-
"
|
|
146
|
+
"Tensors",
|
|
149
147
|
|
|
150
148
|
"MAX_CODE_BUNDLE_BYTES", "CodeBundle",
|
|
151
149
|
"Resources", "Autoscaler", "Timeouts", "DeploymentPolicy", "InferParam", "DeploymentRequest",
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Compressed observe items: what a robot's SDK puts in place of a large array or bytes of its observe data when sending it
|
|
3
|
+
compressed saves more upload time than compressing it costs, and what a worker turns back - losslessly, so every reader
|
|
4
|
+
after the worker is handed the bytes the robot built.
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
In one sentence: the robot's SDK compresses an unmarked numpy array or bytes of 16 KiB or more with zstd level 1 (zlib
|
|
9
|
+
level 1 where it has no zstd) and puts the map below in its place → the worker names contract.observe.ZSTD_FORMAT, so
|
|
10
|
+
only it is sent such maps → as a post comes, before anything keeps it, expand() turns every map back into the array or
|
|
11
|
+
bytes it was (np.frombuffer: read-only, as msgpack_io reads an array) and says how many bytes that added.
|
|
12
|
+
|
|
13
|
+
The map, version 1 (inside the observe body's msgpack; `data` is bin):
|
|
14
|
+
{"__seq__": "compressed", "v": 1, "codec": "zstd" | "zlib", "n": <bytes before>, "data": <compressed>}
|
|
15
|
+
bytes as they were
|
|
16
|
+
... and "type": numpy's string for its dtype, "shape": the array's - an array, its data C-ordered
|
|
17
|
+
`n` is checked against the bytes the data turns back into, and against the most one item may hold (the bytes of an
|
|
18
|
+
episode's observations a worker keeps): a map that says less than it holds is refused rather than decompressed past it.
|
|
19
|
+
|
|
20
|
+
WHY the observation format's reserved key: marked items (codec/marked.py) already take a map with "__seq__", so one key
|
|
21
|
+
is reserved in observe data, not two; a compressed item is never inside a marked one - the SDK compresses after encoding
|
|
22
|
+
its markers and leaves their maps as they are - and expand() does not look inside one.
|
|
23
|
+
WHY decompressed as the post comes, at the parent: the stream, Field data and the standby's saves all read a post's
|
|
24
|
+
frames as it comes, and each must keep the robot's data, not the wire's.
|
|
25
|
+
|
|
26
|
+
wire = compress(frame, "zstd") # a reference encoder (the SDK has its own, with its rule)
|
|
27
|
+
value, grew = expand(post_inputs, limit=64 << 20) # the worker, as a post comes
|
|
28
|
+
"""
|
|
29
|
+
from __future__ import annotations
|
|
30
|
+
|
|
31
|
+
import zlib
|
|
32
|
+
from typing import Any
|
|
33
|
+
|
|
34
|
+
from sequence_base.codec.marked import MARK, is_marked
|
|
35
|
+
|
|
36
|
+
__all__ = ["CODECS", "KIND", "VERSION", "CompressedError", "compress", "expand", "holds", "is_compressed"]
|
|
37
|
+
|
|
38
|
+
KIND = "compressed"
|
|
39
|
+
VERSION = 1
|
|
40
|
+
CODECS = ("zstd", "zlib")
|
|
41
|
+
_LEVEL = 1
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class CompressedError(ValueError):
|
|
45
|
+
"""A compressed item that does not read: an unknown codec, data that does not turn back into the `n` bytes it says,
|
|
46
|
+
an `n` past the limit, or an array's type or shape that does not fit them."""
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def is_compressed(value: Any) -> bool:
|
|
50
|
+
return isinstance(value, dict) and value.get(MARK) == KIND
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _zstd():
|
|
54
|
+
"""zstd's (compress, decompress) - zstandard's, else Python 3.14's compression.zstd. The decompress takes the bytes
|
|
55
|
+
the item says it holds and refuses a frame whose header says other: a header is what sizes the buffer."""
|
|
56
|
+
try:
|
|
57
|
+
import zstandard
|
|
58
|
+
|
|
59
|
+
def unzstd(b: bytes, n: int) -> bytes:
|
|
60
|
+
said = zstandard.frame_content_size(b)
|
|
61
|
+
if said not in (-1, n):
|
|
62
|
+
raise CompressedError(f"a zstd item whose frame says {said} bytes and whose map says {n}")
|
|
63
|
+
return zstandard.ZstdDecompressor().decompress(b, max_output_size=n)
|
|
64
|
+
|
|
65
|
+
return (lambda b: zstandard.ZstdCompressor(level=_LEVEL).compress(b), unzstd)
|
|
66
|
+
except ImportError:
|
|
67
|
+
from compression import zstd
|
|
68
|
+
|
|
69
|
+
def unzstd(b: bytes, n: int) -> bytes:
|
|
70
|
+
said = zstd.get_frame_info(b).decompressed_size
|
|
71
|
+
if said not in (None, n):
|
|
72
|
+
raise CompressedError(f"a zstd item whose frame says {said} bytes and whose map says {n}")
|
|
73
|
+
d = zstd.ZstdDecompressor()
|
|
74
|
+
raw = d.decompress(b, max_length=n)
|
|
75
|
+
if not d.eof:
|
|
76
|
+
raise CompressedError(f"a zstd item that holds more than the {n} bytes it says")
|
|
77
|
+
return raw
|
|
78
|
+
|
|
79
|
+
return (lambda b: zstd.compress(b, level=_LEVEL), unzstd)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def compress(value: Any, codec: str = "zstd") -> dict:
|
|
83
|
+
"""`value` - a numpy array or bytes - as its compressed item: a reference encoder, for tests and the platform's own
|
|
84
|
+
senders; the robot's SDK decides by its own rule what to compress."""
|
|
85
|
+
import numpy as np
|
|
86
|
+
|
|
87
|
+
if isinstance(value, np.ndarray):
|
|
88
|
+
raw = np.ascontiguousarray(value).tobytes()
|
|
89
|
+
extra = {"type": value.dtype.str, "shape": list(value.shape)}
|
|
90
|
+
else:
|
|
91
|
+
raw, extra = bytes(value), {}
|
|
92
|
+
if codec == "zstd":
|
|
93
|
+
data = _zstd()[0](raw)
|
|
94
|
+
elif codec == "zlib":
|
|
95
|
+
data = zlib.compress(raw, _LEVEL)
|
|
96
|
+
else:
|
|
97
|
+
raise CompressedError(f"no codec {codec!r}: {', '.join(CODECS)}")
|
|
98
|
+
return dict([(MARK, KIND), ("v", VERSION), ("codec", codec), ("n", len(raw)), *extra.items(), ("data", data)])
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _raw(item: dict, limit: int) -> bytes:
|
|
102
|
+
codec, n, data = item.get("codec"), item.get("n"), item.get("data")
|
|
103
|
+
if codec not in CODECS:
|
|
104
|
+
raise CompressedError(f"a compressed item of codec {codec!r}: {', '.join(CODECS)} are read")
|
|
105
|
+
if isinstance(n, bool) or not isinstance(n, int) or not 0 <= n <= limit:
|
|
106
|
+
raise CompressedError(f"a compressed item of {n!r} bytes: at most {limit} are taken")
|
|
107
|
+
if not isinstance(data, (bytes, bytearray)):
|
|
108
|
+
raise CompressedError("a compressed item whose data is not bytes")
|
|
109
|
+
try:
|
|
110
|
+
if codec == "zstd":
|
|
111
|
+
raw = _zstd()[1](bytes(data), n)
|
|
112
|
+
else:
|
|
113
|
+
d = zlib.decompressobj()
|
|
114
|
+
raw = d.decompress(bytes(data), n)
|
|
115
|
+
if d.unconsumed_tail or not d.eof:
|
|
116
|
+
raise CompressedError(f"a zlib item that holds more than the {n} bytes it says")
|
|
117
|
+
except CompressedError:
|
|
118
|
+
raise
|
|
119
|
+
except Exception as exc:
|
|
120
|
+
raise CompressedError(f"a {codec} item that does not read: {exc}") from None
|
|
121
|
+
if len(raw) != n:
|
|
122
|
+
raise CompressedError(f"a {codec} item of {len(raw)} bytes that says it holds {n}")
|
|
123
|
+
return raw
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def _value(item: dict, limit: int) -> Any:
|
|
127
|
+
raw = _raw(item, limit)
|
|
128
|
+
if "type" not in item and "shape" not in item:
|
|
129
|
+
return raw
|
|
130
|
+
import numpy as np
|
|
131
|
+
|
|
132
|
+
from sequence_base.msgpack_io import MsgpackError, _dtype
|
|
133
|
+
|
|
134
|
+
shape = item.get("shape")
|
|
135
|
+
if not isinstance(shape, list) or not all(isinstance(d, int) and not isinstance(d, bool) and d >= 0 for d in shape):
|
|
136
|
+
raise CompressedError("a compressed array whose shape is not a list of non-negative ints")
|
|
137
|
+
try:
|
|
138
|
+
dtype = _dtype(item.get("type"))
|
|
139
|
+
except MsgpackError as exc:
|
|
140
|
+
raise CompressedError(f"a compressed array: {exc}") from None
|
|
141
|
+
if dtype.itemsize * int(np.prod(shape, dtype=np.int64)) != len(raw):
|
|
142
|
+
raise CompressedError(f"a compressed {dtype} array of shape {shape} that holds {len(raw)} bytes")
|
|
143
|
+
return np.frombuffer(raw, dtype=dtype).reshape(shape)
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def expand(value: Any, *, limit: int) -> "tuple[Any, int]":
|
|
147
|
+
"""`value` with every compressed item in it - in dicts and lists at any depth, never inside a marked item - turned
|
|
148
|
+
back into the array or bytes it was, the same objects where none is; and how many bytes that added. `limit`: the
|
|
149
|
+
most bytes one item may turn back into. CompressedError for an item that does not read."""
|
|
150
|
+
grew = [0]
|
|
151
|
+
|
|
152
|
+
def walk(v: Any) -> Any:
|
|
153
|
+
if isinstance(v, dict):
|
|
154
|
+
if is_compressed(v):
|
|
155
|
+
out = _value(v, limit)
|
|
156
|
+
grew[0] += max(0, int(v["n"]) - len(v["data"]))
|
|
157
|
+
return out
|
|
158
|
+
if is_marked(v) or not holds(v):
|
|
159
|
+
return v
|
|
160
|
+
return {k: walk(x) for k, x in v.items()}
|
|
161
|
+
if isinstance(v, list):
|
|
162
|
+
return [walk(x) for x in v] if holds(v) else v
|
|
163
|
+
return v
|
|
164
|
+
|
|
165
|
+
return walk(value), grew[0]
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def holds(value: Any) -> bool:
|
|
169
|
+
"""Whether `value` holds a compressed item - in dicts and lists at any depth, not inside a marked item."""
|
|
170
|
+
if isinstance(value, dict):
|
|
171
|
+
return is_compressed(value) or (not is_marked(value) and any(holds(v) for v in value.values()))
|
|
172
|
+
if isinstance(value, list):
|
|
173
|
+
return any(holds(v) for v in value)
|
|
174
|
+
return False
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
"""
|
|
2
|
+
How a class built by its constructor is built - a policy's or a planner's (PolicyRegistration.constructor), a benchmark's
|
|
3
|
+
(BenchmarkSpec.constructor): its constructor - or the factory it names, a classmethod of it or a module's function -
|
|
4
|
+
called with the arguments its decorator was given (init=), once the forms in them are filled in.
|
|
5
|
+
|
|
6
|
+
In one sentence: a value {"$config": path, "$set": {...}} becomes the YAML or JSON file at `path` - from the folder of
|
|
7
|
+
the class's file - read where the class runs, with `$set`'s keys laid over its top level → in every string, a policy's
|
|
8
|
+
"{weights}" becomes the weights directory and "{weights.<part>}" a named part's, a benchmark's "{num_worlds}" how many
|
|
9
|
+
worlds this eval builds and "{policy.<field>}" what the evaluated policy declares (POLICY_FIELDS) - a string that is one
|
|
10
|
+
form alone becomes its value as it is, an int or None, one among other text its value as text → `cls.<make>(*args,
|
|
11
|
+
**kwargs)`, or `function(*args, **kwargs)` for make="package.module:function" - the module imported where the class
|
|
12
|
+
runs - or `cls(*args, **kwargs)` without a factory.
|
|
13
|
+
|
|
14
|
+
WHY a config file is read where the policy runs: it is part of the project (XPolicyLab's deploy.yml beside its
|
|
15
|
+
model.py) and goes up with it, so one copy - the one the policy runs with - is read, and "{weights}" in it means the
|
|
16
|
+
worker's directory.
|
|
17
|
+
|
|
18
|
+
WHY a benchmark's too: its worlds are built after the machine's snapshot by the class's own constructor - a repository's
|
|
19
|
+
env as it is, handed how many worlds and the policy's rate where its own code takes them - where a method of the
|
|
20
|
+
platform's (@seq.setup) built them before.
|
|
21
|
+
|
|
22
|
+
"""
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
import importlib
|
|
26
|
+
import json
|
|
27
|
+
import os
|
|
28
|
+
import re
|
|
29
|
+
from typing import Any
|
|
30
|
+
|
|
31
|
+
__all__ = ["CONFIG_KEY", "FORMS", "POLICY_FIELDS", "SET_KEY", "ConstructError", "build", "config", "expand",
|
|
32
|
+
"is_config", "maker", "read_config"]
|
|
33
|
+
|
|
34
|
+
CONFIG_KEY, SET_KEY = "$config", "$set"
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
POLICY_FIELDS = ("hz", "action_chunk_len", "action_chunk_exec_len")
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
FORMS = re.compile(r"\{(?:weights(?:\.(?P<part>[A-Za-z_][A-Za-z0-9_-]*))?|(?P<worlds>num_worlds)"
|
|
42
|
+
r"|policy\.(?P<field>[A-Za-z_][A-Za-z0-9_]*))\}")
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class ConstructError(ValueError):
|
|
46
|
+
"""Arguments that cannot be filled in: a config file missing, unreadable or of another kind, a weights placeholder
|
|
47
|
+
with no weights - or no such part - to fill it, "{num_worlds}" where no worlds are built, or a policy field no policy
|
|
48
|
+
declares."""
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def config(path: str, **overrides: Any) -> dict:
|
|
52
|
+
"""A config file's contents with `overrides` laid over its top level, as the worker reads it (`seq.config`)."""
|
|
53
|
+
if not isinstance(path, str) or not path:
|
|
54
|
+
raise ConstructError("a config file is a path, from the folder of the class's file")
|
|
55
|
+
if os.path.isabs(path):
|
|
56
|
+
raise ConstructError(f"{path}: give a config file's path from the folder of the class's file - the policy runs "
|
|
57
|
+
"it where the upload puts it")
|
|
58
|
+
return {CONFIG_KEY: path, SET_KEY: dict(overrides)}
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def is_config(value: Any) -> bool:
|
|
62
|
+
return isinstance(value, dict) and set(value) == {CONFIG_KEY, SET_KEY} and isinstance(value[CONFIG_KEY], str)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def read_config(path: str) -> Any:
|
|
66
|
+
"""The YAML (.yml, .yaml) or JSON (.json) file at `path`."""
|
|
67
|
+
kind = os.path.splitext(path)[1].lower()
|
|
68
|
+
if kind not in (".json", ".yml", ".yaml"):
|
|
69
|
+
raise ConstructError(f"{path}: a config file is .json, .yml or .yaml")
|
|
70
|
+
if not os.path.isfile(path):
|
|
71
|
+
raise ConstructError(f"{path}: no such config file")
|
|
72
|
+
with open(path, encoding="utf-8") as f:
|
|
73
|
+
if kind == ".json":
|
|
74
|
+
return json.load(f)
|
|
75
|
+
try:
|
|
76
|
+
import yaml
|
|
77
|
+
except ImportError:
|
|
78
|
+
raise ConstructError(f"{path}: reading YAML needs pyyaml") from None
|
|
79
|
+
return yaml.safe_load(f)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def expand(value: Any, *, base_dir: str, weights: Any = None, worlds: int | None = None, policy: Any = None) -> Any:
|
|
83
|
+
"""`value` with its config files read (from `base_dir`) and its forms filled: a policy's from `weights` - the
|
|
84
|
+
weights directory, or {part: directory} for named parts, or None for a policy without weights - a benchmark's from
|
|
85
|
+
`worlds`, how many worlds this eval builds, and `policy`, what the evaluated policy declares ({field: value}, or an
|
|
86
|
+
object with them; None where none is evaluated, each field None). A string that is one form alone becomes its value
|
|
87
|
+
as it is; one form among other text, its value as text."""
|
|
88
|
+
forms = {"base_dir": base_dir, "weights": weights, "worlds": worlds, "policy": policy}
|
|
89
|
+
if is_config(value):
|
|
90
|
+
loaded = read_config(os.path.join(base_dir, value[CONFIG_KEY]))
|
|
91
|
+
if value[SET_KEY] and not isinstance(loaded, dict):
|
|
92
|
+
raise ConstructError(f"{value[CONFIG_KEY]} holds a {type(loaded).__name__}, which has no keys to set")
|
|
93
|
+
merged = {**loaded, **value[SET_KEY]} if isinstance(loaded, dict) else loaded
|
|
94
|
+
return expand(merged, **forms)
|
|
95
|
+
if isinstance(value, dict):
|
|
96
|
+
return {k: expand(v, **forms) for k, v in value.items()}
|
|
97
|
+
if isinstance(value, (list, tuple)):
|
|
98
|
+
return [expand(v, **forms) for v in value]
|
|
99
|
+
if isinstance(value, str):
|
|
100
|
+
alone = FORMS.fullmatch(value)
|
|
101
|
+
if alone:
|
|
102
|
+
return _filled(alone, weights, worlds, policy)
|
|
103
|
+
return FORMS.sub(lambda m: str(_filled(m, weights, worlds, policy)), value)
|
|
104
|
+
return value
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _filled(form: re.Match, weights: Any, worlds: int | None, policy: Any) -> Any:
|
|
108
|
+
if form.group("worlds"):
|
|
109
|
+
if worlds is None:
|
|
110
|
+
raise ConstructError("{num_worlds} is how many worlds this eval builds, and none is handed: a benchmark of one "
|
|
111
|
+
"world an instance takes none")
|
|
112
|
+
return worlds
|
|
113
|
+
field = form.group("field")
|
|
114
|
+
if field is not None:
|
|
115
|
+
if field not in POLICY_FIELDS:
|
|
116
|
+
raise ConstructError(f"{{policy.{field}}}: a policy declares {', '.join(POLICY_FIELDS)}")
|
|
117
|
+
if policy is None:
|
|
118
|
+
return None
|
|
119
|
+
return policy.get(field) if isinstance(policy, dict) else getattr(policy, field, None)
|
|
120
|
+
return _weights_of(form.group("part"), weights)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _weights_of(part: str | None, weights: Any) -> str:
|
|
124
|
+
if weights is None:
|
|
125
|
+
raise ConstructError("{weights} names the weights directory, and this policy declares no weights=")
|
|
126
|
+
if isinstance(weights, dict):
|
|
127
|
+
if part is None:
|
|
128
|
+
raise ConstructError(f"its weights come in named parts ({', '.join(sorted(weights))}): name one, "
|
|
129
|
+
f"{{weights.<part>}}")
|
|
130
|
+
if part not in weights:
|
|
131
|
+
raise ConstructError(f"{{weights.{part}}}: its weights have no part {part!r} "
|
|
132
|
+
f"({', '.join(sorted(weights))})")
|
|
133
|
+
return str(weights[part])
|
|
134
|
+
if part is not None:
|
|
135
|
+
raise ConstructError(f"{{weights.{part}}} names a part, and its weights come whole: {{weights}}")
|
|
136
|
+
return str(weights)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def build(cls: type, constructor: Any, *, base_dir: str, weights: Any = None, worlds: int | None = None,
|
|
140
|
+
policy: Any = None, params: dict | None = None) -> Any:
|
|
141
|
+
"""The instance `constructor` (a PolicyConstructor) makes of `cls`: `cls.<make>(*args, **kwargs)`, the function
|
|
142
|
+
make="package.module:function" names called so, or `cls(...)` - its arguments filled (expand), and `params`, what
|
|
143
|
+
an eval set of the constructor's other arguments (--param benchmark.NAME=VALUE), laid over them. A keyword argument
|
|
144
|
+
that is "{policy.<field>}" alone is left out while that field is None - no policy evaluated, at a registration
|
|
145
|
+
check, or one that declares none - so the constructor's own default holds, as a benchmark's own code did with what
|
|
146
|
+
@seq.setup was handed before."""
|
|
147
|
+
forms = {"base_dir": base_dir, "weights": weights, "worlds": worlds, "policy": policy}
|
|
148
|
+
args = expand(list(constructor.args), **forms)
|
|
149
|
+
kwargs = {}
|
|
150
|
+
for name, value in dict(constructor.kwargs).items():
|
|
151
|
+
filled = expand(value, **forms)
|
|
152
|
+
alone = FORMS.fullmatch(value) if isinstance(value, str) else None
|
|
153
|
+
if filled is None and alone is not None and alone.group("field") is not None:
|
|
154
|
+
continue
|
|
155
|
+
kwargs[name] = filled
|
|
156
|
+
return maker(cls, constructor.make)(*args, **{**kwargs, **(params or {})})
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def maker(cls: type, make: str | None) -> Any:
|
|
160
|
+
"""What builds the instance: `cls` for no make=, its classmethod or staticmethod `make`, or - make written
|
|
161
|
+
"package.module:function" - that module's function, the module imported (as the policy's code imports it).
|
|
162
|
+
"""
|
|
163
|
+
if not make:
|
|
164
|
+
return cls
|
|
165
|
+
module, colon, name = make.rpartition(":")
|
|
166
|
+
if not colon:
|
|
167
|
+
return getattr(cls, make)
|
|
168
|
+
found = getattr(importlib.import_module(module), name, None)
|
|
169
|
+
if found is None:
|
|
170
|
+
raise ConstructError(f"make={make!r}: {module} has no function {name!r}")
|
|
171
|
+
if not callable(found):
|
|
172
|
+
raise ConstructError(f"make={make!r}: {module}.{name} is not callable ({type(found).__name__})")
|
|
173
|
+
return found
|
|
@@ -45,7 +45,7 @@ from sequence_base.contract.policy import (
|
|
|
45
45
|
)
|
|
46
46
|
|
|
47
47
|
|
|
48
|
-
from sequence_base.contract.own_io import
|
|
48
|
+
from sequence_base.contract.own_io import OBSERVATIONS_GONE, Tensors
|
|
49
49
|
|
|
50
50
|
|
|
51
51
|
from sequence_base.contract.deployments import (
|
|
@@ -124,20 +124,30 @@ from sequence_base.contract.settings import (
|
|
|
124
124
|
)
|
|
125
125
|
|
|
126
126
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
127
|
+
|
|
128
|
+
from sequence_base.contract.planner import (
|
|
129
|
+
KEPT,
|
|
130
|
+
MOST_LAYERS,
|
|
131
|
+
PLANS_MOST,
|
|
132
|
+
STATE_MOST,
|
|
133
|
+
PLAN_ON_A_POLICY,
|
|
134
|
+
PLANNER_ID,
|
|
135
|
+
POLICY_ID,
|
|
136
|
+
ROBOT_ID,
|
|
137
|
+
TEAM_GONE,
|
|
132
138
|
PlanAppend,
|
|
133
139
|
PlanAppended,
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
140
|
+
PlannerMember,
|
|
141
|
+
PlannerObservation,
|
|
142
|
+
PlannerObserve,
|
|
143
|
+
PlannerObserved,
|
|
144
|
+
PlannerPlanIn,
|
|
145
|
+
PlannerPlans,
|
|
146
|
+
PlannerPlansWritten,
|
|
147
|
+
PlannerRobot,
|
|
148
|
+
PlannerRound,
|
|
149
|
+
PlannerRoundRequest,
|
|
150
|
+
PlannerSpec,
|
|
141
151
|
)
|
|
142
152
|
|
|
143
153
|
|
|
@@ -154,7 +164,7 @@ __all__ = [
|
|
|
154
164
|
"TemplateFiles", "ImageStepTemplateFiles", "Project", "ImageStepProject", "FROZEN_PATH", "ImageStep", "ImageSpec",
|
|
155
165
|
"VolumeMount", "VolumeRef", "Concurrency", "ActionContract", "Camera", "ObservationContract",
|
|
156
166
|
"PolicySpec", "PolicyRegistration", "PolicyConstructor",
|
|
157
|
-
"
|
|
167
|
+
"OBSERVATIONS_GONE", "Tensors",
|
|
158
168
|
|
|
159
169
|
"MAX_CODE_BUNDLE_BYTES", "CodeBundle",
|
|
160
170
|
"Resources", "Autoscaler", "Timeouts", "DeploymentPolicy", "InferParam", "DeploymentRequest",
|
|
@@ -174,8 +184,9 @@ __all__ = [
|
|
|
174
184
|
"SETTINGS", "Setting", "SettingsChange", "Applied", "SettingState", "SettingsView", "SettingChanged",
|
|
175
185
|
"SettingsChanged",
|
|
176
186
|
|
|
177
|
-
"
|
|
178
|
-
"
|
|
187
|
+
"KEPT", "MOST_LAYERS", "PLANS_MOST", "STATE_MOST", "PLAN_ON_A_POLICY", "PLANNER_ID", "POLICY_ID", "ROBOT_ID", "TEAM_GONE", "PlannerSpec",
|
|
188
|
+
"PlannerRoundRequest", "PlannerRobot", "PlannerMember", "PlannerObservation", "PlannerPlanIn", "PlannerRound",
|
|
189
|
+
"PlannerPlans", "PlannerPlansWritten", "PlanAppend", "PlanAppended", "PlannerObserve", "PlannerObserved",
|
|
179
190
|
|
|
180
191
|
"TEMPLATE_SCHEME", "parse_template",
|
|
181
192
|
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
"""
|
|
2
|
-
Contract 5 (registration half) - a benchmark is the env you already have, run as it is:
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
do. The eval *run* wire types
|
|
2
|
+
Contract 5 (registration half) - a benchmark is the env you already have, run as it is: its class is built by its own
|
|
3
|
+
constructor after the machine's snapshot (init= its arguments, "{num_worlds}" and "{policy.<field>}" filled), a short class
|
|
4
|
+
of yours marks two methods - @seq.reset starts an episode of a task at a seed, @seq.step plays one action and says what
|
|
5
|
+
came of it - and declares its tasks, its seeds and how long an episode may run. The platform reads none of its
|
|
6
|
+
observations or actions: they go to the policy, and come from it, as a robot's do. The eval *run* wire types
|
|
7
|
+
(RolloutResult, EvalReport, ...) live in contract/eval.py.
|
|
7
8
|
|
|
8
9
|
|
|
9
10
|
"""
|
|
@@ -18,7 +19,8 @@ from sequence_base.constants.limits import MAX_ENVS, TASK_DESCRIPTION_MAX
|
|
|
18
19
|
from sequence_base.contract.accounts import SecretRef
|
|
19
20
|
from sequence_base.contract.deployments import CodeBundle, DeploymentState, InferParam
|
|
20
21
|
from sequence_base.contract.flags import BENCHMARK_BINDABLE, SeqFlags
|
|
21
|
-
from sequence_base.contract.policy import KIND_NAME, ImageSpec, VolumeRef
|
|
22
|
+
from sequence_base.contract.policy import KIND_NAME, ImageSpec, PolicyConstructor, VolumeRef
|
|
23
|
+
from sequence_base.construct import FORMS
|
|
22
24
|
|
|
23
25
|
__all__ = [
|
|
24
26
|
"ReplaySpec",
|
|
@@ -31,6 +33,27 @@ __all__ = [
|
|
|
31
33
|
TASK_NAME = r"^[A-Za-z0-9][A-Za-z0-9._/:-]{0,127}$"
|
|
32
34
|
|
|
33
35
|
|
|
36
|
+
|
|
37
|
+
SETUP_GONE = ("@seq.setup is gone: a benchmark builds its worlds in its constructor, after the machine's snapshot - "
|
|
38
|
+
'init= its arguments, "{num_worlds}" how many worlds this eval builds and "{policy.<field>}" what the '
|
|
39
|
+
"evaluated policy declares; a benchmark registered with @seq.setup is registered again with sequence-ai "
|
|
40
|
+
"0.51.1 or later (seq deploy)")
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _names_worlds(constructor: PolicyConstructor | None) -> bool:
|
|
44
|
+
"""Whether init= hands the constructor "{num_worlds}" anywhere - in a string, at any depth."""
|
|
45
|
+
def walk(value) -> bool:
|
|
46
|
+
if isinstance(value, str):
|
|
47
|
+
return any(m.group("worlds") for m in FORMS.finditer(value))
|
|
48
|
+
if isinstance(value, dict):
|
|
49
|
+
return any(walk(v) for v in value.values())
|
|
50
|
+
if isinstance(value, (list, tuple)):
|
|
51
|
+
return any(walk(v) for v in value)
|
|
52
|
+
return False
|
|
53
|
+
|
|
54
|
+
return constructor is not None and walk([constructor.args, constructor.kwargs])
|
|
55
|
+
|
|
56
|
+
|
|
34
57
|
def _check_tasks(tasks: dict, seeds: list, max_seconds) -> None:
|
|
35
58
|
import re as _re
|
|
36
59
|
|
|
@@ -75,11 +98,11 @@ class ReplaySpec(BaseModel):
|
|
|
75
98
|
|
|
76
99
|
|
|
77
100
|
class BenchmarkSpec(BaseModel):
|
|
78
|
-
"""What `@seq.benchmark(...)` and its marked methods add up to, as an eval runs it. `bindings` names the methods
|
|
79
|
-
RESET and STEP
|
|
80
|
-
stepping `worlds` worlds a call (@seq.concurrent(sizes=[N]) on its step),
|
|
81
|
-
time on the robot's clock, the same for every task or {task: seconds};
|
|
82
|
-
at /assets; `secrets` by name."""
|
|
101
|
+
"""What `@seq.benchmark(...)` and its marked methods add up to, as an eval runs it. `bindings` names the methods,
|
|
102
|
+
RESET and STEP; `constructor` the arguments its class is built with after the machine's snapshot (init=) - which a
|
|
103
|
+
vectorized env, one stepping `worlds` worlds a call (@seq.concurrent(sizes=[N]) on its step), hands how many,
|
|
104
|
+
"{num_worlds}". `max_seconds` is an episode's time on the robot's clock, the same for every task or {task: seconds};
|
|
105
|
+
`assets` are versioned scene volumes mounted at /assets; `secrets` by name."""
|
|
83
106
|
|
|
84
107
|
model_config = ConfigDict(extra="forbid")
|
|
85
108
|
|
|
@@ -89,7 +112,7 @@ class BenchmarkSpec(BaseModel):
|
|
|
89
112
|
seeds: list[int] = Field(..., description="The seeds an episode starts at; trial k of a task runs seeds[k % len]")
|
|
90
113
|
max_seconds: float | dict[str, float] = Field(..., description="An episode's time limit, or {task: seconds}")
|
|
91
114
|
handler_ref: str
|
|
92
|
-
bindings: dict[SeqFlags, str] = Field(..., description="
|
|
115
|
+
bindings: dict[SeqFlags, str] = Field(..., description="RESET / STEP -> method name")
|
|
93
116
|
|
|
94
117
|
|
|
95
118
|
worlds: int | None = Field(default=None, ge=1, le=MAX_ENVS)
|
|
@@ -106,6 +129,10 @@ class BenchmarkSpec(BaseModel):
|
|
|
106
129
|
|
|
107
130
|
engine: str | None = Field(default=None, pattern=KIND_NAME)
|
|
108
131
|
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
constructor: PolicyConstructor | None = None
|
|
135
|
+
|
|
109
136
|
@model_validator(mode="after")
|
|
110
137
|
def _tasks(self) -> "BenchmarkSpec":
|
|
111
138
|
_check_tasks(self.tasks, self.seeds, self.max_seconds)
|
|
@@ -113,15 +140,18 @@ class BenchmarkSpec(BaseModel):
|
|
|
113
140
|
|
|
114
141
|
@model_validator(mode="after")
|
|
115
142
|
def _bindings(self) -> "BenchmarkSpec":
|
|
143
|
+
if SeqFlags.SETUP in self.bindings:
|
|
144
|
+
raise ValueError(SETUP_GONE)
|
|
116
145
|
bad = set(self.bindings) - set(BENCHMARK_BINDABLE)
|
|
117
146
|
if bad:
|
|
118
147
|
names = ", ".join(sorted(f.name or str(f.value) for f in bad))
|
|
119
|
-
raise ValueError(f"a benchmark marks @seq.
|
|
148
|
+
raise ValueError(f"a benchmark marks @seq.reset and @seq.step only; got {names}")
|
|
120
149
|
missing = [f.name for f in (SeqFlags.RESET, SeqFlags.STEP) if f not in self.bindings]
|
|
121
150
|
if missing:
|
|
122
151
|
raise ValueError(f"a benchmark marks @seq.reset and @seq.step; {', '.join(missing).lower()} is missing")
|
|
123
|
-
if self.worlds is not None and
|
|
124
|
-
raise ValueError("a vectorized benchmark (@seq.concurrent on its step) builds its worlds in
|
|
152
|
+
if self.worlds is not None and not _names_worlds(self.constructor):
|
|
153
|
+
raise ValueError("a vectorized benchmark (@seq.concurrent on its step) builds its worlds in its "
|
|
154
|
+
'constructor, handed how many by init= - init={"worlds": "{num_worlds}"}')
|
|
125
155
|
return self
|
|
126
156
|
|
|
127
157
|
@model_validator(mode="after")
|
|
@@ -132,7 +162,7 @@ class BenchmarkSpec(BaseModel):
|
|
|
132
162
|
|
|
133
163
|
class BenchmarkRegisterRequest(BaseModel):
|
|
134
164
|
"""POST /v1/benchmarks - what `seq benchmark register` sends: the declaration, the code, and the parameters its
|
|
135
|
-
|
|
165
|
+
constructor takes with a default beside what init= hands it (each settable by an eval's --param, with its type)."""
|
|
136
166
|
|
|
137
167
|
model_config = ConfigDict(extra="forbid")
|
|
138
168
|
|
|
@@ -172,9 +202,8 @@ class BenchmarkRegisterRequest(BaseModel):
|
|
|
172
202
|
@model_validator(mode="after")
|
|
173
203
|
def _param_names(self) -> "BenchmarkRegisterRequest":
|
|
174
204
|
for name in self.params or {}:
|
|
175
|
-
if not name.isidentifier() or name.startswith("_")
|
|
176
|
-
raise ValueError(f"
|
|
177
|
-
"policy")
|
|
205
|
+
if not name.isidentifier() or name.startswith("_"):
|
|
206
|
+
raise ValueError(f"parameter {name!r} is not a public Python identifier")
|
|
178
207
|
return self
|
|
179
208
|
|
|
180
209
|
@model_validator(mode="after")
|