datatunner 3.2.0__tar.gz → 3.2.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. {datatunner-3.2.0/datatunner.egg-info → datatunner-3.2.2}/PKG-INFO +1 -1
  2. {datatunner-3.2.0 → datatunner-3.2.2}/datatunner/domain/experiment.py +2 -0
  3. {datatunner-3.2.0 → datatunner-3.2.2}/datatunner/orchestrator.py +110 -18
  4. {datatunner-3.2.0 → datatunner-3.2.2}/datatunner/reporting/tables.py +20 -10
  5. {datatunner-3.2.0 → datatunner-3.2.2/datatunner.egg-info}/PKG-INFO +1 -1
  6. {datatunner-3.2.0 → datatunner-3.2.2}/datatunner.egg-info/SOURCES.txt +1 -0
  7. {datatunner-3.2.0 → datatunner-3.2.2}/pyproject.toml +2 -2
  8. datatunner-3.2.2/tests/test_image_support.py +39 -0
  9. {datatunner-3.2.0 → datatunner-3.2.2}/tests/test_orchestrator_regressions.py +30 -1
  10. {datatunner-3.2.0 → datatunner-3.2.2}/LICENSE +0 -0
  11. {datatunner-3.2.0 → datatunner-3.2.2}/MANIFEST.in +0 -0
  12. {datatunner-3.2.0 → datatunner-3.2.2}/README.md +0 -0
  13. {datatunner-3.2.0 → datatunner-3.2.2}/datatunner/__init__.py +0 -0
  14. {datatunner-3.2.0 → datatunner-3.2.2}/datatunner/domain/__init__.py +0 -0
  15. {datatunner-3.2.0 → datatunner-3.2.2}/datatunner/domain/dataset.py +0 -0
  16. {datatunner-3.2.0 → datatunner-3.2.2}/datatunner/domain/generator.py +0 -0
  17. {datatunner-3.2.0 → datatunner-3.2.2}/datatunner/domain/metrics.py +0 -0
  18. {datatunner-3.2.0 → datatunner-3.2.2}/datatunner/evaluation/__init__.py +0 -0
  19. {datatunner-3.2.0 → datatunner-3.2.2}/datatunner/evaluation/performance/__init__.py +0 -0
  20. {datatunner-3.2.0 → datatunner-3.2.2}/datatunner/evaluation/performance/classification.py +0 -0
  21. {datatunner-3.2.0 → datatunner-3.2.2}/datatunner/evaluation/performance/regression.py +0 -0
  22. {datatunner-3.2.0 → datatunner-3.2.2}/datatunner/evaluation/quality/__init__.py +0 -0
  23. {datatunner-3.2.0 → datatunner-3.2.2}/datatunner/evaluation/quality/coverage.py +0 -0
  24. {datatunner-3.2.0 → datatunner-3.2.2}/datatunner/evaluation/quality/fidelity.py +0 -0
  25. {datatunner-3.2.0 → datatunner-3.2.2}/datatunner/evaluation/quality/statistical.py +0 -0
  26. {datatunner-3.2.0 → datatunner-3.2.2}/datatunner/exceptions.py +0 -0
  27. {datatunner-3.2.0 → datatunner-3.2.2}/datatunner/generators/__init__.py +0 -0
  28. {datatunner-3.2.0 → datatunner-3.2.2}/datatunner/generators/augmentation.py +0 -0
  29. {datatunner-3.2.0 → datatunner-3.2.2}/datatunner/generators/base.py +0 -0
  30. {datatunner-3.2.0 → datatunner-3.2.2}/datatunner/generators/ctgan.py +0 -0
  31. {datatunner-3.2.0 → datatunner-3.2.2}/datatunner/generators/smote.py +0 -0
  32. {datatunner-3.2.0 → datatunner-3.2.2}/datatunner/infrastructure/__init__.py +0 -0
  33. {datatunner-3.2.0 → datatunner-3.2.2}/datatunner/infrastructure/hardware.py +0 -0
  34. {datatunner-3.2.0 → datatunner-3.2.2}/datatunner/infrastructure/logging.py +0 -0
  35. {datatunner-3.2.0 → datatunner-3.2.2}/datatunner/infrastructure/persistence.py +0 -0
  36. {datatunner-3.2.0 → datatunner-3.2.2}/datatunner/infrastructure/seed.py +0 -0
  37. {datatunner-3.2.0 → datatunner-3.2.2}/datatunner/mixing/__init__.py +0 -0
  38. {datatunner-3.2.0 → datatunner-3.2.2}/datatunner/mixing/engine.py +0 -0
  39. {datatunner-3.2.0 → datatunner-3.2.2}/datatunner/mixing/strategies.py +0 -0
  40. {datatunner-3.2.0 → datatunner-3.2.2}/datatunner/mixing/validators.py +0 -0
  41. {datatunner-3.2.0 → datatunner-3.2.2}/datatunner/optimization/__init__.py +0 -0
  42. {datatunner-3.2.0 → datatunner-3.2.2}/datatunner/optimization/base.py +0 -0
  43. {datatunner-3.2.0 → datatunner-3.2.2}/datatunner/optimization/bayesian.py +0 -0
  44. {datatunner-3.2.0 → datatunner-3.2.2}/datatunner/optimization/grid.py +0 -0
  45. {datatunner-3.2.0 → datatunner-3.2.2}/datatunner/optimization/random.py +0 -0
  46. {datatunner-3.2.0 → datatunner-3.2.2}/datatunner/py.typed +0 -0
  47. {datatunner-3.2.0 → datatunner-3.2.2}/datatunner/reporting/__init__.py +0 -0
  48. {datatunner-3.2.0 → datatunner-3.2.2}/datatunner/reporting/exporters.py +0 -0
  49. {datatunner-3.2.0 → datatunner-3.2.2}/datatunner/reporting/plots.py +0 -0
  50. {datatunner-3.2.0 → datatunner-3.2.2}/datatunner/training/__init__.py +0 -0
  51. {datatunner-3.2.0 → datatunner-3.2.2}/datatunner/training/checkpoint.py +0 -0
  52. {datatunner-3.2.0 → datatunner-3.2.2}/datatunner/training/environment.py +0 -0
  53. {datatunner-3.2.0 → datatunner-3.2.2}/datatunner/training/runner.py +0 -0
  54. {datatunner-3.2.0 → datatunner-3.2.2}/datatunner.egg-info/dependency_links.txt +0 -0
  55. {datatunner-3.2.0 → datatunner-3.2.2}/datatunner.egg-info/requires.txt +0 -0
  56. {datatunner-3.2.0 → datatunner-3.2.2}/datatunner.egg-info/top_level.txt +0 -0
  57. {datatunner-3.2.0 → datatunner-3.2.2}/examples/__init__.py +0 -0
  58. {datatunner-3.2.0 → datatunner-3.2.2}/examples/example_adult_ctgan.py +0 -0
  59. {datatunner-3.2.0 → datatunner-3.2.2}/examples/example_adult_smote.py +0 -0
  60. {datatunner-3.2.0 → datatunner-3.2.2}/examples/example_cifar_augmentation.py +0 -0
  61. {datatunner-3.2.0 → datatunner-3.2.2}/examples/example_colab_breast_cancer.py +0 -0
  62. {datatunner-3.2.0 → datatunner-3.2.2}/examples/example_colab_drybean_ctgan.py +0 -0
  63. {datatunner-3.2.0 → datatunner-3.2.2}/examples/example_colab_mnist_augmentation.py +0 -0
  64. {datatunner-3.2.0 → datatunner-3.2.2}/examples/example_colab_scatter_per_alpha.py +0 -0
  65. {datatunner-3.2.0 → datatunner-3.2.2}/examples/example_comparison_smote_vs_ctgan.py +0 -0
  66. {datatunner-3.2.0 → datatunner-3.2.2}/setup.cfg +0 -0
  67. {datatunner-3.2.0 → datatunner-3.2.2}/tests/__init__.py +0 -0
  68. {datatunner-3.2.0 → datatunner-3.2.2}/tests/test_aggregation.py +0 -0
  69. {datatunner-3.2.0 → datatunner-3.2.2}/tests/test_domain.py +0 -0
  70. {datatunner-3.2.0 → datatunner-3.2.2}/tests/test_generators.py +0 -0
  71. {datatunner-3.2.0 → datatunner-3.2.2}/tests/test_infrastructure.py +0 -0
  72. {datatunner-3.2.0 → datatunner-3.2.2}/tests/test_metrics_formatting.py +0 -0
  73. {datatunner-3.2.0 → datatunner-3.2.2}/tests/test_mixing.py +0 -0
  74. {datatunner-3.2.0 → datatunner-3.2.2}/tests/test_seed_counter.py +0 -0
  75. {datatunner-3.2.0 → datatunner-3.2.2}/tests/test_training_defaults.py +0 -0
  76. {datatunner-3.2.0 → datatunner-3.2.2}/tests/test_validators_coverage.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: datatunner
