Source code for qca.mlqca.config

"""Configuration objects for machine-learning-enhanced QCA workflows."""

from __future__ import annotations

from collections.abc import Mapping
from dataclasses import dataclass, field
from types import MappingProxyType
from typing import Any, Literal

MLQCAMode = Literal["conservative", "radical"]
MLQCAEngine = Literal["csqca", "fsqca", "gsqca"]
MLQCAValidation = Literal["paper", "cv", "bootstrap"]


[docs] @dataclass(frozen=True) class MLQCAConfig: """Validated settings shared by future mlQCA workflow stages.""" mode: MLQCAMode = "conservative" engine: MLQCAEngine = "csqca" top_k: int = 10 model_size: int = 4 max_cutoffs_per_condition: int = 3 consistency_cutoff: float = 0.8 frequency_cutoff: int = 1 minimizer: str = "standard" random_state: int = 201 validation: MLQCAValidation = "paper" n_splits: int = 5 n_bootstrap: int = 100 max_models: int = 10_000 n_jobs: int = 1 required_conditions: tuple[str, ...] = () excluded_conditions: tuple[str, ...] = () model_params: Mapping[str, Any] = field(default_factory=dict) def __post_init__(self) -> None: _validate_choice("mode", self.mode, {"conservative", "radical"}) _validate_choice("engine", self.engine, {"csqca", "fsqca", "gsqca"}) _validate_choice("validation", self.validation, {"paper", "cv", "bootstrap"}) _validate_positive_int("top_k", self.top_k) _validate_positive_int("model_size", self.model_size) _validate_positive_int( "max_cutoffs_per_condition", self.max_cutoffs_per_condition ) _validate_positive_int("frequency_cutoff", self.frequency_cutoff) _validate_positive_int("n_splits", self.n_splits, minimum=2) _validate_positive_int("n_bootstrap", self.n_bootstrap) _validate_positive_int("max_models", self.max_models) if not isinstance(self.n_jobs, int) or self.n_jobs == 0 or self.n_jobs < -1: raise ValueError("n_jobs must be -1 or a positive integer.") if self.model_size > self.top_k: raise ValueError("model_size cannot be greater than top_k.") if not 0.0 <= float(self.consistency_cutoff) <= 1.0: raise ValueError("consistency_cutoff must be in [0, 1].") if not str(self.minimizer).strip(): raise ValueError("minimizer must be a non-empty string.") required = _normalize_names("required_conditions", self.required_conditions) excluded = _normalize_names("excluded_conditions", self.excluded_conditions) overlap = sorted(set(required) & set(excluded)) if overlap: raise ValueError( "required_conditions and excluded_conditions overlap: " f"{overlap}" ) object.__setattr__(self, "mode", str(self.mode).lower()) object.__setattr__(self, "engine", str(self.engine).lower()) object.__setattr__(self, "validation", str(self.validation).lower()) object.__setattr__(self, "minimizer", str(self.minimizer).strip()) object.__setattr__(self, "required_conditions", required) object.__setattr__(self, "excluded_conditions", excluded) object.__setattr__( self, "model_params", MappingProxyType(dict(self.model_params)), )
[docs] def to_dict(self) -> dict[str, Any]: """Return a serializable configuration dictionary.""" return { "mode": self.mode, "engine": self.engine, "top_k": self.top_k, "model_size": self.model_size, "max_cutoffs_per_condition": self.max_cutoffs_per_condition, "consistency_cutoff": self.consistency_cutoff, "frequency_cutoff": self.frequency_cutoff, "minimizer": self.minimizer, "random_state": self.random_state, "validation": self.validation, "n_splits": self.n_splits, "n_bootstrap": self.n_bootstrap, "max_models": self.max_models, "n_jobs": self.n_jobs, "required_conditions": list(self.required_conditions), "excluded_conditions": list(self.excluded_conditions), "model_params": dict(self.model_params), }
def _validate_choice(name: str, value: str, available: set[str]) -> None: normalized = str(value).strip().lower() if normalized not in available: choices = ", ".join(sorted(available)) raise ValueError(f"{name} must be one of: {choices}. Got {value!r}.") def _validate_positive_int(name: str, value: int, minimum: int = 1) -> None: if isinstance(value, bool) or not isinstance(value, int) or value < minimum: raise ValueError( f"{name} must be an integer greater than or equal to {minimum}." ) def _normalize_names(name: str, values: tuple[str, ...]) -> tuple[str, ...]: normalized = tuple(str(value).strip() for value in values) if any(not value for value in normalized): raise ValueError(f"{name} cannot contain empty names.") duplicates = sorted({value for value in normalized if normalized.count(value) > 1}) if duplicates: raise ValueError(f"{name} contains duplicate names: {duplicates}") return normalized __all__ = [ "MLQCAConfig", "MLQCAEngine", "MLQCAMode", "MLQCAValidation", ]