Source code for isaaclab_tasks.utils.success_monitor
# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md).
# All rights reserved.
#
# SPDX-License-Identifier: BSD-3-Clause
"""Success-rate monitoring shared by task reset strategies."""
from __future__ import annotations
import torch
from isaaclab.utils.configclass import configclass
[docs]
@configclass
class SuccessMonitorCfg:
"""Configuration for :class:`SuccessMonitor`."""
class_type: type[SuccessMonitor] | str = "{DIR}.success_monitor:SuccessMonitor"
"""Monitor implementation, resolved when the environment starts."""
monitored_history_len: int = 10
"""Episodes remembered per slot."""
target_success_rate: float = 0.5
"""Success rate favored by sampling, in ``[0, 1]``."""
kappa: float = 1.0
"""Concentration around :attr:`target_success_rate`; zero is uniform."""
temperature: float = 1.0
"""Sampling-weight temperature, at or above ``1.0``."""
[docs]
class SuccessMonitor:
"""Track recent outcomes per slot and sample within partitioned slot banks."""
[docs]
def __init__(self, cfg: SuccessMonitorCfg, num_partitions: int, partition_size: int, device: str):
self.cfg = cfg
self.num_partitions = num_partitions
self.partition_size = partition_size
self.device = device
num_slots = num_partitions * partition_size
self.success_buf = torch.zeros((num_slots, cfg.monitored_history_len), device=device)
self.success_rate = torch.zeros(num_slots, device=device)
self.success_pointer = torch.zeros(num_slots, device=device, dtype=torch.long)
self.success_size = torch.zeros(num_slots, device=device, dtype=torch.long)
[docs]
def get_success_rate(self) -> torch.Tensor:
"""Return a copy of every slot's measured success rate."""
return self.success_rate.clone()
[docs]
def get_mean_success_rate(self) -> float:
"""Average rates across slots that have recorded outcomes."""
measured = self.success_size > 0
return float(self.success_rate[measured].mean()) if bool(measured.any()) else 0.0
[docs]
def success_update(self, slot_ids: torch.Tensor, success: torch.Tensor):
"""Append outcomes to their slots' ring buffers and update success rates."""
if len(slot_ids) == 0:
return
history = self.cfg.monitored_history_len
order = torch.argsort(slot_ids, stable=True)
ordered_slots = slot_ids[order]
unique_slots, counts = torch.unique_consecutive(ordered_slots, return_counts=True)
starts = counts.cumsum(0) - counts
offset = torch.arange(len(ordered_slots), device=self.device) - starts.repeat_interleave(counts)
offset -= (counts - history).clamp(min=0).repeat_interleave(counts)
kept = offset >= 0
slots = ordered_slots[kept]
positions = (self.success_pointer[slots] + offset[kept]) % history
self.success_buf[slots, positions] = success[order][kept].to(dtype=self.success_buf.dtype)
written = counts.clamp(max=history)
self.success_pointer[unique_slots] = (self.success_pointer[unique_slots] + written) % history
self.success_size[unique_slots] = (self.success_size[unique_slots] + written).clamp(max=history)
self.success_rate[:] = self.success_buf.sum(dim=1) / self.success_size.clamp(min=1)
[docs]
def target_weights(self) -> torch.Tensor:
"""Return unnormalized slot weights peaking at the target success rate."""
target = min(max(self.cfg.target_success_rate, 0.0), 1.0)
kappa = max(self.cfg.kappa, 0.0)
a = 1.0 + kappa * target
b = 1.0 + kappa * (1.0 - target)
eps = 1e-4
rate = self.success_rate
weights = ((rate + eps).pow(a - 1.0) * (1.0 - rate + eps).pow(b - 1.0)).clamp_min(eps)
return weights.pow(1.0 / max(self.cfg.temperature, 1.0))
[docs]
def sample_by_target_rate(self, partition_ids: torch.Tensor) -> torch.Tensor:
"""Draw one slot from each requested partition."""
weights = self.target_weights().view(self.num_partitions, self.partition_size)
slots = torch.multinomial(weights[partition_ids], 1).view(-1)
return partition_ids * self.partition_size + slots