Source code for embodichain.learning.rl.algo.base

# ----------------------------------------------------------------------------
# Copyright (c) 2021-2026 DexForce Technology Co., Ltd.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ----------------------------------------------------------------------------

from __future__ import annotations

from abc import ABC, abstractmethod
from collections.abc import Iterable
from enum import Enum
from typing import Dict, Generic, TypeVar

import torch

from embodichain.learning.rl.utils import (
    AlgorithmCfg,
    bind_scheduler_horizon,
    build_lr_scheduler,
    build_optimizer,
    coerce_lr_scheduler_cfg,
    coerce_optimizer_cfg,
    scheduler_needs_horizon,
)

__all__ = ["BaseAlgorithm", "RolloutKind"]

RolloutT = TypeVar("RolloutT")


[docs] class RolloutKind(str, Enum): """Rollout semantics required by an algorithm.""" STANDARD = "standard" DIFFERENTIABLE = "differentiable"
[docs] class BaseAlgorithm(ABC, Generic[RolloutT]): """Base class for RL algorithms.""" device: torch.device rollout_kind = RolloutKind.STANDARD optimizer: torch.optim.Optimizer lr_scheduler: torch.optim.lr_scheduler.LRScheduler | None
[docs] @abstractmethod def update(self, rollout: RolloutT) -> Dict[str, float]: """Update policy using collected data and return training losses.""" raise NotImplementedError
def _setup_optimization( self, cfg: AlgorithmCfg, parameters: Iterable[torch.nn.Parameter], ) -> None: cfg.optimizer = coerce_optimizer_cfg(cfg.optimizer) cfg.lr_scheduler = coerce_lr_scheduler_cfg(cfg.lr_scheduler) self._lr_scheduler_cfg = cfg.lr_scheduler self.optimizer = build_optimizer(parameters, cfg.optimizer) self.lr_scheduler = None if self._lr_scheduler_cfg.name is not None and not scheduler_needs_horizon( self._lr_scheduler_cfg ): self.lr_scheduler = build_lr_scheduler( self.optimizer, self._lr_scheduler_cfg )
[docs] def bind_schedule(self, *, total_updates: int) -> None: """Bind horizon-dependent LR schedules from the training budget.""" if total_updates <= 0: raise ValueError("total_updates must be positive.") if not scheduler_needs_horizon(self._lr_scheduler_cfg): return self._lr_scheduler_cfg = bind_scheduler_horizon( self._lr_scheduler_cfg, total_updates ) self.lr_scheduler = build_lr_scheduler(self.optimizer, self._lr_scheduler_cfg)
[docs] def current_learning_rate(self) -> float: return float(self.optimizer.param_groups[0]["lr"])
def _step_scheduler(self) -> None: if self.lr_scheduler is not None: self.lr_scheduler.step()