3
- Version: 3.2.0
3
+ Version: 3.2.2
4
4
  Summary: DataTunner: Scientific Platform for Optimal Artificial Data Proportion in Deep Learning
5
5
  Author-email: Leandro Costa Rocha <leandrocrx@gmail.com>
6
6
  License-Expression: MIT
@@ -63,6 +63,7 @@ class ExperimentReport:
63
63
  quality_results: List[Any] = field(default_factory=list)
64
64
  optimization_results: List[ExperimentResult] = field(default_factory=list)
65
65
  baseline_results: List[ExperimentResult] = field(default_factory=list)
66
+ paired_comparisons: List[Dict] = field(default_factory=list)
66
67
  creation_timestamp: float = field(default_factory=time.time)
67
68
 
68
69
  @property
@@ -97,6 +98,7 @@ class ExperimentReport:
97
98
  'best_result': self.best_result.to_dict() if self.best_result else None,
98
99
  'optimization_results': [r.to_dict() for r in self.optimization_results],
99
100
  'baseline_results': [r.to_dict() for r in self.baseline_results],
101
+ 'paired_comparisons': self.paired_comparisons,
100
102
  'n_optimization_results': len(self.optimization_results),
101
103
  'n_baseline_results': len(self.baseline_results),
102
104
  'creation_timestamp': self.creation_timestamp,
