From 477330c7454b891b69da25e108f5333f9d20ecaa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Krzysztof=20G=C5=82owacz?= Date: Fri, 12 Jun 2026 02:05:45 +0200 Subject: [PATCH 01/11] [#26] feat(config): add from_yaml() classmethod to ImpactConfig to expose the interface for recreating ImpactConfig state from the YAML snapshot --- src/impact/config.py | 36 ++++++++++++++++++++++++++++++++++-- 1 file changed, 34 insertions(+), 2 deletions(-) diff --git a/src/impact/config.py b/src/impact/config.py index c3e9b2f..b53cbd8 100644 --- a/src/impact/config.py +++ b/src/impact/config.py @@ -6,6 +6,8 @@ from functools import cached_property from typing import TYPE_CHECKING, Any +import yaml + from impact.dataset import DatasetSize, DatasetType, InteractionType if TYPE_CHECKING: @@ -304,8 +306,6 @@ def write_yaml(self, output_path: Path) -> None: output_path: Target path where the YAML file with the configuration snapshot should be saved. """ - import yaml - output_path.parent.mkdir(parents=True, exist_ok=True) with output_path.open("w", encoding="utf-8") as f: yaml.safe_dump( @@ -315,3 +315,35 @@ def write_yaml(self, output_path: Path) -> None: sort_keys=False, # Prevents from sorting keys alphabetically allow_unicode=True, ) + + @classmethod + def from_yaml(cls, path: Path) -> ImpactConfig: + """Reconstruct an ImpactConfig from a YAML snapshot. + + Args: + path: Path to the YAML file. + + Returns: + Reconstructed ImpactConfig instance. + + Raises: + FileNotFoundError: If the file does not exist. + ValueError: If required sections are missing. + """ + with path.open("r", encoding="utf-8") as f: + data: dict[str, Any] = yaml.safe_load(f) + + return cls( + dataset=DatasetConfig( + size=DatasetSize(data["dataset"]["size"]), + format=DatasetType(data["dataset"]["format"]), + interaction_type=InteractionType(data["dataset"]["interaction_type"]), + ), + session=SessionConfig(**data["session"]), + sequence=SequenceConfig(**data["sequence"]), + gts=GTSConfig(**data["gts"]), + actr=ACTRConfig(**data["actr"]), + sasrec=SASRecConfig(**data["sasrec"]), + eval=EvalConfig(**data["eval"]), + system=SystemConfig(**data["system"]), + ) From 7df748f32da4cc204ff99231ef90b221862caa48 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Krzysztof=20G=C5=82owacz?= Date: Fri, 12 Jun 2026 02:20:52 +0200 Subject: [PATCH 02/11] [#26] feat(config): use config schema version and config hash (SHA-256) to verify reconstructed ImpactConfig integrity and correctness --- src/impact/config.py | 31 +++++++++++++++++++++++++++---- 1 file changed, 27 insertions(+), 4 deletions(-) diff --git a/src/impact/config.py b/src/impact/config.py index b53cbd8..d6ca8b1 100644 --- a/src/impact/config.py +++ b/src/impact/config.py @@ -4,7 +4,7 @@ from dataclasses import asdict, dataclass, field from enum import Enum from functools import cached_property -from typing import TYPE_CHECKING, Any +from typing import TYPE_CHECKING, Any, ClassVar import yaml @@ -187,6 +187,13 @@ class ImpactConfig: file for exact reconstruction of any pipeline artifact. """ + CONFIG_SCHEMA_VERSION: ClassVar[int] = 1 + """Version of the configuration serialization schema. + + Used to validate compatibility when loading ImpactConfig snapshots from YAML. + Increment when the serialized structure changes incompatibly. + """ + dataset: DatasetConfig = field(default_factory=DatasetConfig) """Configuration for the Yambda dataset fetcher.""" @@ -306,10 +313,16 @@ def write_yaml(self, output_path: Path) -> None: output_path: Target path where the YAML file with the configuration snapshot should be saved. """ + data = { + "schema_version": self.CONFIG_SCHEMA_VERSION, + "hash": self.config_hash, + **self.to_dict(), + } output_path.parent.mkdir(parents=True, exist_ok=True) + with output_path.open("w", encoding="utf-8") as f: yaml.safe_dump( - data=self.to_dict(), + data=data, stream=f, indent=2, sort_keys=False, # Prevents from sorting keys alphabetically @@ -328,12 +341,17 @@ def from_yaml(cls, path: Path) -> ImpactConfig: Raises: FileNotFoundError: If the file does not exist. - ValueError: If required sections are missing. + ValueError: If required sections are missing, or the schema version is not + supported, or the configuration checksum does not match. """ with path.open("r", encoding="utf-8") as f: data: dict[str, Any] = yaml.safe_load(f) - return cls( + schema_version = data.pop("schema_version") + if schema_version != cls.CONFIG_SCHEMA_VERSION: + raise ValueError(f"Unsupported config schema version: {schema_version}.") + + config = cls( dataset=DatasetConfig( size=DatasetSize(data["dataset"]["size"]), format=DatasetType(data["dataset"]["format"]), @@ -347,3 +365,8 @@ def from_yaml(cls, path: Path) -> ImpactConfig: eval=EvalConfig(**data["eval"]), system=SystemConfig(**data["system"]), ) + + if config.config_hash != data.pop("hash"): + raise ValueError("Configuration checksum mismatch.") + + return config From 6c4548e6b460c3b27e2a7695be613bbb8ffbfa1c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Krzysztof=20G=C5=82owacz?= Date: Fri, 12 Jun 2026 11:13:12 +0200 Subject: [PATCH 03/11] [#26] feat(utils): add find_sibling_config() function for locating ImpactConfig snapshots saved alongside a model checkpoint --- src/impact/utils.py | 50 ++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 49 insertions(+), 1 deletion(-) diff --git a/src/impact/utils.py b/src/impact/utils.py index 33dd9f0..9856a22 100644 --- a/src/impact/utils.py +++ b/src/impact/utils.py @@ -3,7 +3,7 @@ import math import os import random -from typing import cast +from typing import TYPE_CHECKING, cast import numpy as np import polars as pl @@ -11,6 +11,10 @@ import structlog import torch +if TYPE_CHECKING: + from collections.abc import Sequence + from pathlib import Path + logger = structlog.stdlib.get_logger(__name__) WORKER_SEED_MODULUS: int = 2**32 @@ -57,6 +61,50 @@ def seed_worker(_: int) -> None: # Ignore the argument (worker_id) random.seed(worker_seed) +def find_sibling_config( + checkpoint_path: Path, + config_filename_patterns: Sequence[str] = ( + "impact_config.yaml", + "config.yaml", + "*.yaml", + ), +) -> Path: + """Locate the ImpactConfig YAML saved alongside a model checkpoint. + + Searches `checkpoint_path.parent` for, in order of preference, files matching + each of `config_filename_patterns`. The first pattern that matches exactly one + file wins. + + Args: + checkpoint_path: Path to the model checkpoint whose sibling config is sought. + config_filename_patterns: Glob patterns tried in order. + + Returns: + Path to the matched config file. + + Raises: + FileNotFoundError: If no pattern matches any file. + RuntimeError: If a pattern matches more than one file (ambiguous). + """ + parent = checkpoint_path.parent + for pattern in config_filename_patterns: + matches = sorted(parent.glob(pattern)) + if not matches: + continue + if len(matches) > 1: + raise RuntimeError( + f"Found multiple config candidates matching '{pattern}' in " + f"{parent}: {[m.name for m in matches]}. Expected exactly one." + ) + return matches[0] + + raise FileNotFoundError( + f"No config YAML found next to checkpoint {checkpoint_path.name} in " + f"{parent}. Tried patterns: {', '.join(config_filename_patterns)}. " + "Re-run model training to generate it automatically." + ) + + def estimate_processing_partitions( data: pl.LazyFrame | pl.DataFrame, target_ram_fraction: float = 0.15, From b87c5e1c62b46ae03a94f3145d1420e1b982294b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Krzysztof=20G=C5=82owacz?= Date: Fri, 12 Jun 2026 11:14:49 +0200 Subject: [PATCH 04/11] [#26] docs: improve function-level docstrings in cli.py and config.py modules --- src/impact/cli.py | 11 +++++++++-- src/impact/config.py | 2 +- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/src/impact/cli.py b/src/impact/cli.py index 837f7a6..689a665 100644 --- a/src/impact/cli.py +++ b/src/impact/cli.py @@ -27,8 +27,15 @@ def inject_impact_config(f: Callable[..., Any]) -> Callable[..., Any]: """Inject ImpactConfig fields as Click options and pass the instantiated config. - The decorated function must accept `impact_config: ImpactConfig` as a keyword - argument. + Adds one Click option per ImpactConfig field (grouped by nested config section), + using ImpactConfig's defaults as the option defaults. The decorated function must + accept `impact_config: ImpactConfig` as a keyword argument; it is constructed at + call time from the parsed CLI values. + + Use this for commands that configure a fresh run from scratch (e.g. training, grid + search). For commands operating on an existing checkpoint (e.g. evaluation), use + `inject_config_from_checkpoint` instead, which loads the config that was saved + alongside the checkpoint rather than rebuilding it from CLI flags. """ # Define all click options, matching the defaults from ImpactConfig options = [ diff --git a/src/impact/config.py b/src/impact/config.py index d6ca8b1..8e14f2c 100644 --- a/src/impact/config.py +++ b/src/impact/config.py @@ -331,7 +331,7 @@ def write_yaml(self, output_path: Path) -> None: @classmethod def from_yaml(cls, path: Path) -> ImpactConfig: - """Reconstruct an ImpactConfig from a YAML snapshot. + """Reconstruct an ImpactConfig from a YAML file written by `write_yaml()`. Args: path: Path to the YAML file. From 87b232977c66634abb7af6fd1b2619ba68559b0c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Krzysztof=20G=C5=82owacz?= Date: Fri, 12 Jun 2026 12:51:24 +0200 Subject: [PATCH 05/11] [#26] feat(cli): define (via simple metaprogramming) @inject_impact_config_from_checkpoint() and @inject_impact_config_overrides() decorators to simplify and improve ImpactConfig flow/recreation --- src/impact/cli.py | 154 +++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 151 insertions(+), 3 deletions(-) diff --git a/src/impact/cli.py b/src/impact/cli.py index 689a665..3c21afa 100644 --- a/src/impact/cli.py +++ b/src/impact/cli.py @@ -1,8 +1,10 @@ from __future__ import annotations import dataclasses +from enum import Enum from functools import cached_property, wraps -from typing import TYPE_CHECKING, Any +from pathlib import Path +from typing import TYPE_CHECKING, Any, Literal, get_args, get_origin import click @@ -18,6 +20,7 @@ SystemConfig, ) from impact.dataset import DatasetSize, DatasetType, InteractionType +from impact.utils import find_sibling_config if TYPE_CHECKING: from collections.abc import Callable @@ -345,5 +348,150 @@ class _ConfigSection: kwargs: dict[str, Any] = dataclasses.field(default_factory=dict) @cached_property - def field_names(self) -> set[str]: - return {f.name for f in dataclasses.fields(self.cls)} + def fields(self) -> dict[str, dataclasses.Field[Any]]: + return {f.name: f for f in dataclasses.fields(self.cls)} + + @cached_property + def field_names(self) -> list[str]: + return list(self.fields.keys()) + + +def inject_impact_config_from_checkpoint(f: Callable[..., Any]) -> Callable[..., Any]: + """Add Click option for SASRecNBR checkpoint path and load its sibling YAML config. + + Adds a required `--sasrec-checkpoint-path` option (validated to exist). + The decorated function must accept both of the following keyword arguments, which + this decorator provides: + - `sasrec_checkpoint_path: Path`: path to the checkpoint parsed from the CLI option. + - `impact_config: ImpactConfig`: config loaded via `utils.find_sibling_config()` and + `ImpactConfig.from_yaml()` from the checkpoint's sibling config file. + + Use for commands operating on an existing trained model (e.g., IMPACT evaluation + or plotting). Combine with `cli.inject_impact_config_overrides(...)` to allow CLI + overrides of specific config sections (e.g. eval, actr) on top of the loaded config. + For commands that build a fresh config from CLI flags, use `inject_impact_config` + instead. + """ + options = [ + click.option( + "--sasrec-checkpoint-path", + type=click.Path(exists=True, dir_okay=False, path_type=Path), + required=True, + help="Path to a trained SASRecNBR checkpoint that will be evaluated.", + ), + ] + + # Apply Click options to the function in reverse order to maintain defined ordering + for option in reversed(options): + f = option(f) + + @wraps(f) + def _wrapper(*args: Any, **kwargs: Any) -> Any: + checkpoint_path: Path = kwargs["sasrec_checkpoint_path"] + kwargs["impact_config"] = ImpactConfig.from_yaml( + path=find_sibling_config(checkpoint_path) + ) + return f(*args, **kwargs) + + return _wrapper + + +_OVERRIDE_SECTIONS: dict[str, _ConfigSection] = { + "actr": _ConfigSection(ACTRConfig), + "eval": _ConfigSection(EvalConfig), + "system": _ConfigSection(SystemConfig), +} + + +def inject_impact_config_overrides( + *sections: Literal["actr", "eval", "system"], +) -> Callable[[Callable[..., Any]], Callable[..., Any]]: + """Add CLI options to override fields of selected ImpactConfig sections. + + Args: + sections: Section names to expose as overrides, e.g. "eval" or "actr". Each + field of the corresponding config dataclass becomes a `--{section}-{field}` + option, defaulting to None (meaning "keep the value from loaded config"). + + The decorated function must accept `impact_config: ImpactConfig` as a keyword + argument, populated upstream. Any explicitly-passed override is merged in via + `dataclasses.replace`, leaving other sections (e.g. `sasrec`) untouched. + + Use together with `cli.inject_impact_config_from_checkpoint()` for commands that + load a checkpoint's saved config but need to adjust evaluation/runtime parameters. + """ + + def _decorator(f: Callable[..., Any]) -> Callable[..., Any]: + unsupported = set(sections) - set(_OVERRIDE_SECTIONS.keys()) + if unsupported: + raise ValueError(f"Unsupported override sections: {unsupported}") + + chosen = {s: _OVERRIDE_SECTIONS[s] for s in sections} + options: list[Callable[[Any], Any]] = [] + for section, config_section in chosen.items(): + for field_name, field_def in config_section.fields.items(): + option_name = f"--{section}-{field_name}".replace("_", "-") + multiple = _is_tuple_field(field_def) + options.append( + click.option( + option_name, + type=_click_type_for(field_def), + multiple=multiple, + default=() if multiple else None, + help=f""" + Override {config_section.cls.__name__}.{field_name}. + Defaults to the loaded config's value. + """, + ) + ) + + # Apply Click options to the function in reverse order to maintain ordering + for option in reversed(options): + f = option(f) + + @wraps(f) + def _wrapper(*args: Any, **kwargs: Any) -> Any: + impact_config: ImpactConfig = kwargs["impact_config"] + + for section, config_section in chosen.items(): + overrides: dict[str, Any] = {} + for field_name in config_section.field_names: + kw_key = f"{section}_{field_name}" + value = kwargs.pop(kw_key) + if value in (None, ()): + continue + overrides[field_name] = value + + if overrides: + current_section = getattr(impact_config, section) + impact_config = dataclasses.replace( + impact_config, + **{section: dataclasses.replace(current_section, **overrides)}, + ) + + kwargs["impact_config"] = impact_config + return f(*args, **kwargs) + + return _wrapper + + return _decorator + + +def _is_tuple_field(field_def: dataclasses.Field[Any]) -> bool: + """Return True if the given dataclass field is a tuple type, or False otherwise.""" + return get_origin(field_def.type) is tuple or ( + isinstance(field_def.type, str) and field_def.type.startswith("tuple") + ) + + +def _click_type_for(field_def: dataclasses.Field[Any]) -> Any: + """Map a dataclass field's annotation to a Click parameter type.""" + if _is_tuple_field(field_def): + # tuple[int, ...] -> int, tuple[str, ...] -> str, etc. + inner = get_args(field_def.type)[0] if get_args(field_def.type) else str + return inner + if field_def.type in (int, float, str, bool): + return field_def.type + if isinstance(field_def.type, type) and issubclass(field_def.type, Enum): + return click.Choice([e.value for e in field_def.type]) + return str From 5b15deec892d489a961e3c70020c3a2bd1b03c80 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Krzysztof=20G=C5=82owacz?= Date: Fri, 12 Jun 2026 13:08:02 +0200 Subject: [PATCH 06/11] [#26] feat(cli): fix Python -> Click type conversion, simplify type introspection --- src/impact/cli.py | 58 +++++++++++++++++++++++------------------------ 1 file changed, 28 insertions(+), 30 deletions(-) diff --git a/src/impact/cli.py b/src/impact/cli.py index 3c21afa..934a9bc 100644 --- a/src/impact/cli.py +++ b/src/impact/cli.py @@ -4,7 +4,7 @@ from enum import Enum from functools import cached_property, wraps from pathlib import Path -from typing import TYPE_CHECKING, Any, Literal, get_args, get_origin +from typing import TYPE_CHECKING, Any, Literal, get_args, get_origin, get_type_hints import click @@ -348,12 +348,12 @@ class _ConfigSection: kwargs: dict[str, Any] = dataclasses.field(default_factory=dict) @cached_property - def fields(self) -> dict[str, dataclasses.Field[Any]]: - return {f.name: f for f in dataclasses.fields(self.cls)} + def field_names(self) -> list[str]: + return [f.name for f in dataclasses.fields(self.cls)] @cached_property - def field_names(self) -> list[str]: - return list(self.fields.keys()) + def type_hints(self) -> dict[str, Any]: + return get_type_hints(self.cls) def inject_impact_config_from_checkpoint(f: Callable[..., Any]) -> Callable[..., Any]: @@ -414,8 +414,9 @@ def inject_impact_config_overrides( option, defaulting to None (meaning "keep the value from loaded config"). The decorated function must accept `impact_config: ImpactConfig` as a keyword - argument, populated upstream. Any explicitly-passed override is merged in via - `dataclasses.replace`, leaving other sections (e.g. `sasrec`) untouched. + argument, populated upstream. Only explicitly provided CLI values override the + loaded configuration. All other fields retain their values from the checkpoint + snapshot. Use together with `cli.inject_impact_config_from_checkpoint()` for commands that load a checkpoint's saved config but need to adjust evaluation/runtime parameters. @@ -429,19 +430,22 @@ def _decorator(f: Callable[..., Any]) -> Callable[..., Any]: chosen = {s: _OVERRIDE_SECTIONS[s] for s in sections} options: list[Callable[[Any], Any]] = [] for section, config_section in chosen.items(): - for field_name, field_def in config_section.fields.items(): + for field_name in config_section.field_names: + field_type = config_section.type_hints[field_name] + option_name = f"--{section}-{field_name}".replace("_", "-") - multiple = _is_tuple_field(field_def) + multiple = get_origin(field_type) is tuple + options.append( click.option( option_name, - type=_click_type_for(field_def), + type=_click_type_for(field_type), multiple=multiple, default=() if multiple else None, - help=f""" - Override {config_section.cls.__name__}.{field_name}. - Defaults to the loaded config's value. - """, + help=( + f"Override {config_section.cls.__name__}.{field_name}. " + "Defaults to the loaded config's value." + ), ) ) @@ -457,7 +461,7 @@ def _wrapper(*args: Any, **kwargs: Any) -> Any: overrides: dict[str, Any] = {} for field_name in config_section.field_names: kw_key = f"{section}_{field_name}" - value = kwargs.pop(kw_key) + value = kwargs.pop(kw_key, None) if value in (None, ()): continue overrides[field_name] = value @@ -477,21 +481,15 @@ def _wrapper(*args: Any, **kwargs: Any) -> Any: return _decorator -def _is_tuple_field(field_def: dataclasses.Field[Any]) -> bool: - """Return True if the given dataclass field is a tuple type, or False otherwise.""" - return get_origin(field_def.type) is tuple or ( - isinstance(field_def.type, str) and field_def.type.startswith("tuple") - ) +def _click_type_for(type_: Any) -> Any: + """Map Python type to a Click parameter type.""" + if get_origin(type_) is tuple: + return get_args(type_)[0] + + if type_ in (int, float, str): + return type_ + if isinstance(type_, type) and issubclass(type_, Enum): + return click.Choice([e.value for e in type_]) -def _click_type_for(field_def: dataclasses.Field[Any]) -> Any: - """Map a dataclass field's annotation to a Click parameter type.""" - if _is_tuple_field(field_def): - # tuple[int, ...] -> int, tuple[str, ...] -> str, etc. - inner = get_args(field_def.type)[0] if get_args(field_def.type) else str - return inner - if field_def.type in (int, float, str, bool): - return field_def.type - if isinstance(field_def.type, type) and issubclass(field_def.type, Enum): - return click.Choice([e.value for e in field_def.type]) return str From 2d100f690288be318674a4279447427ce9899dfb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Krzysztof=20G=C5=82owacz?= Date: Fri, 12 Jun 2026 13:14:13 +0200 Subject: [PATCH 07/11] [#26] feat: add newly defined decorators parsing SASRecNBR checkpoint and adjacent config snapshot to eval/plotting functions (in src/impact/main.py and scripts/plots.py) --- scripts/plots.py | 14 ++++++-------- src/impact/main.py | 15 +++++++-------- 2 files changed, 13 insertions(+), 16 deletions(-) diff --git a/scripts/plots.py b/scripts/plots.py index bf810d8..f79ca3c 100644 --- a/scripts/plots.py +++ b/scripts/plots.py @@ -13,7 +13,10 @@ import polars as pl import structlog -from impact.cli import inject_impact_config +from impact.cli import ( + inject_impact_config_from_checkpoint, + inject_impact_config_overrides, +) from impact.config import ImpactConfig from impact.logger import configure_logging from impact.pipelines import DataPipeline, ImpactEvaluationPipeline @@ -138,12 +141,6 @@ def cli() -> None: @cli.command(name="plot-alpha-curve") -@click.option( - "--sasrec-checkpoint-path", - type=click.Path(exists=True, dir_okay=False, path_type=Path), - required=True, - help="Path to a trained SASRecNBR checkpoint that will be evaluated.", -) @click.option( "--log-level", type=click.Choice(list(logging.getLevelNamesMapping()), case_sensitive=False), @@ -255,7 +252,8 @@ def cli() -> None: show_default=True, help="Scale factor for PNG export. 3.0 is approx. 216 PPI - suitable for print.", ) -@inject_impact_config +@inject_impact_config_from_checkpoint +@inject_impact_config_overrides("actr", "eval", "system") def plot_alpha_curve( impact_config: ImpactConfig, sasrec_checkpoint_path: Path, diff --git a/src/impact/main.py b/src/impact/main.py index b0848a9..313f73a 100644 --- a/src/impact/main.py +++ b/src/impact/main.py @@ -12,7 +12,11 @@ import dotenv import structlog -from impact.cli import inject_impact_config +from impact.cli import ( + inject_impact_config, + inject_impact_config_from_checkpoint, + inject_impact_config_overrides, +) from impact.dataset import DatasetSize from impact.logger import configure_logging from impact.pipelines import ( @@ -172,12 +176,6 @@ def train_sasrec( @cli.command(name="evaluate-impact") -@click.option( - "--sasrec-checkpoint-path", - type=click.Path(exists=True, dir_okay=False, path_type=Path), - required=True, - help="Path to a trained SASRecNBR checkpoint that will be evaluated.", -) @click.option( "--partition", type=click.Choice(["val", "test"], case_sensitive=False), @@ -228,7 +226,8 @@ def train_sasrec( default=None, help="Base directory for log files. Defaults to /logs.", ) -@inject_impact_config +@inject_impact_config_from_checkpoint +@inject_impact_config_overrides("actr", "eval", "system") def evaluate_impact( impact_config: ImpactConfig, sasrec_checkpoint_path: Path, From 0a04454c3ebfa9a876244c68ca9658cfec59f309 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Krzysztof=20G=C5=82owacz?= Date: Fri, 12 Jun 2026 13:26:20 +0200 Subject: [PATCH 08/11] [#26] feat(pipelines): make SASRecTrainingPipeline always saves ImpactConfig snapshot next to the best-performing model checkpoint --- src/impact/pipelines/sasrec_training_pipeline.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/impact/pipelines/sasrec_training_pipeline.py b/src/impact/pipelines/sasrec_training_pipeline.py index 330f8e4..b5cb806 100644 --- a/src/impact/pipelines/sasrec_training_pipeline.py +++ b/src/impact/pipelines/sasrec_training_pipeline.py @@ -164,6 +164,7 @@ def train( logger.info("New best model found", best_ndcg=round(best_ndcg, 4)) if save_path is not None: torch.save(model.state_dict(), save_path) + self._config.write_yaml(save_path.parent / "config.yaml") else: epochs_without_improvement += 1 From afa2ac26ed120d0540454bc8dfbace5974f63464 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Krzysztof=20G=C5=82owacz?= Date: Fri, 12 Jun 2026 13:27:40 +0200 Subject: [PATCH 09/11] [#26] feat(main): make '--partition' flag required for 'evaluate-impact' command to enforce the caller to explicitly provide on which split partition the evaluation should be done --- src/impact/main.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/impact/main.py b/src/impact/main.py index 313f73a..74b1f6d 100644 --- a/src/impact/main.py +++ b/src/impact/main.py @@ -6,7 +6,7 @@ import json import logging from pathlib import Path -from typing import TYPE_CHECKING, Any, cast +from typing import TYPE_CHECKING, Any, Literal, cast import click import dotenv @@ -179,8 +179,7 @@ def train_sasrec( @click.option( "--partition", type=click.Choice(["val", "test"], case_sensitive=False), - default="test", - show_default=True, + required=True, help=""" Dataset partition to evaluate on. Use 'val' during hyperparameter search, 'test' for final evaluation on the held-out data. @@ -231,7 +230,7 @@ def train_sasrec( def evaluate_impact( impact_config: ImpactConfig, sasrec_checkpoint_path: Path, - partition: str = "test", + partition: Literal["val", "test"], log_level: str = "INFO", raw_data_dir: Path | None = None, interim_data_dir: Path | None = None, From 87da17c36f41c36a150bb5fa6a539209dc0b8ba2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Krzysztof=20G=C5=82owacz?= Date: Fri, 12 Jun 2026 13:30:57 +0200 Subject: [PATCH 10/11] [#26] fix: resolve minor issues raised by the type-checkers --- pyproject.toml | 2 ++ scripts/tables.py | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index b12a6a6..f92e900 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -198,6 +198,7 @@ strict = true # Strict mode, refer to the mypy docs for details python_version = "3.12" # Type check code assuming it will be running on the specified Python version pretty = true # Use visually nicer output in error messages files = [ # Type-check given files or directories + "scripts", "src", "tests" ] @@ -251,6 +252,7 @@ executionEnvironments = [ { root = "src/impact/cli.py", reportAny = false, reportExplicitAny = false }, { root = "src/impact/config.py", reportAny = false, reportExplicitAny = false }, { root = "src/impact/main.py", reportExplicitAny = false }, + { root = "src/impact/utils.py", reportImplicitStringConcatenation = false }, { root = "src/impact/dataset/fetcher.py", reportUnknownVariableType = false }, { root = "src/impact/features/sequence_extraction.py", reportExplicitAny = false }, { root = "src/impact/features/tensorization.py", reportAny = false, reportExplicitAny = false }, diff --git a/scripts/tables.py b/scripts/tables.py index 4bd2a64..b69d535 100644 --- a/scripts/tables.py +++ b/scripts/tables.py @@ -157,8 +157,8 @@ class _VariantConfig(NamedTuple): logger.info(f"Processing {ds_size.name} dataset variant") variant_config = dataclasses.replace( impact_config, - dataset_size=ds_size, - step=variant.step, + dataset=dataclasses.replace(impact_config.dataset, size=ds_size), + sequence=dataclasses.replace(impact_config.sequence, step=variant.step), ) pipeline.config = variant_config # Switch pipeline config From e5aedc6cfb11946b1f0870eddfb52d3bb95866c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Krzysztof=20G=C5=82owacz?= Date: Fri, 12 Jun 2026 13:33:16 +0200 Subject: [PATCH 11/11] [#26] docs(cli): adjust @inject_impact_config() decorator's docstring to reflect the correct name of the other (mentioned) decorator --- src/impact/cli.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/impact/cli.py b/src/impact/cli.py index 934a9bc..19e9363 100644 --- a/src/impact/cli.py +++ b/src/impact/cli.py @@ -37,8 +37,8 @@ def inject_impact_config(f: Callable[..., Any]) -> Callable[..., Any]: Use this for commands that configure a fresh run from scratch (e.g. training, grid search). For commands operating on an existing checkpoint (e.g. evaluation), use - `inject_config_from_checkpoint` instead, which loads the config that was saved - alongside the checkpoint rather than rebuilding it from CLI flags. + `inject_impact_config_from_checkpoint` instead, which loads the config that was + saved alongside the checkpoint rather than rebuilding it from CLI flags. """ # Define all click options, matching the defaults from ImpactConfig options = [