praxis-eval 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.
- praxis_eval/__init__.py +97 -0
- praxis_eval/api.py +21 -0
- praxis_eval/artifacts.py +54 -0
- praxis_eval/contracts.py +87 -0
- praxis_eval/envs/__init__.py +5 -0
- praxis_eval/envs/async_vector_env.py +304 -0
- praxis_eval/envs/builtins.py +169 -0
- praxis_eval/envs/eval_pool.py +59 -0
- praxis_eval/envs/eval_registry.py +169 -0
- praxis_eval/envs/factory.py +318 -0
- praxis_eval/envs/libero/__init__.py +91 -0
- praxis_eval/envs/libero/config.py +231 -0
- praxis_eval/envs/libero/env.py +218 -0
- praxis_eval/envs/libero/eval.py +225 -0
- praxis_eval/envs/libero/output.py +24 -0
- praxis_eval/envs/libero/processor.py +24 -0
- praxis_eval/envs/libero/registration.py +104 -0
- praxis_eval/envs/libero/runtime.py +218 -0
- praxis_eval/envs/libero/spaces.py +118 -0
- praxis_eval/envs/libero/spec.py +76 -0
- praxis_eval/envs/libero/tasks.py +54 -0
- praxis_eval/envs/metaworld/__init__.py +83 -0
- praxis_eval/envs/metaworld/_compat.py +30 -0
- praxis_eval/envs/metaworld/config.py +84 -0
- praxis_eval/envs/metaworld/env.py +217 -0
- praxis_eval/envs/metaworld/eval.py +156 -0
- praxis_eval/envs/metaworld/registration.py +95 -0
- praxis_eval/envs/metaworld/runtime.py +178 -0
- praxis_eval/envs/metaworld/spec.py +26 -0
- praxis_eval/envs/metaworld/tasks.py +167 -0
- praxis_eval/envs/mshab/__init__.py +35 -0
- praxis_eval/envs/mshab/eval.py +457 -0
- praxis_eval/envs/mshab/registration.py +24 -0
- praxis_eval/envs/robocasa/__init__.py +75 -0
- praxis_eval/envs/robocasa/config.py +123 -0
- praxis_eval/envs/robocasa/datasets.py +147 -0
- praxis_eval/envs/robocasa/env.py +646 -0
- praxis_eval/envs/robocasa/eval.py +208 -0
- praxis_eval/envs/robocasa/processor.py +106 -0
- praxis_eval/envs/robocasa/runtime.py +351 -0
- praxis_eval/envs/robocasa/state.py +70 -0
- praxis_eval/envs/robocasa/tasks.py +126 -0
- praxis_eval/envs/robomimic/__init__.py +75 -0
- praxis_eval/envs/robomimic/config.py +112 -0
- praxis_eval/envs/robomimic/env.py +326 -0
- praxis_eval/envs/robomimic/eval.py +221 -0
- praxis_eval/envs/robomimic/processor.py +89 -0
- praxis_eval/envs/robomimic/runtime.py +295 -0
- praxis_eval/envs/robomimic/state.py +42 -0
- praxis_eval/envs/robomimic/tasks.py +124 -0
- praxis_eval/envs/simpler/__init__.py +28 -0
- praxis_eval/envs/simpler/eval.py +362 -0
- praxis_eval/envs/simpler/registration.py +24 -0
- praxis_eval/envs/subprocess_runtime.py +589 -0
- praxis_eval/evaluation/__init__.py +40 -0
- praxis_eval/evaluation/artifacts.py +54 -0
- praxis_eval/evaluation/config.py +127 -0
- praxis_eval/evaluation/overrides.py +127 -0
- praxis_eval/evaluation/sim/__init__.py +15 -0
- praxis_eval/evaluation/sim/adapters.py +165 -0
- praxis_eval/evaluation/sim/diagnostics.py +105 -0
- praxis_eval/evaluation/sim/metrics.py +133 -0
- praxis_eval/evaluation/sim/rollout_compat.py +695 -0
- praxis_eval/evaluation/sim/runner.py +319 -0
- praxis_eval/evaluation/sim/wave_retry.py +245 -0
- praxis_eval/evaluation/watchdog.py +213 -0
- praxis_eval/managed_paths.py +18 -0
- praxis_eval/metrics.py +70 -0
- praxis_eval/policies/__init__.py +18 -0
- praxis_eval/policies/actions.py +47 -0
- praxis_eval/policies/local.py +66 -0
- praxis_eval/policies/remote.py +65 -0
- praxis_eval/processing.py +116 -0
- praxis_eval/registry.py +72 -0
- praxis_eval/scripts/__init__.py +5 -0
- praxis_eval/scripts/_conda_runtime.py +131 -0
- praxis_eval/scripts/_dispatch.py +55 -0
- praxis_eval/scripts/_package_sources.py +126 -0
- praxis_eval/scripts/_verify_common.py +144 -0
- praxis_eval/scripts/setup.py +27 -0
- praxis_eval/scripts/setup_mshab.py +310 -0
- praxis_eval/scripts/setup_robocasa.py +119 -0
- praxis_eval/scripts/setup_simpler.py +305 -0
- praxis_eval/scripts/verify.py +30 -0
- praxis_eval/scripts/verify_libero.py +197 -0
- praxis_eval/scripts/verify_metaworld.py +171 -0
- praxis_eval/scripts/verify_mshab.py +260 -0
- praxis_eval/scripts/verify_robocasa.py +113 -0
- praxis_eval/scripts/verify_robomimic.py +175 -0
- praxis_eval/scripts/verify_simpler.py +103 -0
- praxis_eval/types.py +82 -0
- praxis_eval-0.1.0.dist-info/METADATA +506 -0
- praxis_eval-0.1.0.dist-info/RECORD +98 -0
- praxis_eval-0.1.0.dist-info/WHEEL +5 -0
- praxis_eval-0.1.0.dist-info/entry_points.txt +3 -0
- praxis_eval-0.1.0.dist-info/licenses/LICENSE +181 -0
- praxis_eval-0.1.0.dist-info/licenses/NOTICE +7 -0
- praxis_eval-0.1.0.dist-info/top_level.txt +1 -0
praxis_eval/__init__.py
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2026 Chaoqi Liu
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
|
|
5
|
+
"""Standalone robot-policy evaluation harness."""
|
|
6
|
+
|
|
7
|
+
from praxis_eval.api import evaluate
|
|
8
|
+
from praxis_eval.contracts import ActionSpec, EnvContract, ObservationKey
|
|
9
|
+
from praxis_eval.evaluation.config import (
|
|
10
|
+
env_kwargs_without_type_task,
|
|
11
|
+
env_type_from_cfg,
|
|
12
|
+
optional_env_task,
|
|
13
|
+
optional_env_task_ids,
|
|
14
|
+
require_positive_int,
|
|
15
|
+
resolve_nonnegative_int,
|
|
16
|
+
resolve_optional_positive_timeout_sec,
|
|
17
|
+
resolve_policy_kwargs,
|
|
18
|
+
)
|
|
19
|
+
from praxis_eval.evaluation.watchdog import (
|
|
20
|
+
EvalPhaseWatchdog,
|
|
21
|
+
resolve_phase_watchdog_threshold_sec,
|
|
22
|
+
)
|
|
23
|
+
from praxis_eval.policies.actions import normalize_batched_action
|
|
24
|
+
from praxis_eval.policies.local import LocalPolicy
|
|
25
|
+
from praxis_eval.registry import (
|
|
26
|
+
EvalDriver,
|
|
27
|
+
available_drivers,
|
|
28
|
+
get_driver,
|
|
29
|
+
register_driver,
|
|
30
|
+
)
|
|
31
|
+
from praxis_eval.types import (
|
|
32
|
+
EvalConfig,
|
|
33
|
+
EvalResult,
|
|
34
|
+
EvalRuntimeHooks,
|
|
35
|
+
Observation,
|
|
36
|
+
Policy,
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
__all__ = [
|
|
40
|
+
"ActionSpec",
|
|
41
|
+
"EnvContract",
|
|
42
|
+
"EvalConfig",
|
|
43
|
+
"EvalDriver",
|
|
44
|
+
"EvalPhaseWatchdog",
|
|
45
|
+
"EvalResult",
|
|
46
|
+
"EvalRuntimeHooks",
|
|
47
|
+
"LocalPolicy",
|
|
48
|
+
"Observation",
|
|
49
|
+
"ObservationKey",
|
|
50
|
+
"Policy",
|
|
51
|
+
"RemotePolicy",
|
|
52
|
+
"available_drivers",
|
|
53
|
+
"build_eval_overrides_from_train_config",
|
|
54
|
+
"env_kwargs_without_type_task",
|
|
55
|
+
"env_type_from_cfg",
|
|
56
|
+
"evaluate",
|
|
57
|
+
"get_driver",
|
|
58
|
+
"infer_eval_env_target",
|
|
59
|
+
"normalize_batched_action",
|
|
60
|
+
"optional_env_task",
|
|
61
|
+
"optional_env_task_ids",
|
|
62
|
+
"register_driver",
|
|
63
|
+
"normalize_eval_overrides",
|
|
64
|
+
"require_positive_int",
|
|
65
|
+
"resolve_eval_step_dir",
|
|
66
|
+
"resolve_nonnegative_int",
|
|
67
|
+
"resolve_optional_positive_timeout_sec",
|
|
68
|
+
"resolve_phase_watchdog_threshold_sec",
|
|
69
|
+
"resolve_policy_kwargs",
|
|
70
|
+
]
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def __getattr__(name: str):
|
|
74
|
+
"""Lazily expose optional remote policy support."""
|
|
75
|
+
if name == "RemotePolicy":
|
|
76
|
+
from praxis_eval.policies.remote import RemotePolicy
|
|
77
|
+
|
|
78
|
+
return RemotePolicy
|
|
79
|
+
if name == "infer_eval_env_target":
|
|
80
|
+
from praxis_eval.envs.factory import infer_eval_env_target
|
|
81
|
+
|
|
82
|
+
return infer_eval_env_target
|
|
83
|
+
if name == "resolve_eval_step_dir":
|
|
84
|
+
from praxis_eval.evaluation.artifacts import resolve_eval_step_dir
|
|
85
|
+
|
|
86
|
+
return resolve_eval_step_dir
|
|
87
|
+
if name == "build_eval_overrides_from_train_config":
|
|
88
|
+
from praxis_eval.evaluation.overrides import (
|
|
89
|
+
build_eval_overrides_from_train_config,
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
return build_eval_overrides_from_train_config
|
|
93
|
+
if name == "normalize_eval_overrides":
|
|
94
|
+
from praxis_eval.evaluation.overrides import normalize_eval_overrides
|
|
95
|
+
|
|
96
|
+
return normalize_eval_overrides
|
|
97
|
+
raise AttributeError(name)
|
praxis_eval/api.py
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2026 Chaoqi Liu
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
|
|
5
|
+
"""Top-level evaluation API."""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from praxis_eval.registry import EvalDriver, get_driver
|
|
10
|
+
from praxis_eval.types import EvalConfig, EvalResult, Policy
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def evaluate(
|
|
14
|
+
env: str | EvalDriver,
|
|
15
|
+
*,
|
|
16
|
+
policy: Policy,
|
|
17
|
+
config: EvalConfig,
|
|
18
|
+
) -> EvalResult:
|
|
19
|
+
"""Evaluate ``policy`` with a registered env driver or explicit driver."""
|
|
20
|
+
driver = get_driver(env) if isinstance(env, str) else env
|
|
21
|
+
return driver.evaluate(policy=policy, config=config)
|
praxis_eval/artifacts.py
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2026 Chaoqi Liu
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
|
|
5
|
+
"""Evaluation artifact helpers."""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
from dataclasses import asdict, is_dataclass
|
|
11
|
+
from datetime import datetime, timezone
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import Any
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def resolve_artifact_dirs(output_dir: str | Path) -> tuple[Path, Path]:
|
|
17
|
+
"""Create and return ``(output_dir, media_dir)``."""
|
|
18
|
+
root = Path(output_dir)
|
|
19
|
+
media = root / "media"
|
|
20
|
+
root.mkdir(parents=True, exist_ok=True)
|
|
21
|
+
media.mkdir(parents=True, exist_ok=True)
|
|
22
|
+
return root, media
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def write_results_json(
|
|
26
|
+
result: Any,
|
|
27
|
+
output_dir: str | Path,
|
|
28
|
+
*,
|
|
29
|
+
filename: str = "results.json",
|
|
30
|
+
metadata: dict[str, Any] | None = None,
|
|
31
|
+
) -> Path:
|
|
32
|
+
"""Write an evaluation result payload to disk."""
|
|
33
|
+
root = Path(output_dir)
|
|
34
|
+
root.mkdir(parents=True, exist_ok=True)
|
|
35
|
+
payload = _jsonable(result)
|
|
36
|
+
if not isinstance(payload, dict):
|
|
37
|
+
raise TypeError(f"result must serialize to a dict, got {type(payload)!r}")
|
|
38
|
+
payload["_meta"] = {
|
|
39
|
+
"timestamp_utc": datetime.now(timezone.utc).isoformat(),
|
|
40
|
+
**(metadata or {}),
|
|
41
|
+
}
|
|
42
|
+
path = root / filename
|
|
43
|
+
path.write_text(json.dumps(payload, indent=2, sort_keys=True) + "\n")
|
|
44
|
+
return path
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _jsonable(value: Any) -> Any:
|
|
48
|
+
if is_dataclass(value):
|
|
49
|
+
return _jsonable(asdict(value))
|
|
50
|
+
if isinstance(value, dict):
|
|
51
|
+
return {str(key): _jsonable(item) for key, item in value.items()}
|
|
52
|
+
if isinstance(value, (list, tuple)):
|
|
53
|
+
return [_jsonable(item) for item in value]
|
|
54
|
+
return value
|
praxis_eval/contracts.py
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2026 Chaoqi Liu
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
|
|
5
|
+
"""Documented observation and action contracts for eval environments."""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from dataclasses import dataclass, field
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
import numpy as np
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass(frozen=True)
|
|
16
|
+
class ObservationKey:
|
|
17
|
+
"""One observation key emitted by an environment."""
|
|
18
|
+
|
|
19
|
+
key: str
|
|
20
|
+
dtype: str
|
|
21
|
+
shape: tuple[int | str, ...] | None = None
|
|
22
|
+
description: str = ""
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass(frozen=True)
|
|
26
|
+
class ActionSpec:
|
|
27
|
+
"""Action contract expected by an environment."""
|
|
28
|
+
|
|
29
|
+
shape: tuple[int, ...] | None
|
|
30
|
+
dtype: str = "float32"
|
|
31
|
+
minimum: float | None = None
|
|
32
|
+
maximum: float | None = None
|
|
33
|
+
convention: str = ""
|
|
34
|
+
description: str = ""
|
|
35
|
+
|
|
36
|
+
def validate(self, action: np.ndarray) -> np.ndarray:
|
|
37
|
+
"""Validate and return ``action`` as a numpy array."""
|
|
38
|
+
arr = np.asarray(action)
|
|
39
|
+
if self.shape is not None and tuple(arr.shape) != tuple(self.shape):
|
|
40
|
+
raise ValueError(
|
|
41
|
+
f"Action shape {tuple(arr.shape)} does not match expected {self.shape}."
|
|
42
|
+
)
|
|
43
|
+
expected_dtype = np.dtype(self.dtype)
|
|
44
|
+
if arr.dtype != expected_dtype:
|
|
45
|
+
raise TypeError(
|
|
46
|
+
f"Action dtype {arr.dtype} does not match {expected_dtype}."
|
|
47
|
+
)
|
|
48
|
+
if not np.all(np.isfinite(arr)):
|
|
49
|
+
raise ValueError("Action contains non-finite values.")
|
|
50
|
+
if self.minimum is not None and np.any(arr < self.minimum):
|
|
51
|
+
raise ValueError(f"Action contains values below {self.minimum}.")
|
|
52
|
+
if self.maximum is not None and np.any(arr > self.maximum):
|
|
53
|
+
raise ValueError(f"Action contains values above {self.maximum}.")
|
|
54
|
+
return arr
|
|
55
|
+
|
|
56
|
+
def validate_batch(self, actions: np.ndarray, *, batch_size: int) -> np.ndarray:
|
|
57
|
+
"""Validate and return a batched action array."""
|
|
58
|
+
arr = np.asarray(actions)
|
|
59
|
+
if batch_size < 1:
|
|
60
|
+
raise ValueError("batch_size must be >= 1")
|
|
61
|
+
if self.shape is None:
|
|
62
|
+
if int(arr.shape[0]) != int(batch_size):
|
|
63
|
+
raise ValueError(
|
|
64
|
+
f"Batched action leading dim {arr.shape[0]} does not match {batch_size}."
|
|
65
|
+
)
|
|
66
|
+
for row in arr:
|
|
67
|
+
self.validate(row)
|
|
68
|
+
return arr
|
|
69
|
+
expected = (int(batch_size), *tuple(self.shape))
|
|
70
|
+
if tuple(arr.shape) != expected:
|
|
71
|
+
raise ValueError(
|
|
72
|
+
f"Batched action shape {tuple(arr.shape)} does not match {expected}."
|
|
73
|
+
)
|
|
74
|
+
for row in arr:
|
|
75
|
+
self.validate(row)
|
|
76
|
+
return arr
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
@dataclass(frozen=True)
|
|
80
|
+
class EnvContract:
|
|
81
|
+
"""Observation/action contract for one eval environment family."""
|
|
82
|
+
|
|
83
|
+
env_type: str
|
|
84
|
+
observation_keys: tuple[ObservationKey, ...]
|
|
85
|
+
action: ActionSpec
|
|
86
|
+
notes: str = ""
|
|
87
|
+
metadata: dict[str, Any] = field(default_factory=dict)
|
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2026 Chaoqi Liu
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
|
|
5
|
+
"""Custom AsyncVectorEnv with optional ``dummy_env_fn`` bootstrap."""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import multiprocessing
|
|
10
|
+
import time
|
|
11
|
+
from collections.abc import Callable, Sequence
|
|
12
|
+
from contextlib import suppress
|
|
13
|
+
from typing import Any, cast
|
|
14
|
+
|
|
15
|
+
import gymnasium as gym
|
|
16
|
+
import numpy as np
|
|
17
|
+
from gymnasium import Space
|
|
18
|
+
from gymnasium.core import Env
|
|
19
|
+
from gymnasium.error import AlreadyPendingCallError, CustomSpaceError, NoAsyncCallError
|
|
20
|
+
from gymnasium.vector.async_vector_env import AsyncState, _async_worker
|
|
21
|
+
from gymnasium.vector.utils import (
|
|
22
|
+
CloudpickleWrapper,
|
|
23
|
+
batch_space,
|
|
24
|
+
clear_mpi_env_vars,
|
|
25
|
+
create_empty_array,
|
|
26
|
+
create_shared_memory,
|
|
27
|
+
read_from_shared_memory,
|
|
28
|
+
)
|
|
29
|
+
from gymnasium.vector.vector_env import AutoresetMode
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class AsyncVectorEnvCallTimeout(RuntimeError):
|
|
33
|
+
"""Raised when ``call_each`` times out waiting on one or more lanes."""
|
|
34
|
+
|
|
35
|
+
def __init__(
|
|
36
|
+
self, *, method: str, timeout_sec: float | int, pending_lanes: list[int]
|
|
37
|
+
):
|
|
38
|
+
self.details = {"method": str(method), "pending_lanes": list(pending_lanes)}
|
|
39
|
+
super().__init__(
|
|
40
|
+
"AsyncVectorEnv.call_each timed out after "
|
|
41
|
+
f"{float(timeout_sec):.3f}s for method={method!r}, "
|
|
42
|
+
f"pending_lanes={pending_lanes}."
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class AsyncVectorEnv(gym.vector.AsyncVectorEnv):
|
|
47
|
+
"""Drop-in replacement for ``gymnasium.vector.AsyncVectorEnv`` that
|
|
48
|
+
accepts an optional *dummy_env_fn* used only to read
|
|
49
|
+
observation/action spaces in the parent process.
|
|
50
|
+
|
|
51
|
+
When ``dummy_env_fn`` is provided the parent never calls
|
|
52
|
+
``env_fns[0]()`` — real environment constructors run exclusively
|
|
53
|
+
inside child workers, avoiding EGL/MuJoCo state corruption under
|
|
54
|
+
fork-based multiprocessing.
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
def __init__(
|
|
58
|
+
self,
|
|
59
|
+
env_fns: Sequence[Callable[[], Env]],
|
|
60
|
+
*,
|
|
61
|
+
dummy_env_fn: Callable[[], Env] | None = None,
|
|
62
|
+
shared_memory: bool = True,
|
|
63
|
+
copy: bool = True,
|
|
64
|
+
context: str | None = None,
|
|
65
|
+
daemon: bool = True,
|
|
66
|
+
worker: Callable | None = None,
|
|
67
|
+
observation_mode: str | Space = "same",
|
|
68
|
+
autoreset_mode: str | AutoresetMode = AutoresetMode.NEXT_STEP,
|
|
69
|
+
):
|
|
70
|
+
# ---- store attributes (mirrors gymnasium) ----
|
|
71
|
+
self.env_fns = env_fns
|
|
72
|
+
self.shared_memory = shared_memory
|
|
73
|
+
self.copy = copy
|
|
74
|
+
self.context = context
|
|
75
|
+
self.daemon = daemon
|
|
76
|
+
self.worker = worker
|
|
77
|
+
self.observation_mode = observation_mode
|
|
78
|
+
self.autoreset_mode = (
|
|
79
|
+
autoreset_mode
|
|
80
|
+
if isinstance(autoreset_mode, AutoresetMode)
|
|
81
|
+
else AutoresetMode(autoreset_mode)
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
self.num_envs = len(env_fns)
|
|
85
|
+
|
|
86
|
+
# >>> Only change from gymnasium: use dummy_env_fn when provided <<<
|
|
87
|
+
dummy_env = (dummy_env_fn or env_fns[0])()
|
|
88
|
+
|
|
89
|
+
self.metadata = dummy_env.metadata
|
|
90
|
+
self.metadata["autoreset_mode"] = self.autoreset_mode
|
|
91
|
+
self.render_mode = dummy_env.render_mode
|
|
92
|
+
|
|
93
|
+
self.single_action_space = dummy_env.action_space
|
|
94
|
+
self.action_space = batch_space(self.single_action_space, self.num_envs)
|
|
95
|
+
|
|
96
|
+
if isinstance(observation_mode, tuple) and len(observation_mode) == 2:
|
|
97
|
+
assert isinstance(observation_mode[0], Space)
|
|
98
|
+
assert isinstance(observation_mode[1], Space)
|
|
99
|
+
self.observation_space, self.single_observation_space = observation_mode
|
|
100
|
+
else:
|
|
101
|
+
if observation_mode == "same":
|
|
102
|
+
self.single_observation_space = dummy_env.observation_space
|
|
103
|
+
self.observation_space = batch_space(
|
|
104
|
+
self.single_observation_space, self.num_envs
|
|
105
|
+
)
|
|
106
|
+
elif observation_mode == "different":
|
|
107
|
+
if dummy_env_fn is not None:
|
|
108
|
+
raise ValueError(
|
|
109
|
+
"AsyncVectorEnv cannot infer observation_mode='different' "
|
|
110
|
+
"from real env_fns when dummy_env_fn is set. Pass explicit "
|
|
111
|
+
"(batch_space, single_space) observation spaces instead."
|
|
112
|
+
)
|
|
113
|
+
env_spaces = [env().observation_space for env in self.env_fns]
|
|
114
|
+
self.single_observation_space = env_spaces[0]
|
|
115
|
+
from gymnasium.vector.utils import batch_differing_spaces
|
|
116
|
+
|
|
117
|
+
self.observation_space = batch_differing_spaces(env_spaces)
|
|
118
|
+
else:
|
|
119
|
+
raise ValueError(
|
|
120
|
+
f"Invalid `observation_mode`, expected: 'same' or 'different' "
|
|
121
|
+
f"or tuple of single and batch observation space, actual got {observation_mode}"
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
dummy_env.close()
|
|
125
|
+
del dummy_env
|
|
126
|
+
|
|
127
|
+
# ---- shared memory / observation buffer (identical to gymnasium) ----
|
|
128
|
+
ctx = cast(Any, multiprocessing.get_context(context))
|
|
129
|
+
if self.shared_memory:
|
|
130
|
+
try:
|
|
131
|
+
_obs_buffer = create_shared_memory(
|
|
132
|
+
self.single_observation_space, n=self.num_envs, ctx=ctx
|
|
133
|
+
)
|
|
134
|
+
self.observations = read_from_shared_memory(
|
|
135
|
+
self.single_observation_space, _obs_buffer, n=self.num_envs
|
|
136
|
+
)
|
|
137
|
+
except CustomSpaceError as e:
|
|
138
|
+
raise ValueError(
|
|
139
|
+
"Using `AsyncVector(..., shared_memory=True)` caused an error, "
|
|
140
|
+
"you can disable this feature with `shared_memory=False` however this is slower."
|
|
141
|
+
) from e
|
|
142
|
+
else:
|
|
143
|
+
_obs_buffer = None
|
|
144
|
+
self.observations = create_empty_array(
|
|
145
|
+
self.single_observation_space, n=self.num_envs, fn=np.zeros
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
# ---- spawn worker processes (identical to gymnasium) ----
|
|
149
|
+
self.parent_pipes, self.processes = [], []
|
|
150
|
+
self.error_queue = ctx.Queue()
|
|
151
|
+
target = worker or _async_worker
|
|
152
|
+
with clear_mpi_env_vars():
|
|
153
|
+
for idx, env_fn in enumerate(self.env_fns):
|
|
154
|
+
parent_pipe, child_pipe = ctx.Pipe()
|
|
155
|
+
process = ctx.Process(
|
|
156
|
+
target=target,
|
|
157
|
+
name=f"Worker<{type(self).__name__}>-{idx}",
|
|
158
|
+
args=(
|
|
159
|
+
idx,
|
|
160
|
+
CloudpickleWrapper(env_fn),
|
|
161
|
+
child_pipe,
|
|
162
|
+
parent_pipe,
|
|
163
|
+
_obs_buffer,
|
|
164
|
+
self.error_queue,
|
|
165
|
+
self.autoreset_mode,
|
|
166
|
+
),
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
self.parent_pipes.append(parent_pipe)
|
|
170
|
+
self.processes.append(process)
|
|
171
|
+
|
|
172
|
+
process.daemon = daemon
|
|
173
|
+
process.start()
|
|
174
|
+
child_pipe.close()
|
|
175
|
+
|
|
176
|
+
self._state = AsyncState.DEFAULT
|
|
177
|
+
self._check_spaces()
|
|
178
|
+
|
|
179
|
+
def _abort_on_call_timeout(self) -> None:
|
|
180
|
+
"""Destroy the pool on ``call_each`` timeout.
|
|
181
|
+
|
|
182
|
+
Re-using an AsyncVectorEnv after a timed-out ``call_each`` is unsafe:
|
|
183
|
+
worker processes may still be executing the first call and their
|
|
184
|
+
pending responses would shift the pipe read offsets of any subsequent
|
|
185
|
+
command, surfacing as spurious AttributeError / garbled return values
|
|
186
|
+
long after the original timeout. Closing parent pipes + terminating
|
|
187
|
+
workers makes every subsequent operation raise immediately.
|
|
188
|
+
"""
|
|
189
|
+
with suppress(Exception):
|
|
190
|
+
self.close(terminate=True)
|
|
191
|
+
self._state = AsyncState.DEFAULT
|
|
192
|
+
|
|
193
|
+
def _poll_parent_pipes(self, timeout: int | float | None = None) -> bool:
|
|
194
|
+
"""Poll all parent pipes until timeout, returning ``True`` if all are ready."""
|
|
195
|
+
self._assert_is_running()
|
|
196
|
+
if timeout is None:
|
|
197
|
+
return True
|
|
198
|
+
end_time = time.perf_counter() + float(timeout)
|
|
199
|
+
for pipe in self.parent_pipes:
|
|
200
|
+
remaining = max(end_time - time.perf_counter(), 0.0)
|
|
201
|
+
if pipe is None:
|
|
202
|
+
return False
|
|
203
|
+
if pipe.closed or (not pipe.poll(remaining)):
|
|
204
|
+
return False
|
|
205
|
+
return True
|
|
206
|
+
|
|
207
|
+
def call_each(
|
|
208
|
+
self,
|
|
209
|
+
name: str,
|
|
210
|
+
*,
|
|
211
|
+
args_list: Sequence[Sequence[Any] | Any] | None = None,
|
|
212
|
+
kwargs_list: Sequence[dict[str, Any]] | None = None,
|
|
213
|
+
timeout: float | None = None,
|
|
214
|
+
) -> tuple[Any, ...]:
|
|
215
|
+
"""Call ``name`` on each worker with lane-specific args/kwargs.
|
|
216
|
+
|
|
217
|
+
This is the per-lane companion to ``call``/``call_async`` where each
|
|
218
|
+
worker receives its own argument tuple.
|
|
219
|
+
"""
|
|
220
|
+
self._assert_is_running()
|
|
221
|
+
if self._state != AsyncState.DEFAULT:
|
|
222
|
+
raise AlreadyPendingCallError(
|
|
223
|
+
f"Calling `call_each` while waiting for `{self._state.value}` to complete.",
|
|
224
|
+
str(self._state.value),
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
n_envs = len(self.parent_pipes)
|
|
228
|
+
if args_list is None:
|
|
229
|
+
args_list = [()] * n_envs
|
|
230
|
+
if kwargs_list is None:
|
|
231
|
+
kwargs_list = [{}] * n_envs
|
|
232
|
+
|
|
233
|
+
if len(args_list) != n_envs:
|
|
234
|
+
raise ValueError(
|
|
235
|
+
f"args_list length must be {n_envs}, got {len(args_list)}."
|
|
236
|
+
)
|
|
237
|
+
if len(kwargs_list) != n_envs:
|
|
238
|
+
raise ValueError(
|
|
239
|
+
f"kwargs_list length must be {n_envs}, got {len(kwargs_list)}."
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
normalized_args: list[tuple[Any, ...]] = []
|
|
243
|
+
for lane_args in args_list:
|
|
244
|
+
if isinstance(lane_args, tuple):
|
|
245
|
+
normalized_args.append(lane_args)
|
|
246
|
+
elif isinstance(lane_args, list):
|
|
247
|
+
normalized_args.append(tuple(lane_args))
|
|
248
|
+
else:
|
|
249
|
+
normalized_args.append((lane_args,))
|
|
250
|
+
|
|
251
|
+
for idx, pipe in enumerate(self.parent_pipes):
|
|
252
|
+
lane_kwargs = dict(kwargs_list[idx])
|
|
253
|
+
pipe.send(("_call", (name, normalized_args[idx], lane_kwargs)))
|
|
254
|
+
self._state = AsyncState.WAITING_CALL
|
|
255
|
+
timeout_sec = 0.0 if timeout is None else float(timeout)
|
|
256
|
+
|
|
257
|
+
self._assert_is_running()
|
|
258
|
+
if self._state != AsyncState.WAITING_CALL:
|
|
259
|
+
raise NoAsyncCallError(
|
|
260
|
+
"Calling `call_each` receive path without pending call.",
|
|
261
|
+
AsyncState.WAITING_CALL.value,
|
|
262
|
+
)
|
|
263
|
+
|
|
264
|
+
poll_fn = getattr(self, "_poll_pipe_envs", None)
|
|
265
|
+
ready = (
|
|
266
|
+
bool(poll_fn(timeout))
|
|
267
|
+
if callable(poll_fn)
|
|
268
|
+
else self._poll_parent_pipes(timeout)
|
|
269
|
+
)
|
|
270
|
+
if not ready:
|
|
271
|
+
pending_lanes = [
|
|
272
|
+
lane_idx
|
|
273
|
+
for lane_idx, pipe in enumerate(self.parent_pipes)
|
|
274
|
+
if not pipe.poll(0.0)
|
|
275
|
+
]
|
|
276
|
+
if not pending_lanes:
|
|
277
|
+
pending_lanes = list(range(len(self.parent_pipes)))
|
|
278
|
+
self._abort_on_call_timeout()
|
|
279
|
+
raise AsyncVectorEnvCallTimeout(
|
|
280
|
+
method=name,
|
|
281
|
+
timeout_sec=timeout_sec,
|
|
282
|
+
pending_lanes=pending_lanes,
|
|
283
|
+
)
|
|
284
|
+
|
|
285
|
+
deadline = time.perf_counter() + timeout_sec if timeout is not None else None
|
|
286
|
+
results_with_status: list[tuple[Any, bool]] = []
|
|
287
|
+
for lane_idx, pipe in enumerate(self.parent_pipes):
|
|
288
|
+
if deadline is not None:
|
|
289
|
+
remaining = max(deadline - time.perf_counter(), 0.0)
|
|
290
|
+
if not pipe.poll(remaining):
|
|
291
|
+
pending_lanes = [lane_idx]
|
|
292
|
+
pending_lanes.extend(range(lane_idx + 1, len(self.parent_pipes)))
|
|
293
|
+
self._abort_on_call_timeout()
|
|
294
|
+
raise AsyncVectorEnvCallTimeout(
|
|
295
|
+
method=name,
|
|
296
|
+
timeout_sec=timeout_sec,
|
|
297
|
+
pending_lanes=pending_lanes,
|
|
298
|
+
)
|
|
299
|
+
results_with_status.append(pipe.recv())
|
|
300
|
+
|
|
301
|
+
results, successes = zip(*results_with_status, strict=True)
|
|
302
|
+
self._raise_if_errors(successes)
|
|
303
|
+
self._state = AsyncState.DEFAULT
|
|
304
|
+
return results
|