Skip to content

Commit f82c0ba

Browse files
committed
feat: add NoResetBenchmarkRunner
1 parent ef318da commit f82c0ba

6 files changed

Lines changed: 743 additions & 61 deletions

File tree

Lines changed: 127 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,24 @@
1+
# -*- coding: ascii -*-
2+
3+
"""
4+
NoReset benchmark runner implementation.
5+
6+
This module provides NoResetBenchmarkRunner - an optimised benchmark for
7+
series with a single change point. The solver is executed only once per
8+
(algorithm, provider) pair with threshold=inf, and all threshold
9+
evaluations are simulated via ThresholdPolicy on the cached trace.
10+
"""
11+
12+
__author__ = "Danil Totmyanin"
13+
__copyright__ = "Copyright (c) 2026 PySATL project"
14+
__license__ = "SPDX-License-Identifier: MIT"
15+
116
from collections.abc import Sequence
217
from pathlib import Path
318
from typing import Any
419

520
from pysatl_cpd.analysis.labeled_data import LabeledData
21+
from pysatl_cpd.benchmark.core.benchmark_executor import BenchmarkExecutor
622
from pysatl_cpd.benchmark.metrics.multiple_run_metric import MultipleRunMetric
723
from pysatl_cpd.benchmark.noreset.noreset_detection_trace import NoResetDetectionTrace
824
from pysatl_cpd.benchmark.noreset.threshold_policy import ThresholdPolicy
@@ -13,28 +29,130 @@
1329

1430

1531
class NoResetBenchmarkRunner[ProviderT: LabeledData[Any]](OnlineBenchmarkRunner[NoResetDetectionTrace[Any], ProviderT]):
32+
"""
33+
Optimised benchmark runner for series with a single change point.
34+
35+
For each (algorithm, provider) pair the solver is executed exactly
36+
once with threshold=inf, producing a full detection function trace.
37+
All threshold evaluations are then simulated by applying a
38+
ThresholdPolicy to that cached trace, avoiding redundant solver runs.
39+
Caching is handled entirely by BenchmarkExecutor.
40+
41+
Parameters
42+
----------
43+
algorithms : Sequence[tuple[OnlineAlgorithm[Any, Any, Any], Sequence[float]]]
44+
Sequence of (algorithm, thresholds) pairs to evaluate.
45+
providers : Sequence[ProviderT]
46+
Labeled data providers to run against.
47+
metrics : dict[str, MultipleRunMetric[NoResetDetectionTrace[Any], ProviderT, Any]]
48+
Named metrics to evaluate for each (algorithm, threshold) batch.
49+
solver : OnlineCpdSolver
50+
Solver used to produce inf traces.
51+
policy : ThresholdPolicy
52+
Policy used to extract detected change points from the inf trace
53+
for each threshold.
54+
dump_dir : Path | str | None, optional
55+
Directory for caching inf traces via BenchmarkExecutor.
56+
If None, caching is disabled. Default is None.
57+
"""
58+
1659
def __init__(
1760
self,
1861
algorithms: Sequence[tuple[OnlineAlgorithm[Any, Any, Any], Sequence[float]]],
1962
providers: Sequence[ProviderT],
2063
metrics: dict[str, MultipleRunMetric[NoResetDetectionTrace[Any], ProviderT, Any]],
2164
solver: OnlineCpdSolver,
2265
policy: ThresholdPolicy,
23-
dump_dir: Path | None = None,
66+
dump_dir: Path | str | None = None,
2467
) -> None:
25-
return
68+
super().__init__(
69+
algorithms=algorithms,
70+
providers=providers,
71+
metrics=metrics,
72+
solver=solver,
73+
dump_dir=dump_dir,
74+
)
75+
self._policy = policy
76+
77+
def _get_inf_trace(
78+
self,
79+
algorithm: OnlineAlgorithm[Any, Any, Any],
80+
provider: ProviderT,
81+
) -> OnlineDetectionTrace[Any]:
82+
"""
83+
Compute or retrieve the infinite-threshold trace for a given pair.
84+
85+
Delegates entirely to BenchmarkExecutor which handles disk caching
86+
when dump_dir is set.
87+
88+
Parameters
89+
----------
90+
algorithm : OnlineAlgorithm[Any, Any, Any]
91+
The algorithm to run.
92+
provider : ProviderT
93+
The data provider to run against.
94+
95+
Returns
96+
-------
97+
OnlineDetectionTrace[Any]
98+
Trace produced with threshold=inf.
99+
"""
100+
executor: BenchmarkExecutor[Any] = BenchmarkExecutor(
101+
algorithms=[(algorithm, [float("inf")])],
102+
providers=[provider],
103+
solver=self._solver,
104+
dump_dir=self._dump_dir,
105+
)
106+
_, inf_trace = executor.execute()[0]
107+
return inf_trace
26108

