Source code for embodichain.lab.sim.motion.workspace.caches.disk_cache

# ----------------------------------------------------------------------------
# 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.
# ----------------------------------------------------------------------------

import gc
import os
from typing import List
import numpy as np
from tqdm import tqdm


from embodichain.lab.sim.motion.workspace.caches.base_cache import BaseCache

from embodichain.utils import logger

all = [
    "DiskCache",
]


[docs] class DiskCache(BaseCache): """Disk-based cache for workspace sampling. Saves pose samples to disk in batches to minimize memory usage. Suitable for large-scale sampling operations. Default cache location: ~/.cache/embodichain/workspace_analyzer/ """
[docs] @staticmethod def get_default_cache_dir(subdir: str = "default") -> str: """Get default cache directory in user's home. Args: subdir: Subdirectory name under workspace_analyzer cache Returns: Path to cache directory: ~/.cache/embodichain/workspace_analyzer/{subdir} """ cache_home = os.path.expanduser("~/.cache") cache_dir = os.path.join( cache_home, "embodichain", "workspace_analyzer", subdir ) return cache_dir
[docs] def __init__( self, save_dir: str | None = None, batch_size: int = 5000, save_threshold: int = 10000000, use_cached: bool = True, ): """Initialize disk cache. Args: save_dir: Directory path for saving batch files. If None, uses ~/.cache/embodichain/workspace_analyzer/default batch_size: Number of samples per batch save_threshold: Number of samples to accumulate before writing to disk use_cached: Whether to use existing cached files if available """ super().__init__(batch_size, save_threshold) # Use default cache dir if not specified if save_dir is None: import time timestamp = time.strftime("%Y%m%d_%H%M%S") save_dir = self.get_default_cache_dir(subdir=f"session_{timestamp}") logger.log_info(f"Using default cache directory: {save_dir}") self.save_dir = save_dir self.use_cached = use_cached self._buffer: List[np.ndarray] = [] self._batch_count = 0 # Create batches directory self._batches_dir = os.path.join(save_dir, "batches") os.makedirs(self._batches_dir, exist_ok=True) # Check for existing cached data if use_cached and self._has_cached_data(): logger.log_info(f"Found existing cached data in {self._batches_dir}")
def _has_cached_data(self) -> bool: """Check if cached batch files exist. Returns: True if cached .npy files exist """ if not os.path.exists(self._batches_dir): return False npy_files = [f for f in os.listdir(self._batches_dir) if f.endswith(".npy")] return len(npy_files) > 0
[docs] def add(self, poses: List[np.ndarray]) -> None: """Add poses to buffer and save to disk when threshold is reached. Args: poses: List of 4x4 transformation matrices """ self._buffer.extend(poses) self._total_processed += len(poses) # Write to disk when buffer reaches threshold if len(self._buffer) >= self.save_threshold: self._save_batch()
def _save_batch(self) -> None: """Save current buffer to disk as a batch file.""" if not self._buffer: return batch_path = os.path.join( self._batches_dir, f"batch_{self._batch_count:04d}.npy" ) np.save(batch_path, np.array(self._buffer)) logger.log_info( f"Saved batch {self._batch_count}: " f"{len(self._buffer)} poses -> {batch_path}" ) self._batch_count += 1 self._buffer.clear() gc.collect()
[docs] def flush(self) -> None: """Flush any remaining data in buffer to disk.""" if self._buffer: self._save_batch()
[docs] def get_all(self) -> List[np.ndarray] | None: """Load and merge all batch files from disk. Returns: List of all cached poses merged from batch files, or None if no data """ # First flush any pending data self.flush() # Get all batch files npy_files = sorted( [f for f in os.listdir(self._batches_dir) if f.endswith(".npy")] ) if not npy_files: return None logger.log_info(f"Loading {len(npy_files)} batch files...") all_poses = [] for npy_file in tqdm(npy_files, desc="Merging batches"): batch_path = os.path.join(self._batches_dir, npy_file) try: batch_data = np.load(batch_path) all_poses.extend(batch_data) except Exception as e: logger.log_warning(f"Error loading {npy_file}: {str(e)}") logger.log_info(f"Loaded {len(all_poses)} total poses") return all_poses if all_poses else None
[docs] def clear(self) -> None: """Clear all cached data and remove batch files.""" self._buffer.clear() self._batch_count = 0 self._total_processed = 0 # Remove all batch files if os.path.exists(self._batches_dir): for file in os.listdir(self._batches_dir): if file.endswith(".npy"): os.remove(os.path.join(self._batches_dir, file)) gc.collect()
[docs] def get_batch_count(self) -> int: """Get number of batches written to disk. Returns: Number of batch files on disk """ if not os.path.exists(self._batches_dir): return 0 return len([f for f in os.listdir(self._batches_dir) if f.endswith(".npy")])