Source code for interpretune.config.runner

from __future__ import annotations  # see PEP 749, no longer needed when 3.13 reaches EOL
from typing import TYPE_CHECKING, Iterable
from dataclasses import dataclass, field
from pathlib import Path

from interpretune.utils import rank_zero_warn, rank_zero_debug, MisconfigurationException
from interpretune.analysis import AnalysisOp
from interpretune.analysis.cache import get_analysis_cache_dir
from interpretune.config.analysis import AnalysisCfg, AnalysisArtifactCfg, LatentAnalysisTargets
from interpretune.protocol import StrOrPath

if TYPE_CHECKING:
    from interpretune.session import ITSession
    from interpretune.protocol import ITModuleProtocol, ITDataModuleProtocol, LatentAnalysisModuleProtocol


# Standalone functions for analysis initialization
def to_analysis_cfgs(
    analysis_cfgs: "AnalysisCfg | AnalysisOp | Iterable[AnalysisCfg | AnalysisOp] | None",
) -> list[AnalysisCfg]:
    """Convert various input formats to a list of AnalysisCfg objects.

    Args:
        analysis_cfgs: Input that can be:
            - An AnalysisCfg instance
            - An AnalysisOp instance
            - An iterable of AnalysisCfg or AnalysisOp instances
            - None (returns empty list)

    Returns:
        List of standardized AnalysisCfg objects

    Raises:
        ValueError: If the input type is not supported
    """
    processed_cfgs = []

    # Handle None case
    if analysis_cfgs is None:
        return processed_cfgs

    # Handle single AnalysisCfg
    if isinstance(analysis_cfgs, AnalysisCfg):
        processed_cfgs.append(analysis_cfgs)
        return processed_cfgs

    # Handle single AnalysisOp
    if hasattr(analysis_cfgs, "name") and hasattr(analysis_cfgs, "alias"):
        processed_cfgs.append(AnalysisCfg(target_op=analysis_cfgs))  # type: ignore[arg-type]
        return processed_cfgs

    # Handle iterable of AnalysisCfg or AnalysisOp
    try:
        # Check if it's iterable first
        iter(analysis_cfgs)  # type: ignore[call-overload]
        for cfg in analysis_cfgs:  # type: ignore[union-attr]
            if isinstance(cfg, AnalysisCfg):
                processed_cfgs.append(cfg)
            elif hasattr(cfg, "name") and hasattr(cfg, "alias"):  # Check if it's an AnalysisOp
                processed_cfgs.append(AnalysisCfg(target_op=cfg))
            else:
                raise ValueError(f"Unsupported analysis configuration type: {type(cfg)}")
    except TypeError:
        # If analysis_cfgs is not iterable
        raise ValueError(
            f"analysis_cfgs must be an AnalysisCfg, AnalysisOp, or an iterable of these types, "
            f"but got {type(analysis_cfgs)}"
        )

    return processed_cfgs