27109
def _collect_runs(
28110
self,
29111
algorithm: OnlineAlgorithm[Any, Any, Any],
30112
threshold: float,
31113
providers: Sequence[ProviderT],
32114
) -> list[tuple[NoResetDetectionTrace[Any], ProviderT]]:
33-
raise NotImplementedError("Method '_collect_runs' is not implemented yet.")
115+
"""
116+
Collect NoReset runs for a given algorithm and threshold.
34117
35-
def _get_inf_trace(
36-
self,
37-
algorithm: OnlineAlgorithm[Any, Any, Any],
38-
provider: ProviderT,
39-
) -> OnlineDetectionTrace[Any]:
40-
raise NotImplementedError("Method '_get_inf_trace' is not implemented yet.")
118+
For each provider, retrieves the inf trace via BenchmarkExecutor
119+
and applies the ThresholdPolicy to produce a lightweight
120+
NoResetDetectionTrace.
121+
122+
Parameters
123+
----------
124+
algorithm : OnlineAlgorithm[Any, Any, Any]
125+
The algorithm to evaluate.
126+
threshold : float
127+
The detection threshold to simulate.
128+
providers : Sequence[ProviderT]
129+
Data providers to run against.
130+
131+
Returns
132+
-------
133+
list[tuple[NoResetDetectionTrace[Any], ProviderT]]
134+
List of (noreset_trace, provider) pairs, one per provider.
135+
"""
136+
if not providers:
137+
return []
138+
139+
runs: list[tuple[NoResetDetectionTrace[Any], ProviderT]] = []
140+
141+
for provider in providers:
142+
inf_trace = self._get_inf_trace(algorithm, provider)
143+
144+
detected_change_points: list[int] = self._policy.apply(
145+
inf_trace.detection_function,
146+
threshold,
147+
provider.change_points,
148+
)
149+
150+
noreset_trace = NoResetDetectionTrace.from_inf_trace(
151+
source_trace=inf_trace,
152+
detected_change_points=detected_change_points,
153+
threshold=threshold,
154+
)
155+
156+
runs.append((noreset_trace, provider))
157+
158+
return runs

tests/mocks/algorithms/online/error.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -168,7 +168,5 @@ def __repr__(self) -> str:
168168
return (
169169
f"{self.__class__.__name__}("
170170
f"name={self._name!r}, "
171-
f"error_on_call={self._error_on_call}, "
172171
f"learning_period_size={self._config.learning_period_size}, "
173-
f"process_count={self._process_count})"
174172
)

tests/mocks/algorithms/online/simple.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -155,5 +155,4 @@ def __repr__(self) -> str:
155155
f"{self.__class__.__name__}("
156156
f"name={self._name!r}, "
157157
f"learning_period_size={self._config.learning_period_size}, "
158-
f"process_count={self._process_count})"
159158
)

tests/mocks/analysis/labeled_data.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,3 +35,32 @@ def __init__(self, change_points: Sequence[int], name: str = "MockLabeledData"):
3535
max_idx = max(change_points) if change_points else 0
3636
dummy_raw_data = [0.0] * max_idx
3737
super().__init__(raw_data=dummy_raw_data, change_points=change_points, name=name)
38+
39+
40+
class MockLabeledDataWithPadding(LabeledData[float]):
41+
"""
42+
Mock LabeledData where raw data length exceeds the maximum change point index.
43+
44+
Unlike MockLabeledData (where len == max_cp), this mock adds padding so
45+
that the last observation index is not a change point. This prevents
46+
algorithms from producing detections at index 0 due to insufficient data.
47+
48+
Parameters
49+
----------
50+
change_points : Sequence[int]
51+
Known change point indices (1-based, must be positive).
52+
padding : int, default=10
53+
Number of extra observations to append after the last change point.
54+
name : str, default="MockLabeledDataWithPadding"
55+
Dataset identifier.
56+
"""
57+
58+
def __init__(
59+
self,
60+
change_points: Sequence[int],
61+
padding: int = 10,
62+
name: str = "MockLabeledDataWithPadding",
63+
) -> None:
64+
max_idx = max(change_points) if change_points else 0
65+
dummy_raw_data = [0.0] * (max_idx + padding)
66+
super().__init__(raw_data=dummy_raw_data, change_points=change_points, name=name)

0 commit comments

Comments
 (0)