Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 7 additions & 3 deletions docs/concepts/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -263,13 +263,17 @@ class DatasetAdapter(Protocol):

**Current commands:**
- `mindact config-check` - Validate YAML configuration
- `mindact doctor` - Report core and optional integration availability
- `mindact eval --runner fake` - Run the dependency-free evaluation smoke path
- `mindact manifest show` - Display experiment manifest

**Planned commands:**
- `mindact train` - Run training loop
- `mindact eval` - Evaluate checkpoint
- `mindact train` - Run training orchestration
- `mindact eval --runner libero` - Evaluate a policy in a supported simulator
- `mindact datasets list` - List available datasets
- `mindact policies list` - List available policies
- `mindact manifest show` - Display experiment manifest
- `mindact compare` - Compare provenance and metrics across runs
- `mindact manifest diff` - Display manifest differences

## Error Handling

Expand Down
6 changes: 6 additions & 0 deletions docs/getting-started/installation.md
Original file line number Diff line number Diff line change
Expand Up @@ -126,9 +126,15 @@ Test your installation:
# Check CLI is available
mindact --help

# Check the local installation and optional integrations
mindact doctor

# Validate example configuration
mindact config-check configs/experiments/libero-baseline.yaml

# Run the dependency-free smoke evaluator
mindact eval configs/experiments/libero-baseline.yaml --runner fake --episodes 1

# Run unit tests (no optional deps required)
pytest tests/unit/ -v

Expand Down
33 changes: 33 additions & 0 deletions docs/guides/developer-tools.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# Developer tools

## `mindact doctor`

Run the non-invasive environment check before attempting an optional integration:

```bash
mindact doctor
```

The command reports the MindAct and Python versions, platform, and whether the
optional PyTorch, Hugging Face Hub, LeRobot, and LIBERO modules are importable.
`FOUND` means only that a module can be imported. It does not verify MuJoCo
rendering, device execution, checkpoint access, or a complete simulator setup.
Missing optional packages do not make the dependency-free core unhealthy.

## `mindact manifest show`

Inspect a run's immutable provenance without changing any artifact:

```bash
mindact manifest show outputs/run-001
# or
mindact manifest show outputs/run-001/manifest.json
```

The command prints the run ID, experiment, seed, code revision, dataset,
policy, environment, checkpoint, and evaluation artifact references. It uses
the same manifest parser as the Python API and returns a validation error for a
missing or malformed manifest.

These commands are intentionally useful in a minimal installation and do not
import optional machine-learning or simulator packages.
10 changes: 9 additions & 1 deletion docs/guides/training-and-evaluation.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,15 @@ This makes it possible to replace a storage backend or simulator without changin

## Optional integrations

MindAct does not import LeRobot or LIBERO while importing the core package. Request the integration explicitly:
MindAct does not import LeRobot or LIBERO while importing the core package. Use `mindact doctor` to inspect optional package availability without importing those packages, then request an integration explicitly:

```bash
mindact doctor
```

A `FOUND` result only means that a module is importable; it does not validate MuJoCo rendering, device execution, checkpoint access, or a complete simulator installation.

Request the integration explicitly:

```python
from mindact.integrations.lerobot import load_lerobot
Expand Down
8 changes: 6 additions & 2 deletions src/mindact/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
"""MindAct: reproducible training and evaluation for embodied policies."""

__version__ = "0.1.0"

from mindact.configs import (
ConfigError,
DatasetConfig,
Expand All @@ -9,13 +11,14 @@
PolicyConfig,
TrainingConfig,
)
from mindact.diagnostics import DiagnosticCheck, DoctorReport, run_doctor
from mindact.evaluation import EpisodeRecord, EvaluationResult, EvaluationRunner, Evaluator
from mindact.experiments import ArtifactStore, ExperimentManifest

__version__ = "0.1.0"

__all__ = [
"ArtifactStore",
"DiagnosticCheck",
"DoctorReport",
"EpisodeRecord",
"EvaluationResult",
"EvaluationRunner",
Expand All @@ -29,4 +32,5 @@
"PolicyConfig",
"TrainingConfig",
"__version__",
"run_doctor",
]
52 changes: 51 additions & 1 deletion src/mindact/cli/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,38 @@
from __future__ import annotations

import argparse
import json
import sys
from collections.abc import Sequence
from pathlib import Path

from mindact import __version__
from mindact.configs import ConfigError, ExperimentConfig
from mindact.diagnostics import run_doctor
from mindact.evaluation import EvaluationRunner
from mindact.evaluation.fake import FakeEnvironment, FakePolicy
from mindact.experiments import ExperimentManifest


def _manifest_path(value: str) -> str:
"""Resolve a manifest file or run directory supplied on the CLI."""
path = Path(value)
if path.is_dir():
path /= "manifest.json"
return str(path)


def _print_manifest(path: str) -> int:
"""Print stable identity fields from an experiment manifest."""
manifest = ExperimentManifest.read_json(path)
data = manifest.to_dict()
print(f"run_id: {data['run_id']}")
print(f"experiment: {data['experiment_name']}")
print(f"seed: {data['seed']}")
print(f"code_revision: {data['code_revision'] or '-'}")
for section in ("dataset", "policy", "environment", "checkpoint", "evaluation"):
value = data[section]
print(f"{section}: {json.dumps(value, sort_keys=True)}")
return 0


def build_parser() -> argparse.ArgumentParser:
Expand All @@ -27,6 +52,19 @@ def build_parser() -> argparse.ArgumentParser:
)
config_parser.add_argument("path", help="path to an experiment YAML file")

subparsers.add_parser(
"doctor",
help="check core and optional integration availability",
)

manifest_parser = subparsers.add_parser(
"manifest",
help="inspect experiment provenance",
)
manifest_subparsers = manifest_parser.add_subparsers(dest="manifest_command", required=True)
show_parser = manifest_subparsers.add_parser("show", help="display a manifest")
show_parser.add_argument("path", help="manifest.json or an experiment run directory")

eval_parser = subparsers.add_parser(
"eval",
help="run the dependency-free fake evaluator",
Expand Down Expand Up @@ -69,8 +107,20 @@ def main(argv: Sequence[str] | None = None) -> int:
print(f"valid: {config.name}")
return 0

if args.command == "doctor":
print(run_doctor().render())
return 0

if args.command == "manifest" and args.manifest_command == "show":
try:
return _print_manifest(_manifest_path(args.path))
except (OSError, TypeError, ValueError) as exc:
parser.error(str(exc))

if args.command == "eval":
try:
from mindact.evaluation.fake import FakeEnvironment, FakePolicy

config = _override_config(args)
policy = FakePolicy()
runner = EvaluationRunner(
Expand Down
107 changes: 107 additions & 0 deletions src/mindact/diagnostics.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
"""Environment diagnostics for the MindAct command-line interface."""

from __future__ import annotations

import platform
import sys
from dataclasses import dataclass
from typing import Any

from mindact import __version__
from mindact.utils.imports import is_available

__all__ = ["DiagnosticCheck", "DoctorReport", "run_doctor"]


@dataclass(frozen=True, slots=True, kw_only=True)
class DiagnosticCheck:
"""One stable, human-readable environment diagnostic."""

name: str
status: str
detail: str

def __post_init__(self) -> None:
for field_name in ("name", "status", "detail"):
value = getattr(self, field_name)
if not isinstance(value, str) or not value.strip():
raise ValueError(f"{field_name} must be a non-empty string")

def to_dict(self) -> dict[str, str]:
"""Return a JSON-compatible representation."""
return {"name": self.name, "status": self.status, "detail": self.detail}


@dataclass(frozen=True, slots=True, kw_only=True)
class DoctorReport:
"""Immutable collection of checks produced by :func:`run_doctor`."""

checks: tuple[DiagnosticCheck, ...]

def __post_init__(self) -> None:
if not isinstance(self.checks, tuple) or not all(
isinstance(check, DiagnosticCheck) for check in self.checks
):
raise ValueError("checks must be a tuple of DiagnosticCheck values")

@property
def healthy(self) -> bool:
"""Return whether no diagnostic reports a core/runtime error.

Missing optional integrations are expected in a minimal installation,
while ``INFO`` entries describe scope rather than health.
"""
return all(check.status != "ERROR" for check in self.checks)

def to_dict(self) -> dict[str, Any]:
"""Return a JSON-compatible representation."""
return {"healthy": self.healthy, "checks": [check.to_dict() for check in self.checks]}

def render(self) -> str:
"""Render checks as stable, terminal-friendly text."""
return "\n".join(
f"{check.name:<18} {check.status:<12} {check.detail}" for check in self.checks
)


_OPTIONAL_PACKAGES: tuple[tuple[str, str, str], ...] = (
("torch", "PyTorch", 'pip install "mindact[torch]"'),
("huggingface_hub", "Hugging Face Hub", 'pip install "mindact[hub]"'),
("lerobot", "LeRobot", 'pip install "mindact[lerobot]"'),
("libero", "LIBERO", 'pip install "mindact[libero]"'),
)


def run_doctor() -> DoctorReport:
"""Collect non-invasive diagnostics without importing optional packages.

Package availability is reported separately from runtime validation. In
particular, an ``OK`` package check does not claim that a simulator,
renderer, device, or checkpoint is usable.
"""
checks = [
DiagnosticCheck(name="MindAct", status="OK", detail=__version__),
DiagnosticCheck(name="Python", status="OK", detail=platform.python_version()),
DiagnosticCheck(name="Platform", status="INFO", detail=sys.platform),
]
for module_name, display_name, install_hint in _OPTIONAL_PACKAGES:
if is_available(module_name):
checks.append(
DiagnosticCheck(name=display_name, status="FOUND", detail="importable module found")
)
else:
checks.append(
DiagnosticCheck(
name=display_name,
status="MISSING",
detail=f"not installed; install with {install_hint}",
)
)
checks.append(
DiagnosticCheck(
name="Scope",
status="INFO",
detail="package checks only; renderer, device, and checkpoint access are not validated",
)
)
return DoctorReport(checks=tuple(checks))
Loading
Loading