[docs] def init_analysis_dirs( module: "LatentAnalysisModuleProtocol", cache_dir: str | Path | None = None, op_output_dataset_path: str | Path | None = None, analysis_cfgs: list[AnalysisCfg] | None = None, ) -> tuple[Path, Path]: """Initialize the analysis directories for the given module and analysis configurations. Args: module: The module to set up analysis directories for cache_dir: Optional path to cache directory, will be created if it doesn't exist op_output_dataset_path: Optional path for analysis outputs, will be created if it doesn't exist analysis_cfgs: Optional list of analysis configurations to check for op directories Returns: Tuple of (cache_dir, op_output_dataset_path) as Path objects """ # Setup cache directory using analysis/cache helper which selects a temporary # directory by default and a permanent path under IT_ANALYSIS_CACHE only # when persistent analysisstore caching is enabled. cache_dir = get_analysis_cache_dir(module, explicit_cache_dir=cache_dir) assert isinstance(cache_dir, StrOrPath), "cache_dir must be a str or Path" cache_dir = Path(cache_dir) # Creation is handled by get_analysis_cache_dir for permanent dirs; ensure # temp dirs exist as well. cache_dir.mkdir(exist_ok=True, parents=True) # Setup output dataset path if op_output_dataset_path is None: op_output_dataset_path = module.core_log_dir / "analysis_datasets" # type: ignore[attr-defined] # protocol provides core_log_dir assert isinstance(op_output_dataset_path, StrOrPath), "op_output_dataset_path must be a str or Path" op_output_dataset_path = Path(op_output_dataset_path) op_output_dataset_path.mkdir(exist_ok=True, parents=True) # Check for op in analysis configurations and verify directory is empty if analysis_cfgs: for cfg in analysis_cfgs: if cfg.op is not None: # Accept either AnalysisOp instances or OpWrapper-like objects / simple objects providing a `name` if not (isinstance(cfg.op, AnalysisOp) or hasattr(cfg.op, "name")): raise AssertionError("cfg.op must be an AnalysisOp instance or provide a `name` attribute") op_dir = op_output_dataset_path / cfg.op.name # type: ignore[attr-defined] # validated above to have name if op_dir.exists() and any(op_dir.iterdir()): raise Exception( f"Analysis dataset directory for op '{cfg.op.name}' ({op_dir}) is not empty. " # type: ignore[attr-defined] # validated above to have name "Please delete it or specify a different path." ) return cache_dir, op_output_dataset_path
[docs] def init_analysis_cfgs( module: "LatentAnalysisModuleProtocol", analysis_cfgs: "AnalysisCfg | AnalysisOp | Iterable[AnalysisCfg | AnalysisOp] | None", cache_dir: str | Path | None = None, op_output_dataset_path: str | Path | None = None, latent_analysis_targets: "LatentAnalysisTargets | None" = None, ignore_manual: bool = False, ) -> None: """Initialize analysis configurations for the given module. Args: module: The module to initialize configurations for analysis_cfgs: Analysis configuration(s) to initialize cache_dir: Optional path to cache directory op_output_dataset_path: Optional path for analysis outputs latent_analysis_targets: Optional analysis targets to use ignore_manual: Whether to ignore existing manual analysis steps """ analysis_cfgs = to_analysis_cfgs(analysis_cfgs) # Initialize directories cache_dir, op_output_dataset_path = init_analysis_dirs(module, cache_dir, op_output_dataset_path, analysis_cfgs) # Apply ignore_manual setting if specified if ignore_manual: for cfg in analysis_cfgs: cfg.ignore_manual = True # Apply each analysis configuration to the module, respecting already-applied configs for cfg in analysis_cfgs: if not cfg.applied_to(module): cfg.apply(module, str(cache_dir), str(op_output_dataset_path), latent_analysis_targets)
[docs] @dataclass(kw_only=True) class SessionRunnerCfg: it_session: ITSession | None = None module: ITModuleProtocol | None = None datamodule: ITDataModuleProtocol | None = None limit_train_batches: int = -1 limit_val_batches: int = -1 limit_test_batches: int = -1 max_steps: int = -1 max_epochs: int = -1 def __post_init__(self): if self.it_session is not None: self._session_validation() else: if not all((self.module, self.datamodule)): raise MisconfigurationException( "If not providing `it_session`, must provide both a `datamodule` and `module`" ) def _session_validation(self): if any((self.module, self.datamodule)): rank_zero_warn( "`module`/`datamodule` should only be specified if not providing `it_session`. Attempting to" " use the `module`/`datamodule` handles from `it_session`." ) assert self.it_session is not None self.module = self.it_session.module self.datamodule = self.it_session.datamodule
[docs] @dataclass(kw_only=True) class AnalysisRunnerCfg(SessionRunnerCfg): # Change the field to a private attribute that will store the raw value analysis_cfgs: AnalysisCfg | AnalysisOp | Iterable[AnalysisCfg | AnalysisOp] | None = None limit_analysis_batches: int = -1 cache_dir: str | Path | None = None op_output_dataset_path: str | Path | None = None # Add optional latent_analysis_targets as a fallback latent_analysis_targets: LatentAnalysisTargets | None = None # Add artifact configuration artifact_cfg: AnalysisArtifactCfg = field(default_factory=AnalysisArtifactCfg) # Global override for ignore_manual setting in analysis configs ignore_manual: bool = False def __post_init__(self): super().__post_init__() assert self.it_session is not None and self.it_session.module is not None self.it_session.module.analysis_run_cfg = self # type: ignore[attr-defined] # No need to call _process_analysis_cfgs() as it's now a property if self.analysis_cfgs is None: rank_zero_debug( "No analysis_cfgs provided on runner initialization, expecting one to be passed with" " run_analysis invocation" ) # Convert Path objects to strings if provided if isinstance(self.cache_dir, Path): self.cache_dir = str(self.cache_dir) if isinstance(self.op_output_dataset_path, Path): self.op_output_dataset_path = str(self.op_output_dataset_path) @property def _processed_analysis_cfgs(self) -> list[AnalysisCfg]: """Process and return the analysis_cfgs as a standardized list of AnalysisCfg objects.""" return to_analysis_cfgs(self.analysis_cfgs)