@@ -168,21 +168,56 @@ class DataTunner:
168
168
  seed_manager=self.seed_manager
169
169
  )
170
170
 
171
- # Phase 4: Run baselines
171
+ # Phase 4: Run baselines with a dedicated seed block (paired design)
172
172
  baseline_results = []
173
+ paired_comparisons = []
173
174
  if baselines:
174
- print(f"[DataTunner] Running {len(baselines)} baseline comparisons...")
175
+ print(f"[DataTunner] Running {len(baselines)} baseline comparisons (paired, n={self.config.n_repetitions})...")
176
+
177
+ # Best point from the search grid — used for the paired re-run
178
+ best_alpha = None
179
+ if optimization_results:
180
+ target = self.config.target_metric
181
+ best_alpha = max(
182
+ optimization_results,
183
+ key=lambda r: r.performance.to_dict().get(target, -float('inf'))
184
+ ).alpha
185
+
175
186
  for name, alpha, base_gen in baselines:
176
- base_result = self._run_baseline(name, alpha, base_gen, real_data, test_data)
177
- if base_result:
178
- baseline_results.append(base_result)
187
+ # ONE seed block per baseline; the same seeds are reused for
188
+ # alpha* so each pair shares the same random state (thesis
189
+ # Section 4.7 — "blocagem por semente").
190
+ seed_block = self.seed_manager.generate_seeds(self.config.n_repetitions)
191
+
192
+ base_results = self._run_baseline(name, alpha, base_gen, real_data, test_data,
193
+ seed_block=seed_block)
194
+ baseline_results.extend(base_results)
195
+
196
+ if best_alpha is not None and base_results:
197
+ star_results = self._run_paired_alpha_star(best_alpha, seed_block)
198
+ if star_results:
199
+ paired_comparisons.append({
200
+ 'baseline_name': name,
201
+ 'baseline_alpha': alpha,
202
+ 'alpha_star': best_alpha,
203
+ 'target_metric': self.config.target_metric,
204
+ 'pairs': [
205
+ {
206
+ 'seed': b.seed,
207
+ 'baseline_metric': b.performance.to_dict().get(self.config.target_metric),
208
+ 'alpha_star_metric': s.performance.to_dict().get(self.config.target_metric),
209
+ }
210
+ for b, s in zip(base_results, star_results)
211
+ ],
212
+ })
179
213
 
180
214
  # Phase 5: Compile report
181
215
  report = ExperimentReport(
182
216
  config=self.config,
183
217
  quality_results=quality_results,
184
218
  optimization_results=optimization_results,
185
- baseline_results=baseline_results
219
+ baseline_results=baseline_results,
220
+ paired_comparisons=paired_comparisons,
186
221
  )
