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
4 changes: 2 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,9 @@ wheels/
.venv/
venv/

# UV
# uv and ruff
.uv/
.ruff_cache/

# IDE
.claude/
Expand Down Expand Up @@ -87,4 +88,3 @@ temp/

# Docker compose overrides (generated per-worktree)
docker-compose.override.yml

6 changes: 3 additions & 3 deletions environments/_template/env.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
import structlog

from kinitro.environments import get_environment
from kinitro.environments.registry import get_all_environment_ids
from kinitro.environments.registry import get_environments_by_family
from kinitro.rl_interface import Action

logger = structlog.get_logger()
Expand Down Expand Up @@ -69,8 +69,8 @@ async def _call_miner(

async def list_environments(self) -> list[str]:
"""List available environments in this family."""
# TODO: Change "myenv/" to your environment family prefix
return [e for e in get_all_environment_ids() if e.startswith("myenv/")]
# TODO: Change "myenv" to your environment family prefix
return get_environments_by_family("myenv")

async def evaluate(
self,
Expand Down
4 changes: 2 additions & 2 deletions environments/genesis/env.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
# Import from kinitro package (installed in container via PYTHONPATH)
from kinitro.environments import get_environment
from kinitro.environments.base import RoboticsEnvironment
from kinitro.environments.registry import get_all_environment_ids
from kinitro.environments.registry import get_environments_by_family
from kinitro.rl_interface import Action

logger = structlog.get_logger()
Expand Down Expand Up @@ -101,7 +101,7 @@ async def _call_miner(

async def list_environments(self) -> list[str]:
"""List available Genesis environments."""
return [env_id for env_id in get_all_environment_ids() if env_id.startswith("genesis/")]
return get_environments_by_family("genesis")

async def evaluate(
self,
Expand Down
4 changes: 2 additions & 2 deletions environments/metaworld/env.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@

# Import from kinitro package (installed in container via PYTHONPATH)
from kinitro.environments import get_environment
from kinitro.environments.registry import get_all_environment_ids
from kinitro.environments.registry import get_environments_by_family
from kinitro.rl_interface import Action

logger = structlog.get_logger()
Expand Down Expand Up @@ -93,7 +93,7 @@ async def _call_miner(

async def list_environments(self) -> list[str]:
"""List available MetaWorld environments."""
return [env_id for env_id in get_all_environment_ids() if env_id.startswith("metaworld/")]
return get_environments_by_family("metaworld")

async def evaluate(
self,
Expand Down
60 changes: 21 additions & 39 deletions kinitro/api/routes/scores.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,27 +6,20 @@
from kinitro.api.deps import get_session, get_storage
from kinitro.backend.models import (
EvaluationCycle,
EvaluationCycleORM,
MinerScore,
MinerScoreORM,
ScoresResponse,
)
from kinitro.backend.storage import Storage

router = APIRouter(prefix="/v1/scores", tags=["Scores"])


@router.get("/latest", response_model=ScoresResponse)
async def get_latest_scores(
session: AsyncSession = Depends(get_session),
storage: Storage = Depends(get_storage),
):
"""Get scores from the most recent completed evaluation cycle."""
cycle = await storage.get_latest_cycle(session, completed_only=True)
if cycle is None:
raise HTTPException(status_code=404, detail="No completed evaluations yet")

scores_orm = await storage.get_scores_for_cycle(session, cycle.id)

# Convert to response format
def _build_scores_response(
cycle: EvaluationCycleORM, scores_orm: list[MinerScoreORM]
) -> ScoresResponse:
"""Build a ScoresResponse from a cycle ORM object and its scores."""
scores = [
MinerScore(
uid=s.uid,
Expand All @@ -40,7 +33,6 @@ async def get_latest_scores(
for s in scores_orm
]

# Build miner summary
miner_summary: dict[int, dict[str, float]] = {}
for s in scores_orm:
if s.uid not in miner_summary:
Expand All @@ -54,6 +46,20 @@ async def get_latest_scores(
)


@router.get("/latest", response_model=ScoresResponse)
async def get_latest_scores(
session: AsyncSession = Depends(get_session),
storage: Storage = Depends(get_storage),
):
"""Get scores from the most recent completed evaluation cycle."""
cycle = await storage.get_latest_cycle(session, completed_only=True)
if cycle is None:
raise HTTPException(status_code=404, detail="No completed evaluations yet")

scores_orm = await storage.get_scores_for_cycle(session, cycle.id)
return _build_scores_response(cycle, scores_orm)


@router.get("/{cycle_id}", response_model=ScoresResponse)
async def get_scores_for_cycle(
cycle_id: int,
Expand All @@ -66,28 +72,4 @@ async def get_scores_for_cycle(
raise HTTPException(status_code=404, detail=f"Cycle {cycle_id} not found")

scores_orm = await storage.get_scores_for_cycle(session, cycle_id)

scores = [
MinerScore(
uid=s.uid,
hotkey=s.hotkey,
env_id=s.env_id,
success_rate=s.success_rate,
mean_reward=s.mean_reward,
episodes_completed=s.episodes_completed,
episodes_failed=s.episodes_failed,
)
for s in scores_orm
]

miner_summary: dict[int, dict[str, float]] = {}
for s in scores_orm:
if s.uid not in miner_summary:
miner_summary[s.uid] = {}
miner_summary[s.uid][s.env_id] = s.success_rate

return ScoresResponse(
cycle=EvaluationCycle.model_validate(cycle),
scores=scores,
miner_summary=miner_summary,
)
return _build_scores_response(cycle, scores_orm)
65 changes: 30 additions & 35 deletions kinitro/api/routes/weights.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,29 +4,21 @@
from sqlalchemy.ext.asyncio import AsyncSession

from kinitro.api.deps import get_session, get_storage
from kinitro.backend.models import WeightsResponse, WeightsU16
from kinitro.backend.models import (
ComputedWeightsORM,
EvaluationCycleORM,
WeightsResponse,
WeightsU16,
)
from kinitro.backend.storage import Storage

router = APIRouter(prefix="/v1/weights", tags=["Weights"])


@router.get("/latest", response_model=WeightsResponse)
async def get_latest_weights(
session: AsyncSession = Depends(get_session),
storage: Storage = Depends(get_storage),
):
"""
Get the most recently computed weights.

These weights are ready to be submitted to the chain by validators.
"""
weights_orm = await storage.get_latest_weights(session)
if weights_orm is None:
raise HTTPException(status_code=404, detail="No weights available yet")

# Get the associated cycle for metadata
cycle = await storage.get_cycle(session, weights_orm.cycle_id)

def _build_weights_response(
weights_orm: ComputedWeightsORM, cycle: EvaluationCycleORM | None
) -> WeightsResponse:
"""Build a WeightsResponse from a weights ORM object and its cycle."""
return WeightsResponse(
cycle_id=weights_orm.cycle_id,
block_number=weights_orm.block_number,
Expand All @@ -44,12 +36,30 @@ async def get_latest_weights(
)


@router.get("/latest", response_model=WeightsResponse)
async def get_latest_weights(
session: AsyncSession = Depends(get_session),
storage: Storage = Depends(get_storage),
) -> WeightsResponse:
"""
Get the most recently computed weights.

These weights are ready to be submitted to the chain by validators.
"""
weights_orm = await storage.get_latest_weights(session)
if weights_orm is None:
raise HTTPException(status_code=404, detail="No weights available yet")

cycle = await storage.get_cycle(session, weights_orm.cycle_id)
return _build_weights_response(weights_orm, cycle)


@router.get("/{block_number}", response_model=WeightsResponse)
async def get_weights_for_block(
block_number: int,
session: AsyncSession = Depends(get_session),
storage: Storage = Depends(get_storage),
):
) -> WeightsResponse:
"""Get weights computed at a specific block."""
weights_orm = await storage.get_weights_for_block(session, block_number)
if weights_orm is None:
Expand All @@ -59,19 +69,4 @@ async def get_weights_for_block(
)

cycle = await storage.get_cycle(session, weights_orm.cycle_id)

return WeightsResponse(
cycle_id=weights_orm.cycle_id,
block_number=weights_orm.block_number,
timestamp=weights_orm.created_at,
weights={int(k): float(v) for k, v in weights_orm.weights_json.items()},
weights_u16=WeightsU16(
uids=weights_orm.weights_u16_json["uids"],
values=weights_orm.weights_u16_json["values"],
),
metadata={
"n_miners_evaluated": cycle.n_miners if cycle else None,
"n_environments": cycle.n_environments if cycle else None,
"evaluation_duration_seconds": cycle.duration_seconds if cycle else None,
},
)
return _build_weights_response(weights_orm, cycle)
19 changes: 2 additions & 17 deletions kinitro/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,17 +15,9 @@ class NetworkConfig(BaseSettings):
hotkey_name: str = Field(default="default", description="Hotkey name")


class ValidatorConfig(BaseSettings):
class ValidatorConfig(NetworkConfig):
"""Validator-specific configuration."""

model_config = SettingsConfigDict(env_prefix="KINITRO_")

# Network settings (inherited concept)
network: str = Field(default="finney")
netuid: int = Field(default=1)
wallet_name: str = Field(default="default")
hotkey_name: str = Field(default="default")

# Evaluation settings
episodes_per_env: int = Field(
default=50, description="Number of episodes per environment per evaluation cycle"
Expand Down Expand Up @@ -55,16 +47,9 @@ class ValidatorConfig(BaseSettings):
log_level: str = Field(default="INFO", description="Logging level")


class MinerConfig(BaseSettings):
class MinerConfig(NetworkConfig):
"""Miner-specific configuration."""

model_config = SettingsConfigDict(env_prefix="KINITRO_")

network: str = Field(default="finney")
netuid: int = Field(default=1)
wallet_name: str = Field(default="default")
hotkey_name: str = Field(default="default")

# Model settings
huggingface_repo: str | None = Field(default=None, description="HuggingFace model repo")
model_revision: str | None = Field(default=None, description="Model revision/commit SHA")
Expand Down
3 changes: 1 addition & 2 deletions kinitro/environments/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
robotics simulation environments (MetaWorld, Genesis, DM Control, ManiSkill).
"""

from kinitro.environments.base import EpisodeResult, RoboticsEnvironment, TaskConfig
from kinitro.environments.base import RoboticsEnvironment, TaskConfig
from kinitro.environments.registry import (
ENVIRONMENTS,
get_all_environment_ids,
Expand All @@ -16,7 +16,6 @@
__all__ = [
"RoboticsEnvironment",
"TaskConfig",
"EpisodeResult",
"ENVIRONMENTS",
"get_environment",
"get_all_environment_ids",
Expand Down
17 changes: 0 additions & 17 deletions kinitro/environments/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,23 +45,6 @@ def to_dict(self) -> dict[str, Any]:
}


@dataclass
class EpisodeResult:
"""Result of a single episode evaluation."""

success: bool
total_reward: float
timesteps: int
info: dict[str, Any] = field(default_factory=dict)

@property
def efficiency(self) -> float:
"""Reward per timestep (higher is better)."""
if self.timesteps == 0:
return 0.0
return self.total_reward / self.timesteps


class RoboticsEnvironment(ABC):
"""
Abstract base class for all robotics environments.
Expand Down
Loading