from __future__ import annotations from dataclasses import dataclass, field from typing import Dict, List, Optional import numpy as np @dataclass class TaskConfig: task_id: str name: str classes: List[str] pool_size: int budget: int success_threshold: float rare_classes: List[str] modality: str = "xray" body_part: str = "chest" variable_cost: bool = False base_annotation_cost: int = 1 rare_annotation_cost: int = 2 @dataclass class TaskState: config: TaskConfig pool_embeddings: np.ndarray pool_labels: np.ndarray pool_image_ids: List[str] val_embeddings: np.ndarray val_labels: np.ndarray labeled_indices: List[int] = field(default_factory=list) labeled_labels: List[str] = field(default_factory=list) unlabeled_indices: List[int] = field(default_factory=list) budget_used: int = 0 annotation_cost_spent: int = 0 current_auc: float = 0.5 step_count: int = 0 cumulative_reward: float = 0.0 auc_trajectory: List[float] = field(default_factory=list) classes_discovered: List[str] = field(default_factory=list) def get_labeled_embeddings(self) -> np.ndarray: if not self.labeled_indices: return np.zeros((0, self.pool_embeddings.shape[1]), dtype=np.float32) return self.pool_embeddings[self.labeled_indices] def get_unlabeled_embeddings(self) -> np.ndarray: if not self.unlabeled_indices: return np.zeros((0, self.pool_embeddings.shape[1]), dtype=np.float32) return self.pool_embeddings[self.unlabeled_indices] def get_unlabeled_image_ids(self) -> List[str]: return [self.pool_image_ids[i] for i in self.unlabeled_indices] def find_index_by_image_id(self, image_id: str) -> Optional[int]: try: pool_pos = self.pool_image_ids.index(image_id) if pool_pos in self.unlabeled_indices: return pool_pos return None except ValueError: return None def get_budget_phase(self) -> str: if self.config.budget == 0: return "late" progress = self.budget_used / self.config.budget if progress < 0.3: return "early" if progress < 0.7: return "mid" return "late" def get_class_distribution(self) -> Dict[str, int]: distribution: Dict[str, int] = {cls: 0 for cls in self.config.classes} for lbl in self.labeled_labels: if lbl in distribution: distribution[lbl] += 1 return distribution def get_class_coverage_ratio(self) -> float: discovered = sum(1 for cls in self.config.classes if cls in self.classes_discovered) return discovered / len(self.config.classes) if self.config.classes else 0.0 def compute_annotation_cost(self, label: str) -> int: if not self.config.variable_cost: return self.config.base_annotation_cost if label in self.config.rare_classes: return self.config.rare_annotation_cost return self.config.base_annotation_cost