187
222
 
188
223
  # Save artifacts
@@ -201,6 +236,18 @@ class DataTunner:
201
236
 
202
237
  return report
203
238
 
239
+ @staticmethod
240
+ def _count_samples(data: Any) -> int:
241
+ """Number of samples in a dataset, handling (images, labels) tuples.
242
+
243
+ Image pipelines pass real_data as a (X, y) tuple; ``len()`` on the
244
+ tuple returns the number of elements (2), not the number of samples.
245
+ Without this, n_synthetic = int(alpha * 2) ≈ 0 for every alpha.
246
+ """
247
+ if isinstance(data, tuple) and len(data) == 2:
248
+ return len(data[0])
249
+ return len(data)
250
+
204
251
  def _run_single_experiment(self, alpha: float, seed: int) -> ExperimentResult:
205
252
  """Execute ONE complete experiment: generate -> mix -> train -> evaluate.
206
253
 
@@ -210,7 +257,7 @@ class DataTunner:
210
257
 
211
258
  with self.environment.isolate(seed, experiment_id):
212
259
  # 1. Generate synthetic data
213
- n_real = len(self._real_data)
260
+ n_real = self._count_samples(self._real_data)
214
261
  n_synthetic = int(alpha * n_real)
215
262
 
216
263
  # Bind the generator RNG to this trial's seed so repetitions
@@ -311,7 +358,7 @@ class DataTunner:
311
358
  ) -> List[Dict]:
312
359
  """Pre-evaluate generator by generating a sample and measuring fidelity."""
313
360
  # Generate a fixed-size sample for quality assessment
314
- sample_size = min(1000, len(real_data))
361
+ sample_size = min(1000, self._count_samples(real_data))
315
362
  generator.set_seed(generator.spec.random_state)
316
363
  synthetic_sample = generator.generate(sample_size)
317
364
 
@@ -329,25 +376,70 @@ class DataTunner:
329
376
  alpha: float,
330
377
  generator: AbstractGenerator,
331
378
  real_data: Any,
332
- test_data: Any
333
- ) -> Optional[ExperimentResult]:
334
- """Run a single baseline configuration."""
379
+ test_data: Any,
380
+ seed_block: Optional[List[int]] = None
381
+ ) -> List[ExperimentResult]:
382
+ """Run baseline trials over a dedicated seed block.
383
+
384
+ The block is generated ONCE per baseline (Section 4.7 of the thesis:
385
+ "blocagem por semente") and the SAME seeds are later reused to
386
+ re-run alpha* — that pairing is what legitimizes the Wilcoxon
387
+ signed-rank test of P1/H1. Earlier versions ran a single trial with
388
+ one fresh seed, which made a paired comparison impossible.
389
+
390
+ Args:
391
+ name: Baseline label.
392
+ alpha: Proportion to evaluate (usually 0.0).
393
+ generator: Generator instance for this baseline.
394
+ real_data: Training data.
395
+ test_data: Hold-out test data.
396
+ seed_block: Optional pre-generated block of seeds. If None, a
397
+ fresh block of ``config.n_repetitions`` seeds is
398
+ drawn from the counter.
399
+ """
335
400
  old_generator = self._generator
401
+ results: List[ExperimentResult] = []
336
402
  try:
337
- # generate_seeds advances a counter, so each baseline gets a seed
338
- # that is fresh (never reused from the main search).
339
- seed = self.seed_manager.generate_seeds(1)[0]
403
+ if seed_block is None:
404
+ seed_block = self.seed_manager.generate_seeds(self.config.n_repetitions)
340
405
 
341
406
  # Temporarily swap generator
342
407
  self._generator = generator
343
408
  generator.fit(real_data)
344
409
 
345
- result = self._run_single_experiment(alpha, seed)
346
-
347
- return result
410
+ for seed in seed_block:
411
+ try:
412
+ result = self._run_single_experiment(alpha, seed)
413
+ if result is not None:
414
+ results.append(result)
415
+ except Exception as e:
416
+ print(f"[DataTunner] Baseline '{name}' trial seed={seed} failed: {e}")
417
+ return results
348
418
  except Exception as e:
349
419
  print(f"[DataTunner] Baseline '{name}' failed: {e}")
350
- return None
420
+ return results
351
421
  finally:
352
422
  # Always restore the main generator, even on failure.
353
423
  self._generator = old_generator
424
+
425
+ def _run_paired_alpha_star(
426
+ self,
427
+ alpha_star: float,
428
+ seed_block: List[int]
429
+ ) -> List[ExperimentResult]:
430
+ """Re-run alpha* with the SAME seed block used by a baseline.
431
+
432
+ This is the second half of the paired design: for each seed in the
433
+ block, one trial at alpha* is executed with the main generator. The
434
+ caller zips these results with the baseline results (same seed
435
+ order) to build the paired comparison for the Wilcoxon test.
436
+ """
437
+ results: List[ExperimentResult] = []
438
+ for seed in seed_block:
439
+ try:
440
+ result = self._run_single_experiment(alpha_star, seed)
441
+ if result is not None:
442
+ results.append(result)
443
+ except Exception as e:
444
+ print(f"[DataTunner] Paired alpha* trial seed={seed} failed: {e}")
445
+ return results
@@ -1,7 +1,8 @@
1
1
  """Publication-ready table generation for experiment reports."""
2
2
 
3
- from typing import List
3
+ from typing import Dict, List, Tuple
4
4
 
5
+ import numpy as np
5
6
  import pandas as pd
6
7
 
7
8
  from datatunner.domain.experiment import ExperimentReport, ExperimentResult
@@ -59,20 +60,29 @@ class TableGenerator:
59
60
  "type": "optimization",
60
61
  })
61
62
 
63
+ # Baselines: aggregate the n_r trials of the paired block into one
64
+ # row per (method, alpha) so the comparison table stays readable.
65
+ baseline_agg: Dict[Tuple[str, float], List[float]] = {}
62
66
  for r in report.baseline_results:
63
67
  perf = r.performance.to_dict()
64
68
  value = perf.get(target)
69
+ method = (
70
+ r.generator_spec.name
71
+ if hasattr(r.generator_spec, "name")
72
+ else str(r.generator_spec)
73
+ )
74
+ if value is not None:
75
+ baseline_agg.setdefault((method, r.alpha), []).append(value)
76
+
77
+ for (method, alpha), values in baseline_agg.items():
78
+ mean = float(np.mean(values))
65
79
  improvement = None
66
- if best_value is not None and value is not None and best_value != 0:
67
- improvement = ((value - best_value) / best_value) * 100.0
80
+ if best_value is not None and best_value != 0:
81
+ improvement = ((mean - best_value) / best_value) * 100.0
68
82
  rows.append({
69
- "method": (
70
- r.generator_spec.name
71
- if hasattr(r.generator_spec, "name")
72
- else str(r.generator_spec)
73
- ),
74
- "alpha": r.alpha,
75
- target: f"{value:.4f}" if value is not None else "N/A",
83
+ "method": method,
84
+ "alpha": alpha,
85
+ target: f"{mean:.4f}",
76
86
  "improvement_%": (
77
87
  f"{improvement:+.2f}%" if improvement is not None else "N/A"
78
88
  ),
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: datatunner
3
- Version: 3.2.0
3
+ Version: 3.2.2
4
4
  Summary: DataTunner: Scientific Platform for Optimal Artificial Data Proportion in Deep Learning
5
5
  Author-email: Leandro Costa Rocha <leandrocrx@gmail.com>
6
6
  License-Expression: MIT
@@ -64,6 +64,7 @@ tests/__init__.py
64
64
  tests/test_aggregation.py
65
65
  tests/test_domain.py
66
66
  tests/test_generators.py
67
+ tests/test_image_support.py
67
68
  tests/test_infrastructure.py
68
69
  tests/test_metrics_formatting.py
69
70
  tests/test_mixing.py
@@ -4,8 +4,8 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "datatunner"
7
- # Bump on release: the current published version on PyPI is 3.1.1.
8
- version = "3.2.0"
7
+ # Bump on release: the current published version on PyPI is 3.2.1.
8
+ version = "3.2.2"
9
9
  description = "DataTunner: Scientific Platform for Optimal Artificial Data Proportion in Deep Learning"
10
10
  readme = "README.md"
11
11
  license = "MIT"
@@ -0,0 +1,39 @@
1
+ """Regression tests for image pipeline support (X, y) tuples.
2
+
3
+ Bug fix (2026-08): `_run_single_experiment` computed n_synthetic with
4
+ ``len(real_data)``. For image pipelines real_data is a (X, y) tuple, so
5
+ len() returned 2 → n_synthetic = int(alpha * 2) ≈ 0 for every alpha:
6
+ the ImageAugmentation pipeline generated (almost) no synthetic data.
7
+
8
+ Fix: DataTunner._count_samples() counts rows of data[0] for 2-tuples.
9
+ Used in _run_single_experiment and _evaluate_generator_quality.
10
+
11
+ Run where deps are installed (CI / Colab): pytest tests/test_image_support.py
12
+ """
13
+ import pytest
14
+
15
+ from datatunner.orchestrator import DataTunner
16
+
17
+
18
+ class TestCountSamples:
19
+ @pytest.mark.parametrize("n", [0, 1, 2, 10, 1000])
20
+ def test_tuple_counts_samples_not_elements(self, n):
21
+ X = [0] * n
22
+ y = [0] * n
23
+ assert DataTunner._count_samples((X, y)) == n
24
+
25
+ def test_nested_2d_like_image_array(self):
26
+ # X shaped (N, H, W): must count N, not dimensions
27
+ X = [[[0] * 4 for _ in range(4)] for _ in range(250)]
28
+ y = [0] * 250
29
+ assert DataTunner._count_samples((X, y)) == 250
30
+
31
+ def test_plain_list_counts_len(self):
32
+ assert DataTunner._count_samples([1, 2, 3, 4]) == 4
33
+
34
+ def test_single_element_list(self):
35
+ assert DataTunner._count_samples(["only"]) == 1
36
+
37
+ def test_non_tuple_pair_not_misclassified(self):
38
+ # A 2-element tuple is ambiguous; contract: (X, y) tuples mean images
39
+ assert DataTunner._count_samples((1, 2)) == 1 # len(data[0]) = 1
@@ -116,7 +116,7 @@ class TestBaselineIsolation:
116
116
 
117
117
  def test_baseline_uses_fresh_seed(self):
118
118
  spec = GeneratorSpec(name="stub", mechanism="smote", hyperparameters={}, random_state=42)
119
- tunner = _make_tunner()
119
+ tunner = _make_tunner() # n_repetitions=1
120
120
  baseline_gen = _StubGenerator(spec)
121
121
 
122
122
  report = _simple_run(tunner, _StubGenerator(spec), baselines=[("base", 0.0, baseline_gen)])
@@ -125,3 +125,32 @@ class TestBaselineIsolation:
125
125
  baseline_result = report.baseline_results[0]
126
126
  # Regression: baselines reused the first grid seed
127
127
  assert baseline_result.seed != grid_seed
128
+
129
+ def test_baseline_paired_block_shares_seed_with_alpha_star(self):
130
+ """Paired design (thesis 4.7): baseline and alpha* share a seed block."""
131
+ config = ExperimentConfig(
132
+ data_type="tabular",
133
+ search_strategy=GridSearch(),
134
+ alpha_bounds=(0.0, 1.0),
135
+ n_repetitions=3,
136
+ search_budget=3,
137
+ target_metric="f1_macro",
138
+ )
139
+ spec = GeneratorSpec(name="stub", mechanism="smote", hyperparameters={}, random_state=42)
140
+ tunner = _make_tunner(config)
141
+ baseline_gen = _StubGenerator(spec)
142
+
143
+ report = _simple_run(tunner, _StubGenerator(spec), baselines=[("base", 0.0, baseline_gen)])
144
+
145
+ # Baseline now runs n_repetitions trials (one per seed in the block)
146
+ assert len(report.baseline_results) == config.n_repetitions
147
+ # Paired comparison: baseline seeds == alpha* re-run seeds
148
+ assert report.paired_comparisons, "paired_comparisons should be populated"
149
+ pc = report.paired_comparisons[0]
150
+ assert len(pc["pairs"]) == config.n_repetitions
151
+ seeds = [p["seed"] for p in pc["pairs"]]
152
+ assert len(set(seeds)) == config.n_repetitions
153
+ assert pc["alpha_star"] == report.best_result.alpha
154
+ # Every pair has both metrics
155
+ for p in pc["pairs"]:
156
+ assert "baseline_metric" in p and "alpha_star_metric" in p
File without changes
File without changes
File without changes
File without changes
File without changes