Skip to content

Commit 91a0986

Browse files
authored
Merge pull request #80 from bernalde/fix/issue-75-sa-reliability
Run SA tutorial reliability analysis
2 parents 5687c3c + e1952ba commit 91a0986

9 files changed

Lines changed: 4687 additions & 382 deletions

examples/QAOA_multipleSplits/qaoa_multiple_splits.ipynb

Lines changed: 99 additions & 35 deletions
Large diffs are not rendered by default.

examples/Simulated_Annealing/reliability_analysis.ipynb

Lines changed: 3038 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 299 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,299 @@
1+
"""Repeat-reliability helpers for the Simulated Annealing tutorial."""
2+
3+
from __future__ import annotations
4+
5+
import pickle
6+
import re
7+
from collections.abc import Mapping, Sequence
8+
from pathlib import Path
9+
10+
import numpy as np
11+
import pandas as pd
12+
13+
14+
DEFAULT_QUALITY_TARGET = 0.95
15+
DEFAULT_RELATIVE_ERROR_THRESHOLD = 0.10
16+
DEFAULT_TARGET_CONFIDENCE = 0.99
17+
DEFAULT_INSTANCE_IDS = (0, 1, 2)
18+
TARGET_REPEAT_COLUMN_RE = re.compile(r"^R\d+(?:_\d+)?$")
19+
20+
DIAGNOSTIC_COLUMNS = [
21+
"instance",
22+
"sweeps",
23+
"current_resource",
24+
"current_repeats",
25+
"required_repeats",
26+
"additional_repeats_required",
27+
"p_ci_lower",
28+
"p_ci_upper",
29+
"R_c",
30+
"R_c_ci_lower",
31+
"R_c_ci_upper",
32+
"R99",
33+
"R99_ci_lower",
34+
"R99_ci_upper",
35+
"CETS",
36+
"CETS_ci_lower",
37+
"CETS_ci_upper",
38+
"reliability_status",
39+
"statistically_unresolved",
40+
]
41+
42+
43+
def quality_threshold_from_baseline(
44+
best_value: float,
45+
random_value: float,
46+
quality_target: float = DEFAULT_QUALITY_TARGET,
47+
) -> float:
48+
"""Return the energy threshold induced by the tutorial performance ratio."""
49+
50+
if not 0.0 <= quality_target <= 1.0:
51+
raise ValueError("quality_target must be between 0 and 1")
52+
return float(random_value) + quality_target * (float(best_value) - float(random_value))
53+
54+
55+
def attach_quality_thresholds(
56+
runs: pd.DataFrame,
57+
baselines: Mapping[int, Mapping[str, float]] | pd.DataFrame,
58+
quality_target: float = DEFAULT_QUALITY_TARGET,
59+
) -> pd.DataFrame:
60+
"""Attach per-instance reliability success thresholds to SA run rows."""
61+
62+
if "instance" not in runs.columns:
63+
raise ValueError("runs must include an instance column")
64+
65+
baseline_frame = _baseline_frame(baselines)
66+
missing_instances = sorted(set(runs["instance"].unique()) - set(baseline_frame["instance"]))
67+
if missing_instances:
68+
raise ValueError(f"missing baselines for instances: {missing_instances}")
69+
70+
thresholds = baseline_frame.copy()
71+
thresholds["quality_target"] = quality_target
72+
thresholds["quality_threshold"] = thresholds.apply(
73+
lambda row: quality_threshold_from_baseline(
74+
row["best_value"],
75+
row["random_value"],
76+
quality_target=quality_target,
77+
),
78+
axis=1,
79+
)
80+
return runs.merge(
81+
thresholds[
82+
[
83+
"instance",
84+
"best_value",
85+
"random_value",
86+
"quality_target",
87+
"quality_threshold",
88+
]
89+
],
90+
on="instance",
91+
how="left",
92+
)
93+
94+
95+
def load_selected_granular_runs(
96+
example_dir: str | Path,
97+
instance_ids: Sequence[int] = DEFAULT_INSTANCE_IDS,
98+
) -> pd.DataFrame:
99+
"""Load real granular SA runs for a selected set of instances."""
100+
101+
example_path = Path(example_dir)
102+
frames = []
103+
for instance_id in instance_ids:
104+
data_path = example_path / "granular_data" / f"granular_data_{instance_id}.pkl"
105+
if not data_path.is_file():
106+
raise FileNotFoundError(f"granular SA data not found: {data_path}")
107+
frames.append(pd.read_pickle(data_path))
108+
109+
if not frames:
110+
raise ValueError("at least one instance_id is required")
111+
return pd.concat(frames, ignore_index=True)
112+
113+
114+
def load_selected_raw_runs(
115+
example_dir: str | Path,
116+
instance_ids: Sequence[int] = DEFAULT_INSTANCE_IDS,
117+
) -> pd.DataFrame:
118+
"""Load selected SA runs from the tracked aggregate raw-runs pickle."""
119+
120+
raw_runs_path = Path(example_dir) / "results" / "all_raw_runs.pkl"
121+
if not raw_runs_path.is_file():
122+
raise FileNotFoundError(f"aggregate SA raw runs not found: {raw_runs_path}")
123+
124+
with raw_runs_path.open("rb") as raw_runs_file:
125+
all_raw_runs = pickle.load(raw_runs_file)
126+
127+
frames = []
128+
for instance_id in instance_ids:
129+
if instance_id not in all_raw_runs:
130+
raise ValueError(f"missing raw runs for instance: {instance_id}")
131+
132+
for sweeps, energies in all_raw_runs[instance_id].items():
133+
frame = pd.DataFrame(
134+
{
135+
"instance": instance_id,
136+
"sweeps": int(sweeps),
137+
"energy": pd.Series(energies, dtype=float),
138+
"resource": 1,
139+
}
140+
)
141+
frames.append(frame)
142+
143+
if not frames:
144+
raise ValueError("at least one instance_id is required")
145+
return pd.concat(frames, ignore_index=True)
146+
147+
148+
def load_targeted_rerun_runs(example_dir: str | Path) -> pd.DataFrame:
149+
"""Load the committed targeted SA reruns used by the reliability tutorial."""
150+
151+
results_path = Path(example_dir) / "results"
152+
data_path = results_path / "targeted_sa_reruns.npz"
153+
if not data_path.is_file():
154+
raise FileNotFoundError(f"targeted SA rerun data not found: {data_path}")
155+
156+
with np.load(data_path) as data:
157+
missing = {"instance", "sweeps", "energy", "resource"} - set(data.files)
158+
if missing:
159+
raise ValueError(f"targeted SA rerun data missing arrays: {sorted(missing)}")
160+
reruns = pd.DataFrame(
161+
{
162+
"instance": data["instance"].astype(int),
163+
"sweeps": data["sweeps"].astype(int),
164+
"energy": data["energy"].astype(float),
165+
"resource": data["resource"].astype(int),
166+
}
167+
)
168+
169+
return reruns
170+
171+
172+
def run_reliability_analysis(
173+
benchmark,
174+
runs: pd.DataFrame,
175+
*,
176+
quality_target: float = DEFAULT_QUALITY_TARGET,
177+
relative_error_threshold: float = DEFAULT_RELATIVE_ERROR_THRESHOLD,
178+
target_confidence: float = DEFAULT_TARGET_CONFIDENCE,
179+
) -> pd.DataFrame:
180+
"""Run the tutorial reliability report through stochastic_benchmark."""
181+
182+
report = benchmark.run_RepeatReliability(
183+
runs,
184+
group_cols=["instance", "sweeps"],
185+
response_col="energy",
186+
success_rule="min",
187+
threshold="quality_threshold",
188+
iterations="sweeps",
189+
effort_per_iteration=1.0,
190+
comparison_cols="instance",
191+
relative_error_threshold=relative_error_threshold,
192+
target_confidence=target_confidence,
193+
)
194+
195+
report = _add_tutorial_context(report, runs, quality_target)
196+
benchmark.repeat_reliability = report
197+
return report
198+
199+
200+
def select_reliability_diagnostics(
201+
report: pd.DataFrame,
202+
rows_per_instance: int = 4,
203+
) -> pd.DataFrame:
204+
"""Select compact diagnostics, prioritizing unresolved or under-sampled rows."""
205+
206+
if rows_per_instance <= 0:
207+
raise ValueError("rows_per_instance must be positive")
208+
diagnostic_columns = _diagnostic_columns_for_report(report)
209+
if report.empty:
210+
return report.reindex(columns=diagnostic_columns)
211+
212+
work = report.copy()
213+
work["_needs_more_trials"] = work["reliability_status"].ne("reliable")
214+
work["_unresolved"] = work["statistically_unresolved"].astype(bool)
215+
work["_additional_repeats_sort"] = pd.to_numeric(
216+
work["additional_repeats_required"],
217+
errors="coerce",
218+
).fillna(0)
219+
selected = (
220+
work.sort_values(
221+
[
222+
"instance",
223+
"_needs_more_trials",
224+
"_unresolved",
225+
"_additional_repeats_sort",
226+
"current_resource",
227+
"sweeps",
228+
],
229+
ascending=[True, False, False, False, True, True],
230+
)
231+
.groupby("instance", group_keys=False)
232+
.head(rows_per_instance)
233+
)
234+
return selected[diagnostic_columns]
235+
236+
237+
def _baseline_frame(
238+
baselines: Mapping[int, Mapping[str, float]] | pd.DataFrame,
239+
) -> pd.DataFrame:
240+
if isinstance(baselines, pd.DataFrame):
241+
baseline_frame = baselines.copy()
242+
else:
243+
baseline_frame = pd.DataFrame.from_records(
244+
[
245+
{
246+
"instance": instance,
247+
"best_value": values["best_value"],
248+
"random_value": values["random_value"],
249+
}
250+
for instance, values in baselines.items()
251+
]
252+
)
253+
254+
required = {"instance", "best_value", "random_value"}
255+
missing = required - set(baseline_frame.columns)
256+
if missing:
257+
raise ValueError(f"baselines missing required columns: {sorted(missing)}")
258+
return baseline_frame.loc[:, ["instance", "best_value", "random_value"]]
259+
260+
261+
def _diagnostic_columns_for_report(report: pd.DataFrame) -> list[str]:
262+
columns = [col for col in DIAGNOSTIC_COLUMNS if col in report.columns]
263+
seen = set(columns)
264+
for column in report.columns:
265+
if not _is_target_repeat_column(column):
266+
continue
267+
for repeat_column in [column, f"{column}_ci_lower", f"{column}_ci_upper"]:
268+
if repeat_column in report.columns and repeat_column not in seen:
269+
columns.append(repeat_column)
270+
seen.add(repeat_column)
271+
return columns
272+
273+
274+
def _is_target_repeat_column(column: str) -> bool:
275+
return bool(TARGET_REPEAT_COLUMN_RE.fullmatch(column))
276+
277+
278+
def _add_tutorial_context(
279+
report: pd.DataFrame,
280+
runs: pd.DataFrame,
281+
quality_target: float,
282+
) -> pd.DataFrame:
283+
thresholds = runs[
284+
[
285+
"instance",
286+
"best_value",
287+
"random_value",
288+
"quality_target",
289+
"quality_threshold",
290+
]
291+
].drop_duplicates("instance")
292+
enriched = report.merge(thresholds, on="instance", how="left")
293+
enriched["quality_target"] = quality_target
294+
enriched["current_repeats"] = enriched["trials"]
295+
enriched["required_repeats"] = enriched["required_trials"]
296+
enriched["additional_repeats_required"] = enriched["additional_trials_required"]
297+
enriched["current_resource"] = enriched["sweeps"] * enriched["current_repeats"]
298+
enriched["required_resource"] = enriched["sweeps"] * enriched["required_repeats"]
299+
return enriched
1.45 MB
Binary file not shown.

0 commit comments

Comments
 (0)