From 80eb44f0c1e74ac2e908095be506f6030e82bb15 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 2 Sep 2026 08:30:07 +0000 Subject: [PATCH 1/5] Add Qiskit route construction and example Co-authored-by: fgfuchs <2428162+fgfuchs@users.noreply.github.com> --- README.md | 26 ++- qiskit_route_construction_example.py | 77 ++++++++ requirements.txt | 4 + src/qiskit_route_construction.py | 224 ++++++++++++++++++++++++ tests/test_qiskit_route_construction.py | 107 +++++++++++ 5 files changed, 437 insertions(+), 1 deletion(-) create mode 100644 qiskit_route_construction_example.py create mode 100644 src/qiskit_route_construction.py create mode 100644 tests/test_qiskit_route_construction.py diff --git a/README.md b/README.md index 2e19536..30a319b 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,8 @@ This repository is a small simulation sandbox for exploring quantum-inspired wor It currently includes: - a baseline phase-encoding pipeline, - an option-1 QSVT pipeline using a nonnegative quality map and monotonic filter, -- a thresholded Grover pipeline (`cost_norm <= tau` marking). +- a thresholded Grover pipeline (`cost_norm <= tau` marking), +- a Qiskit construction with route register, reversible validity oracle, validity-controlled cost-phase oracle, and uncomputation. All experiments are currently set up for small `n` (default `n=5`) and are meant for simulator study, not scalable production solving. @@ -33,6 +34,13 @@ All experiments are currently set up for small `n` (default `n=5`) and are meant - sweeps multiple threshold values, - runs an adaptive threshold-tightening loop from sampled costs, - writes per-threshold outputs plus grid/adaptive summaries. +- `src/qiskit_route_construction.py` + - Qiskit building blocks for: + - route register superposition, + - reversible validity compute/uncompute, + - validity-controlled cost-phase oracle. +- `qiskit_route_construction_example.py` + - Qiskit Aer statevector example that builds the per-route table (`tour`, `phi`, `validity`) and plots all entries. ## High-Level Flow @@ -117,6 +125,22 @@ Output: - `tsp_threshold_grover_best_samples.csv` - `tsp_threshold_grover_best_ranking.csv` +### Qiskit route/validity/cost-phase construction (Aer statevector) + +```bash +python3 qiskit_route_construction_example.py --n 5 --seed 42 +``` + +Output: +- `qiskit_tsp_table.csv` +- `qiskit_tsp_table.png` (plot of the full table entries) + +Run tests: + +```bash +pytest -q +``` + ## Notes - These scripts use `lightning.qubit` (`pennylane-lightning` plugin). diff --git a/qiskit_route_construction_example.py b/qiskit_route_construction_example.py new file mode 100644 index 0000000..11e899c --- /dev/null +++ b/qiskit_route_construction_example.py @@ -0,0 +1,77 @@ +import argparse + +import matplotlib.pyplot as plt +import numpy as np +from qiskit import transpile +from qiskit_aer import Aer + +import src.classical_funcs as cf +from src.qiskit_route_construction import ( + build_qiskit_tsp_construction_circuit, + build_route_phase_table, +) + + +def build_normalized_cost_matrix(n: int, seed: int) -> np.ndarray: + np.random.seed(seed) + raw = cf.generate_cost_matrix(n) + walks = cf.generate_all_walks(n, start_node=n - 1) + all_costs = cf.find_all_cost(raw, walks) + return raw / float(np.max(all_costs)) + + +def plot_table(df, out_path: str) -> None: + fig_height = max(8, 0.18 * len(df)) + fig, ax = plt.subplots(figsize=(14, fig_height)) + ax.axis("off") + table = ax.table( + cellText=df.values, + colLabels=df.columns, + loc="center", + cellLoc="center", + ) + table.auto_set_font_size(False) + table.set_fontsize(7) + table.scale(1.0, 1.1) + fig.tight_layout() + fig.savefig(out_path, dpi=200, bbox_inches="tight") + plt.close(fig) + + +def main() -> None: + parser = argparse.ArgumentParser( + description="Qiskit Aer statevector example for route/validity/cost-phase construction." + ) + parser.add_argument("--n", type=int, default=5, help="Number of cities.") + parser.add_argument("--seed", type=int, default=42, help="Random seed.") + parser.add_argument("--csv", default="qiskit_tsp_table.csv", help="Output CSV path.") + parser.add_argument("--plot", default="qiskit_tsp_table.png", help="Output plot path.") + args = parser.parse_args() + + cost_matrix = build_normalized_cost_matrix(args.n, args.seed) + circuit, layout = build_qiskit_tsp_construction_circuit( + cost_matrix=cost_matrix, + start_node=args.n - 1, + ) + + backend = Aer.get_backend("statevector_simulator") + compiled = transpile(circuit, backend=backend) + result = backend.run(compiled).result() + statevector = np.asarray(result.get_statevector(compiled)) + + df = build_route_phase_table( + statevector=statevector, + layout=layout, + cost_matrix=cost_matrix, + ) + df = df.sort_values(by=["validity", "expected_phi", "tour"], ascending=[False, True, True]).reset_index(drop=True) + df.to_csv(args.csv, index=False) + plot_table(df, args.plot) + + print(f"Saved table CSV: {args.csv}") + print(f"Saved table plot: {args.plot}") + print(df.head(20).to_string(index=False)) + + +if __name__ == "__main__": + main() diff --git a/requirements.txt b/requirements.txt index a8d0a3b..57f11b8 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,3 +2,7 @@ numpy pandas pennylane pennylane-lightning +matplotlib +pytest +qiskit +qiskit-aer diff --git a/src/qiskit_route_construction.py b/src/qiskit_route_construction.py new file mode 100644 index 0000000..2ae9fff --- /dev/null +++ b/src/qiskit_route_construction.py @@ -0,0 +1,224 @@ +import itertools +import math +from dataclasses import dataclass + +import numpy as np +import pandas as pd +from qiskit import QuantumCircuit + + +@dataclass(frozen=True) +class RouteLayout: + n: int + start_node: int + steps: int + bits_per_step: int + route_wires_by_step: list[list[int]] + route_wires: list[int] + state_wires: list[int] + good_wire: int + phase_wire: int + num_qubits: int + + +def build_route_layout(n: int, start_node: int | None = None) -> RouteLayout: + if n < 3: + raise ValueError("n must be at least 3.") + if start_node is None: + start_node = n - 1 + if not 0 <= start_node < n: + raise ValueError("start_node must be in [0, n-1].") + + steps = n - 1 + bits_per_step = int(math.ceil(math.log2(n - 1))) + route_wires_by_step: list[list[int]] = [] + cursor = 0 + for _ in range(steps): + step_wires = list(range(cursor, cursor + bits_per_step)) + route_wires_by_step.append(step_wires) + cursor += bits_per_step + + route_wires = [w for step in route_wires_by_step for w in step] + state_wires = list(range(cursor, cursor + (n - 1))) + cursor += n - 1 + good_wire = cursor + cursor += 1 + phase_wire = cursor + cursor += 1 + + return RouteLayout( + n=n, + start_node=start_node, + steps=steps, + bits_per_step=bits_per_step, + route_wires_by_step=route_wires_by_step, + route_wires=route_wires, + state_wires=state_wires, + good_wire=good_wire, + phase_wire=phase_wire, + num_qubits=cursor, + ) + + +def _mcx_on_value(circuit: QuantumCircuit, controls: list[int], bitstring: str, target: int) -> None: + for wire, bit in zip(controls, bitstring): + if bit == "0": + circuit.x(wire) + circuit.mcx(controls, target) + for wire, bit in zip(controls, bitstring): + if bit == "0": + circuit.x(wire) + + +def add_route_register_superposition(circuit: QuantumCircuit, layout: RouteLayout) -> None: + for wire in layout.route_wires: + circuit.h(wire) + + +def add_validity_oracle_compute(circuit: QuantumCircuit, layout: RouteLayout) -> None: + for step_wires in layout.route_wires_by_step: + for city in range(layout.n - 1): + bits = format(city, f"0{layout.bits_per_step}b") + _mcx_on_value(circuit, step_wires, bits, layout.state_wires[city]) + circuit.mcx(layout.state_wires, layout.good_wire) + + +def add_validity_oracle_uncompute(circuit: QuantumCircuit, layout: RouteLayout) -> None: + circuit.mcx(layout.state_wires, layout.good_wire) + for step_wires in reversed(layout.route_wires_by_step): + for city in reversed(range(layout.n - 1)): + bits = format(city, f"0{layout.bits_per_step}b") + _mcx_on_value(circuit, step_wires, bits, layout.state_wires[city]) + + +def _apply_phase_kickback( + circuit: QuantumCircuit, + controls: list[int], + control_bits: str, + phase_wire: int, + angle: float, +) -> None: + for wire, bit in zip(controls, control_bits): + if bit == "0": + circuit.x(wire) + circuit.mcx(controls, phase_wire) + circuit.p(float(angle), phase_wire) + circuit.mcx(controls, phase_wire) + for wire, bit in zip(controls, control_bits): + if bit == "0": + circuit.x(wire) + + +def add_validity_controlled_cost_phase_oracle( + circuit: QuantumCircuit, + layout: RouteLayout, + cost_matrix: np.ndarray, +) -> None: + if cost_matrix.shape != (layout.n, layout.n): + raise ValueError("cost_matrix shape must be (n, n).") + + first_step = layout.route_wires_by_step[0] + for city in range(layout.n - 1): + city_bits = format(city, f"0{layout.bits_per_step}b") + controls = [layout.good_wire] + first_step + bits = "1" + city_bits + _apply_phase_kickback( + circuit=circuit, + controls=controls, + control_bits=bits, + phase_wire=layout.phase_wire, + angle=float(cost_matrix[layout.start_node, city]), + ) + + for t in range(layout.steps - 1): + from_step = layout.route_wires_by_step[t] + to_step = layout.route_wires_by_step[t + 1] + controls = [layout.good_wire] + from_step + to_step + for i in range(layout.n - 1): + i_bits = format(i, f"0{layout.bits_per_step}b") + for j in range(layout.n - 1): + j_bits = format(j, f"0{layout.bits_per_step}b") + _apply_phase_kickback( + circuit=circuit, + controls=controls, + control_bits="1" + i_bits + j_bits, + phase_wire=layout.phase_wire, + angle=float(cost_matrix[i, j]), + ) + + last_step = layout.route_wires_by_step[-1] + for city in range(layout.n - 1): + city_bits = format(city, f"0{layout.bits_per_step}b") + controls = [layout.good_wire] + last_step + bits = "1" + city_bits + _apply_phase_kickback( + circuit=circuit, + controls=controls, + control_bits=bits, + phase_wire=layout.phase_wire, + angle=float(cost_matrix[city, layout.start_node]), + ) + + +def build_qiskit_tsp_construction_circuit( + cost_matrix: np.ndarray, + start_node: int | None = None, +) -> tuple[QuantumCircuit, RouteLayout]: + n = int(cost_matrix.shape[0]) + layout = build_route_layout(n=n, start_node=start_node) + circuit = QuantumCircuit(layout.num_qubits) + + add_route_register_superposition(circuit, layout) + add_validity_oracle_compute(circuit, layout) + add_validity_controlled_cost_phase_oracle(circuit, layout, cost_matrix) + add_validity_oracle_uncompute(circuit, layout) + + return circuit, layout + + +def classical_route_validity(route: tuple[int, ...] | list[int], n: int) -> bool: + return sorted(route) == list(range(n - 1)) + + +def classical_route_cost(cost_matrix: np.ndarray, route: tuple[int, ...] | list[int], start_node: int) -> float: + cost = float(cost_matrix[start_node, route[0]]) + for i in range(len(route) - 1): + cost += float(cost_matrix[route[i], route[i + 1]]) + cost += float(cost_matrix[route[-1], start_node]) + return float(cost) + + +def route_basis_index(route: tuple[int, ...] | list[int], layout: RouteLayout) -> int: + idx = 0 + for step_wires, city in zip(layout.route_wires_by_step, route): + bits = format(int(city), f"0{layout.bits_per_step}b") + for wire, bit in zip(step_wires, bits): + if bit == "1": + idx |= 1 << wire + return idx + + +def wrap_to_pi(angle: float) -> float: + return float((angle + np.pi) % (2.0 * np.pi) - np.pi) + + +def build_route_phase_table( + statevector: np.ndarray, + layout: RouteLayout, + cost_matrix: np.ndarray, +) -> pd.DataFrame: + rows = [] + for route in itertools.product(range(layout.n - 1), repeat=layout.steps): + idx = route_basis_index(route, layout) + amp = statevector[idx] + validity = int(classical_route_validity(route, n=layout.n)) + rows.append( + { + "tour": list(route), + "validity": validity, + "phi": round(wrap_to_pi(float(np.angle(amp))), 8), + "expected_phi": round(classical_route_cost(cost_matrix, route, layout.start_node), 8), + "prob": round(float(np.abs(amp) ** 2), 8), + } + ) + return pd.DataFrame(rows) diff --git a/tests/test_qiskit_route_construction.py b/tests/test_qiskit_route_construction.py new file mode 100644 index 0000000..6d183ca --- /dev/null +++ b/tests/test_qiskit_route_construction.py @@ -0,0 +1,107 @@ +import itertools +import math + +import numpy as np +from qiskit import QuantumCircuit +from qiskit.quantum_info import Statevector + +from src.qiskit_route_construction import ( + add_route_register_superposition, + add_validity_controlled_cost_phase_oracle, + add_validity_oracle_compute, + add_validity_oracle_uncompute, + build_qiskit_tsp_construction_circuit, + build_route_layout, + classical_route_cost, + classical_route_validity, + route_basis_index, + wrap_to_pi, +) + + +def _test_cost_matrix() -> np.ndarray: + return np.array( + [ + [0.0, 0.2, 0.5, 0.4], + [0.3, 0.0, 0.1, 0.6], + [0.7, 0.4, 0.0, 0.2], + [0.8, 0.3, 0.9, 0.0], + ] + ) + + +def _parity_bits(route, n): + return [sum(1 for city in route if city == i) % 2 for i in range(n - 1)] + + +def _amp_phase(statevector: np.ndarray, idx: int) -> float: + return float(np.angle(statevector[idx])) + + +def test_validity_oracle_marks_good_for_permutations_only(): + n = 4 + layout = build_route_layout(n=n, start_node=n - 1) + circuit = QuantumCircuit(layout.num_qubits) + add_route_register_superposition(circuit, layout) + add_validity_oracle_compute(circuit, layout) + + state = np.asarray(Statevector.from_instruction(circuit).data) + expected_abs = 1.0 / math.sqrt(2 ** len(layout.route_wires)) + + for route in itertools.product(range(n - 1), repeat=layout.steps): + idx = route_basis_index(route, layout) + parity = _parity_bits(route, n) + for wire, bit in zip(layout.state_wires, parity): + if bit: + idx |= 1 << wire + is_valid = classical_route_validity(route, n=n) + if is_valid: + idx |= 1 << layout.good_wire + amp = state[idx] + assert np.isclose(abs(amp), expected_abs, atol=1e-12) + + +def test_validity_controlled_phase_changes_only_valid_routes(): + cost_matrix = _test_cost_matrix() + n = cost_matrix.shape[0] + circuit, layout = build_qiskit_tsp_construction_circuit(cost_matrix=cost_matrix, start_node=n - 1) + state = np.asarray(Statevector.from_instruction(circuit).data) + + invalid_routes = [r for r in itertools.product(range(n - 1), repeat=layout.steps) if not classical_route_validity(r, n=n)] + invalid_phases = [_amp_phase(state, route_basis_index(route, layout)) for route in invalid_routes] + for phase in invalid_phases[1:]: + assert np.isclose(wrap_to_pi(phase - invalid_phases[0]), 0.0, atol=1e-10) + + valid_routes = [r for r in itertools.product(range(n - 1), repeat=layout.steps) if classical_route_validity(r, n=n)] + ref_route = valid_routes[0] + ref_phase = _amp_phase(state, route_basis_index(ref_route, layout)) + ref_cost = classical_route_cost(cost_matrix, ref_route, start_node=layout.start_node) + for route in valid_routes[1:]: + phase = _amp_phase(state, route_basis_index(route, layout)) + cost = classical_route_cost(cost_matrix, route, start_node=layout.start_node) + assert np.isclose( + wrap_to_pi((phase - ref_phase) - (cost - ref_cost)), + 0.0, + atol=1e-10, + ) + + +def test_uncomputation_resets_all_ancilla_registers(): + cost_matrix = _test_cost_matrix() + n = cost_matrix.shape[0] + layout = build_route_layout(n=n, start_node=n - 1) + circuit = QuantumCircuit(layout.num_qubits) + add_route_register_superposition(circuit, layout) + add_validity_oracle_compute(circuit, layout) + add_validity_controlled_cost_phase_oracle(circuit, layout, cost_matrix) + add_validity_oracle_uncompute(circuit, layout) + + state = np.asarray(Statevector.from_instruction(circuit).data) + ancillas = layout.state_wires + [layout.good_wire, layout.phase_wire] + leaked_probability = 0.0 + for idx, amp in enumerate(state): + if abs(amp) < 1e-14: + continue + if any(((idx >> wire) & 1) for wire in ancillas): + leaked_probability += float(abs(amp) ** 2) + assert np.isclose(leaked_probability, 0.0, atol=1e-12) From ba97ad3b06cc40e0d0d2361bfb277259026faf5e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 2 Sep 2026 08:34:13 +0000 Subject: [PATCH 2/5] Fix review feedback and finalize Qiskit implementation Co-authored-by: fgfuchs <2428162+fgfuchs@users.noreply.github.com> --- qiskit_route_construction_example.py | 9 ++++--- src/__init__.py | 0 src/qiskit_route_construction.py | 32 ++++++++++++++++++------- tests/conftest.py | 6 +++++ tests/test_qiskit_route_construction.py | 8 +++++++ 5 files changed, 44 insertions(+), 11 deletions(-) create mode 100644 src/__init__.py create mode 100644 tests/conftest.py diff --git a/qiskit_route_construction_example.py b/qiskit_route_construction_example.py index 11e899c..ed24e1c 100644 --- a/qiskit_route_construction_example.py +++ b/qiskit_route_construction_example.py @@ -13,8 +13,9 @@ def build_normalized_cost_matrix(n: int, seed: int) -> np.ndarray: - np.random.seed(seed) - raw = cf.generate_cost_matrix(n) + rng = np.random.default_rng(seed) + raw = rng.uniform(0.0, 1.0, size=(n, n)) + np.fill_diagonal(raw, 0.0) walks = cf.generate_all_walks(n, start_node=n - 1) all_costs = cf.find_all_cost(raw, walks) return raw / float(np.max(all_costs)) @@ -64,7 +65,9 @@ def main() -> None: layout=layout, cost_matrix=cost_matrix, ) - df = df.sort_values(by=["validity", "expected_phi", "tour"], ascending=[False, True, True]).reset_index(drop=True) + df["tour_key"] = df["tour"].apply(tuple) + df = df.sort_values(by=["validity", "expected_phi", "tour_key"], ascending=[False, True, True]).reset_index(drop=True) + df = df.drop(columns=["tour_key"]) df.to_csv(args.csv, index=False) plot_table(df, args.plot) diff --git a/src/__init__.py b/src/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/qiskit_route_construction.py b/src/qiskit_route_construction.py index 2ae9fff..a876561 100644 --- a/src/qiskit_route_construction.py +++ b/src/qiskit_route_construction.py @@ -13,6 +13,7 @@ class RouteLayout: start_node: int steps: int bits_per_step: int + route_city_nodes: list[int] route_wires_by_step: list[list[int]] route_wires: list[int] state_wires: list[int] @@ -31,6 +32,7 @@ def build_route_layout(n: int, start_node: int | None = None) -> RouteLayout: steps = n - 1 bits_per_step = int(math.ceil(math.log2(n - 1))) + route_city_nodes = [node for node in range(n) if node != start_node] route_wires_by_step: list[list[int]] = [] cursor = 0 for _ in range(steps): @@ -51,6 +53,7 @@ def build_route_layout(n: int, start_node: int | None = None) -> RouteLayout: start_node=start_node, steps=steps, bits_per_step=bits_per_step, + route_city_nodes=route_city_nodes, route_wires_by_step=route_wires_by_step, route_wires=route_wires, state_wires=state_wires, @@ -119,6 +122,7 @@ def add_validity_controlled_cost_phase_oracle( first_step = layout.route_wires_by_step[0] for city in range(layout.n - 1): + actual_city = layout.route_city_nodes[city] city_bits = format(city, f"0{layout.bits_per_step}b") controls = [layout.good_wire] + first_step bits = "1" + city_bits @@ -127,7 +131,7 @@ def add_validity_controlled_cost_phase_oracle( controls=controls, control_bits=bits, phase_wire=layout.phase_wire, - angle=float(cost_matrix[layout.start_node, city]), + angle=float(cost_matrix[layout.start_node, actual_city]), ) for t in range(layout.steps - 1): @@ -135,19 +139,22 @@ def add_validity_controlled_cost_phase_oracle( to_step = layout.route_wires_by_step[t + 1] controls = [layout.good_wire] + from_step + to_step for i in range(layout.n - 1): + actual_i = layout.route_city_nodes[i] i_bits = format(i, f"0{layout.bits_per_step}b") for j in range(layout.n - 1): + actual_j = layout.route_city_nodes[j] j_bits = format(j, f"0{layout.bits_per_step}b") _apply_phase_kickback( circuit=circuit, controls=controls, control_bits="1" + i_bits + j_bits, phase_wire=layout.phase_wire, - angle=float(cost_matrix[i, j]), + angle=float(cost_matrix[actual_i, actual_j]), ) last_step = layout.route_wires_by_step[-1] for city in range(layout.n - 1): + actual_city = layout.route_city_nodes[city] city_bits = format(city, f"0{layout.bits_per_step}b") controls = [layout.good_wire] + last_step bits = "1" + city_bits @@ -156,7 +163,7 @@ def add_validity_controlled_cost_phase_oracle( controls=controls, control_bits=bits, phase_wire=layout.phase_wire, - angle=float(cost_matrix[city, layout.start_node]), + angle=float(cost_matrix[actual_city, layout.start_node]), ) @@ -181,10 +188,13 @@ def classical_route_validity(route: tuple[int, ...] | list[int], n: int) -> bool def classical_route_cost(cost_matrix: np.ndarray, route: tuple[int, ...] | list[int], start_node: int) -> float: - cost = float(cost_matrix[start_node, route[0]]) - for i in range(len(route) - 1): - cost += float(cost_matrix[route[i], route[i + 1]]) - cost += float(cost_matrix[route[-1], start_node]) + n = int(cost_matrix.shape[0]) + route_city_nodes = [node for node in range(n) if node != start_node] + route_nodes = [route_city_nodes[int(city)] for city in route] + cost = float(cost_matrix[start_node, route_nodes[0]]) + for i in range(len(route_nodes) - 1): + cost += float(cost_matrix[route_nodes[i], route_nodes[i + 1]]) + cost += float(cost_matrix[route_nodes[-1], start_node]) return float(cost) @@ -207,17 +217,23 @@ def build_route_phase_table( layout: RouteLayout, cost_matrix: np.ndarray, ) -> pd.DataFrame: + """Build a logical-route table with tour labels, validity, phase, expected phase, and probability.""" rows = [] for route in itertools.product(range(layout.n - 1), repeat=layout.steps): idx = route_basis_index(route, layout) amp = statevector[idx] validity = int(classical_route_validity(route, n=layout.n)) + expected_phi = ( + round(classical_route_cost(cost_matrix, route, layout.start_node), 8) + if validity + else np.nan + ) rows.append( { "tour": list(route), "validity": validity, "phi": round(wrap_to_pi(float(np.angle(amp))), 8), - "expected_phi": round(classical_route_cost(cost_matrix, route, layout.start_node), 8), + "expected_phi": expected_phi, "prob": round(float(np.abs(amp) ** 2), 8), } ) diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..2a855d9 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,6 @@ +import sys +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] +if str(ROOT) not in sys.path: + sys.path.insert(0, str(ROOT)) diff --git a/tests/test_qiskit_route_construction.py b/tests/test_qiskit_route_construction.py index 6d183ca..a17efe6 100644 --- a/tests/test_qiskit_route_construction.py +++ b/tests/test_qiskit_route_construction.py @@ -66,6 +66,14 @@ def test_validity_controlled_phase_changes_only_valid_routes(): n = cost_matrix.shape[0] circuit, layout = build_qiskit_tsp_construction_circuit(cost_matrix=cost_matrix, start_node=n - 1) state = np.asarray(Statevector.from_instruction(circuit).data) + ancillas = layout.state_wires + [layout.good_wire, layout.phase_wire] + leaked_probability = 0.0 + for idx, amp in enumerate(state): + if abs(amp) < 1e-14: + continue + if any(((idx >> wire) & 1) for wire in ancillas): + leaked_probability += float(abs(amp) ** 2) + assert np.isclose(leaked_probability, 0.0, atol=1e-12) invalid_routes = [r for r in itertools.product(range(n - 1), repeat=layout.steps) if not classical_route_validity(r, n=n)] invalid_phases = [_amp_phase(state, route_basis_index(route, layout)) for route in invalid_routes] From d062d170b239041e9c0b3d7850a33435b13b0097 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 2 Sep 2026 09:09:56 +0000 Subject: [PATCH 3/5] Convert Qiskit example to notebook Co-authored-by: fgfuchs <2428162+fgfuchs@users.noreply.github.com> --- README.md | 10 +- qiskit_route_construction_example.ipynb | 176 ++++++++++++++++++++++++ qiskit_route_construction_example.py | 80 ----------- 3 files changed, 183 insertions(+), 83 deletions(-) create mode 100644 qiskit_route_construction_example.ipynb delete mode 100644 qiskit_route_construction_example.py diff --git a/README.md b/README.md index 30a319b..a77aa3e 100644 --- a/README.md +++ b/README.md @@ -39,8 +39,8 @@ All experiments are currently set up for small `n` (default `n=5`) and are meant - route register superposition, - reversible validity compute/uncompute, - validity-controlled cost-phase oracle. -- `qiskit_route_construction_example.py` - - Qiskit Aer statevector example that builds the per-route table (`tour`, `phi`, `validity`) and plots all entries. +- `qiskit_route_construction_example.ipynb` + - Jupyter notebook using Qiskit Aer statevector simulation to build the per-route table (`tour`, `phi`, `validity`), plot all table entries, plot feasible/infeasible bitstrings, and plot the circuit. ## High-Level Flow @@ -127,13 +127,17 @@ Output: ### Qiskit route/validity/cost-phase construction (Aer statevector) +Open: + ```bash -python3 qiskit_route_construction_example.py --n 5 --seed 42 +jupyter notebook /home/runner/work/TSP/TSP/qiskit_route_construction_example.ipynb ``` Output: - `qiskit_tsp_table.csv` - `qiskit_tsp_table.png` (plot of the full table entries) +- `qiskit_tsp_validity_phi.png` (feasible and infeasible bitstring phase plot) +- `qiskit_tsp_circuit.png` (circuit plot) Run tests: diff --git a/qiskit_route_construction_example.ipynb b/qiskit_route_construction_example.ipynb new file mode 100644 index 0000000..ea5d434 --- /dev/null +++ b/qiskit_route_construction_example.ipynb @@ -0,0 +1,176 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Qiskit route construction example\n", + "\n", + "This notebook builds the route register, reversible validity oracle, validity-controlled cost-phase oracle, and uncomputation, then reproduces the tours/\\(\\phi\\)/validity table with Aer statevector simulation." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import matplotlib.pyplot as plt\n", + "import numpy as np\n", + "from qiskit import transpile\n", + "from qiskit.visualization import circuit_drawer\n", + "from qiskit_aer import Aer\n", + "\n", + "import src.classical_funcs as cf\n", + "from src.qiskit_route_construction import (\n", + " build_qiskit_tsp_construction_circuit,\n", + " build_route_phase_table,\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "n = 5\n", + "seed = 42\n", + "csv_out = 'qiskit_tsp_table.csv'\n", + "table_plot_out = 'qiskit_tsp_table.png'\n", + "validity_plot_out = 'qiskit_tsp_validity_phi.png'\n", + "circuit_plot_out = 'qiskit_tsp_circuit.png'" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def build_normalized_cost_matrix(n: int, seed: int) -> np.ndarray:\n", + " rng = np.random.default_rng(seed)\n", + " raw = rng.uniform(0.0, 1.0, size=(n, n))\n", + " np.fill_diagonal(raw, 0.0)\n", + " walks = cf.generate_all_walks(n, start_node=n - 1)\n", + " all_costs = cf.find_all_cost(raw, walks)\n", + " return raw / float(np.max(all_costs))\n", + "\n", + "def route_to_bitstring(route, bits_per_step: int) -> str:\n", + " return ''.join(format(int(city), f'0{bits_per_step}b') for city in route)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "cost_matrix = build_normalized_cost_matrix(n=n, seed=seed)\n", + "circuit, layout = build_qiskit_tsp_construction_circuit(cost_matrix=cost_matrix, start_node=n - 1)\n", + "\n", + "backend = Aer.get_backend('statevector_simulator')\n", + "compiled = transpile(circuit, backend=backend)\n", + "result = backend.run(compiled).result()\n", + "statevector = np.asarray(result.get_statevector(compiled))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "df = build_route_phase_table(statevector=statevector, layout=layout, cost_matrix=cost_matrix)\n", + "df['bitstring'] = df['tour'].apply(lambda tour: route_to_bitstring(tour, layout.bits_per_step))\n", + "df['tour_key'] = df['tour'].apply(tuple)\n", + "df = df.sort_values(by=['validity', 'expected_phi', 'tour_key'], ascending=[False, True, True]).reset_index(drop=True)\n", + "df = df.drop(columns=['tour_key'])\n", + "df.to_csv(csv_out, index=False)\n", + "df" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "fig_height = max(8, 0.18 * len(df))\n", + "fig, ax = plt.subplots(figsize=(16, fig_height))\n", + "ax.axis('off')\n", + "table = ax.table(cellText=df.values, colLabels=df.columns, loc='center', cellLoc='center')\n", + "table.auto_set_font_size(False)\n", + "table.set_fontsize(6)\n", + "table.scale(1.0, 1.1)\n", + "fig.tight_layout()\n", + "fig.savefig(table_plot_out, dpi=200, bbox_inches='tight')\n", + "plt.show()\n", + "plt.close(fig)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "df_plot = df.copy()\n", + "df_plot['row'] = np.arange(len(df_plot))\n", + "\n", + "valid = df_plot[df_plot['validity'] == 1]\n", + "invalid = df_plot[df_plot['validity'] == 0]\n", + "\n", + "fig, ax = plt.subplots(figsize=(16, 6))\n", + "ax.scatter(valid['row'], valid['phi'], s=30, label='feasible', color='tab:green')\n", + "ax.scatter(invalid['row'], invalid['phi'], s=16, label='infeasible', color='tab:red', alpha=0.6)\n", + "ax.set_xlabel('bitstring row index')\n", + "ax.set_ylabel('phi (rad)')\n", + "ax.set_title('Phase by route bitstring (feasible and infeasible)')\n", + "ax.legend()\n", + "ax.grid(alpha=0.25)\n", + "fig.tight_layout()\n", + "fig.savefig(validity_plot_out, dpi=200, bbox_inches='tight')\n", + "plt.show()\n", + "plt.close(fig)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "fig = circuit_drawer(compiled, output='mpl', fold=80, idle_wires=False)\n", + "fig.savefig(circuit_plot_out, dpi=200, bbox_inches='tight')\n", + "plt.show()\n", + "plt.close(fig)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "print(f'Saved table CSV: {csv_out}')\n", + "print(f'Saved full table plot: {table_plot_out}')\n", + "print(f'Saved feasible/infeasible phase plot: {validity_plot_out}')\n", + "print(f'Saved circuit plot: {circuit_plot_out}')" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "name": "python", + "version": "3.12" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} \ No newline at end of file diff --git a/qiskit_route_construction_example.py b/qiskit_route_construction_example.py deleted file mode 100644 index ed24e1c..0000000 --- a/qiskit_route_construction_example.py +++ /dev/null @@ -1,80 +0,0 @@ -import argparse - -import matplotlib.pyplot as plt -import numpy as np -from qiskit import transpile -from qiskit_aer import Aer - -import src.classical_funcs as cf -from src.qiskit_route_construction import ( - build_qiskit_tsp_construction_circuit, - build_route_phase_table, -) - - -def build_normalized_cost_matrix(n: int, seed: int) -> np.ndarray: - rng = np.random.default_rng(seed) - raw = rng.uniform(0.0, 1.0, size=(n, n)) - np.fill_diagonal(raw, 0.0) - walks = cf.generate_all_walks(n, start_node=n - 1) - all_costs = cf.find_all_cost(raw, walks) - return raw / float(np.max(all_costs)) - - -def plot_table(df, out_path: str) -> None: - fig_height = max(8, 0.18 * len(df)) - fig, ax = plt.subplots(figsize=(14, fig_height)) - ax.axis("off") - table = ax.table( - cellText=df.values, - colLabels=df.columns, - loc="center", - cellLoc="center", - ) - table.auto_set_font_size(False) - table.set_fontsize(7) - table.scale(1.0, 1.1) - fig.tight_layout() - fig.savefig(out_path, dpi=200, bbox_inches="tight") - plt.close(fig) - - -def main() -> None: - parser = argparse.ArgumentParser( - description="Qiskit Aer statevector example for route/validity/cost-phase construction." - ) - parser.add_argument("--n", type=int, default=5, help="Number of cities.") - parser.add_argument("--seed", type=int, default=42, help="Random seed.") - parser.add_argument("--csv", default="qiskit_tsp_table.csv", help="Output CSV path.") - parser.add_argument("--plot", default="qiskit_tsp_table.png", help="Output plot path.") - args = parser.parse_args() - - cost_matrix = build_normalized_cost_matrix(args.n, args.seed) - circuit, layout = build_qiskit_tsp_construction_circuit( - cost_matrix=cost_matrix, - start_node=args.n - 1, - ) - - backend = Aer.get_backend("statevector_simulator") - compiled = transpile(circuit, backend=backend) - result = backend.run(compiled).result() - statevector = np.asarray(result.get_statevector(compiled)) - - df = build_route_phase_table( - statevector=statevector, - layout=layout, - cost_matrix=cost_matrix, - ) - df["tour_key"] = df["tour"].apply(tuple) - df = df.sort_values(by=["validity", "expected_phi", "tour_key"], ascending=[False, True, True]).reset_index(drop=True) - df = df.drop(columns=["tour_key"]) - df.to_csv(args.csv, index=False) - plot_table(df, args.plot) - - print(f"Saved table CSV: {args.csv}") - print(f"Saved table plot: {args.plot}") - print(df.head(20).to_string(index=False)) - - -if __name__ == "__main__": - main() From da289c1975c16ba09782215417661cd0364b2f3d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 2 Sep 2026 09:11:19 +0000 Subject: [PATCH 4/5] Fix notebook launch path in README Co-authored-by: fgfuchs <2428162+fgfuchs@users.noreply.github.com> --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index a77aa3e..2ed7e0c 100644 --- a/README.md +++ b/README.md @@ -130,7 +130,7 @@ Output: Open: ```bash -jupyter notebook /home/runner/work/TSP/TSP/qiskit_route_construction_example.ipynb +jupyter notebook qiskit_route_construction_example.ipynb ``` Output: From d2b18ed9a874776548ed9184eb120c9e408276e8 Mon Sep 17 00:00:00 2001 From: Franz Fuchs Date: Wed, 2 Sep 2026 17:00:29 +0200 Subject: [PATCH 5/5] small fixes --- qiskit_route_construction_example.ipynb | 688 ++++++++++++++++++------ src/classical_funcs.py | 2 +- 2 files changed, 522 insertions(+), 168 deletions(-) diff --git a/qiskit_route_construction_example.ipynb b/qiskit_route_construction_example.ipynb index ea5d434..5229e13 100644 --- a/qiskit_route_construction_example.ipynb +++ b/qiskit_route_construction_example.ipynb @@ -1,176 +1,530 @@ { - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Qiskit route construction example\n", - "\n", - "This notebook builds the route register, reversible validity oracle, validity-controlled cost-phase oracle, and uncomputation, then reproduces the tours/\\(\\phi\\)/validity table with Aer statevector simulation." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "import matplotlib.pyplot as plt\n", - "import numpy as np\n", - "from qiskit import transpile\n", - "from qiskit.visualization import circuit_drawer\n", - "from qiskit_aer import Aer\n", - "\n", - "import src.classical_funcs as cf\n", - "from src.qiskit_route_construction import (\n", - " build_qiskit_tsp_construction_circuit,\n", - " build_route_phase_table,\n", - ")" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "n = 5\n", - "seed = 42\n", - "csv_out = 'qiskit_tsp_table.csv'\n", - "table_plot_out = 'qiskit_tsp_table.png'\n", - "validity_plot_out = 'qiskit_tsp_validity_phi.png'\n", - "circuit_plot_out = 'qiskit_tsp_circuit.png'" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "def build_normalized_cost_matrix(n: int, seed: int) -> np.ndarray:\n", - " rng = np.random.default_rng(seed)\n", - " raw = rng.uniform(0.0, 1.0, size=(n, n))\n", - " np.fill_diagonal(raw, 0.0)\n", - " walks = cf.generate_all_walks(n, start_node=n - 1)\n", - " all_costs = cf.find_all_cost(raw, walks)\n", - " return raw / float(np.max(all_costs))\n", - "\n", - "def route_to_bitstring(route, bits_per_step: int) -> str:\n", - " return ''.join(format(int(city), f'0{bits_per_step}b') for city in route)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "cost_matrix = build_normalized_cost_matrix(n=n, seed=seed)\n", - "circuit, layout = build_qiskit_tsp_construction_circuit(cost_matrix=cost_matrix, start_node=n - 1)\n", - "\n", - "backend = Aer.get_backend('statevector_simulator')\n", - "compiled = transpile(circuit, backend=backend)\n", - "result = backend.run(compiled).result()\n", - "statevector = np.asarray(result.get_statevector(compiled))" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "df = build_route_phase_table(statevector=statevector, layout=layout, cost_matrix=cost_matrix)\n", - "df['bitstring'] = df['tour'].apply(lambda tour: route_to_bitstring(tour, layout.bits_per_step))\n", - "df['tour_key'] = df['tour'].apply(tuple)\n", - "df = df.sort_values(by=['validity', 'expected_phi', 'tour_key'], ascending=[False, True, True]).reset_index(drop=True)\n", - "df = df.drop(columns=['tour_key'])\n", - "df.to_csv(csv_out, index=False)\n", - "df" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "fig_height = max(8, 0.18 * len(df))\n", - "fig, ax = plt.subplots(figsize=(16, fig_height))\n", - "ax.axis('off')\n", - "table = ax.table(cellText=df.values, colLabels=df.columns, loc='center', cellLoc='center')\n", - "table.auto_set_font_size(False)\n", - "table.set_fontsize(6)\n", - "table.scale(1.0, 1.1)\n", - "fig.tight_layout()\n", - "fig.savefig(table_plot_out, dpi=200, bbox_inches='tight')\n", - "plt.show()\n", - "plt.close(fig)" - ] - }, + "cells": [ + { + "cell_type": "markdown", + "id": "6213065b", + "metadata": {}, + "source": [ + "# Qiskit route construction example\n", + "\n", + "This notebook builds the route register, reversible validity oracle, validity-controlled cost-phase oracle, and uncomputation, then reproduces the tours/\\(\\phi\\)/validity table with Aer statevector simulation." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "a4e886b5", + "metadata": {}, + "outputs": [], + "source": [ + "import matplotlib.pyplot as plt\n", + "import numpy as np\n", + "from qiskit import transpile\n", + "from qiskit.visualization import circuit_drawer\n", + "from qiskit_aer import Aer\n", + "\n", + "import src.classical_funcs as cf\n", + "from src.qiskit_route_construction import (\n", + " build_qiskit_tsp_construction_circuit,\n", + " build_route_phase_table,\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "a59c8f72", + "metadata": {}, + "outputs": [], + "source": [ + "n = 5\n", + "seed = 42\n", + "csv_out = 'qiskit_tsp_table.csv'\n", + "table_plot_out = 'qiskit_tsp_table.png'\n", + "validity_plot_out = 'qiskit_tsp_validity_phi.png'\n", + "circuit_plot_out = 'qiskit_tsp_circuit.png'" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "a555392a", + "metadata": {}, + "outputs": [], + "source": [ + "def build_normalized_cost_matrix(n: int, seed: int) -> np.ndarray:\n", + " np.random.seed(seed)\n", + " raw = cf.generate_cost_matrix(n)\n", + " walks = cf.generate_all_walks(n, start_node=n - 1)\n", + " all_costs = cf.find_all_cost(raw, walks)\n", + " Lmax = n*float(np.max(raw))\n", + " mask = ~np.eye(raw.shape[0], dtype=bool)\n", + " Lmin = n*float(np.min(raw[mask]))\n", + " print(Lmin,Lmax)\n", + " return (raw - Lmin)/ (Lmax-Lmin)\n", + "\n", + "def route_to_bitstring(route, bits_per_step: int) -> str:\n", + " return ''.join(format(int(city), f'0{bits_per_step}b') for city in route)" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "97753a40", + "metadata": {}, + "outputs": [ { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "df_plot = df.copy()\n", - "df_plot['row'] = np.arange(len(df_plot))\n", - "\n", - "valid = df_plot[df_plot['validity'] == 1]\n", - "invalid = df_plot[df_plot['validity'] == 0]\n", - "\n", - "fig, ax = plt.subplots(figsize=(16, 6))\n", - "ax.scatter(valid['row'], valid['phi'], s=30, label='feasible', color='tab:green')\n", - "ax.scatter(invalid['row'], invalid['phi'], s=16, label='infeasible', color='tab:red', alpha=0.6)\n", - "ax.set_xlabel('bitstring row index')\n", - "ax.set_ylabel('phi (rad)')\n", - "ax.set_title('Phase by route bitstring (feasible and infeasible)')\n", - "ax.legend()\n", - "ax.grid(alpha=0.25)\n", - "fig.tight_layout()\n", - "fig.savefig(validity_plot_out, dpi=200, bbox_inches='tight')\n", - "plt.show()\n", - "plt.close(fig)" - ] - }, + "name": "stdout", + "output_type": "stream", + "text": [ + "0.10292247147901223 4.8495492608099715\n" + ] + } + ], + "source": [ + "cost_matrix = build_normalized_cost_matrix(n=n, seed=seed)\n", + "circuit, layout = build_qiskit_tsp_construction_circuit(cost_matrix=cost_matrix, start_node=n - 1)\n", + "\n", + "backend = Aer.get_backend('statevector_simulator')\n", + "compiled = transpile(circuit, backend=backend)\n", + "result = backend.run(compiled).result()\n", + "statevector = np.asarray(result.get_statevector(compiled))" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "d642d245", + "metadata": {}, + "outputs": [ { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "fig = circuit_drawer(compiled, output='mpl', fold=80, idle_wires=False)\n", - "fig.savefig(circuit_plot_out, dpi=200, bbox_inches='tight')\n", - "plt.show()\n", - "plt.close(fig)" + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
tourvalidityphiexpected_phiprobbitstring
0[2, 3, 1, 0]10.1276960.1276960.00390610110100
1[1, 3, 2, 0]10.1953720.1953720.00390601111000
2[1, 0, 2, 3]10.2141390.2141390.00390601001011
3[1, 2, 3, 0]10.2196970.2196970.00390601101100
4[1, 0, 3, 2]10.2288180.2288180.00390601001110
5[3, 1, 2, 0]10.2525520.2525520.00390611011000
6[3, 1, 0, 2]10.2582480.2582480.00390611010010
7[1, 3, 0, 2]10.2787710.2787710.00390601110010
8[1, 2, 0, 3]10.2952690.2952690.00390601100011
9[2, 0, 3, 1]10.2968610.2968610.00390610001101
10[2, 0, 1, 3]10.3457560.3457560.00390610000111
11[3, 2, 1, 0]10.3493910.3493910.00390611100100
12[2, 1, 3, 0]10.3556170.3556170.00390610011100
13[2, 1, 0, 3]10.3778100.3778100.00390610010011
14[2, 3, 0, 1]10.3859720.3859720.00390610110001
15[3, 0, 1, 2]10.4284870.4284870.00390611000110
16[0, 3, 1, 2]10.4314940.4314940.00390600110110
17[0, 2, 3, 1]10.4327050.4327050.00390600101101
18[3, 2, 0, 1]10.4331240.4331240.00390611100001
19[0, 1, 3, 2]10.4962790.4962790.00390600011110
20[0, 1, 2, 3]10.5093510.5093510.00390600011011
21[3, 0, 2, 1]10.5151300.5151300.00390611001001
22[0, 2, 1, 3]10.5670320.5670320.00390600100111
23[0, 3, 2, 1]10.6106730.6106730.00390600111001
24[0, 0, 0, 0]00.000000NaN0.00390600000000
25[0, 0, 0, 1]00.000000NaN0.00390600000001
\n", + "
" + ], + "text/plain": [ + " tour validity phi expected_phi prob bitstring\n", + "0 [2, 3, 1, 0] 1 0.127696 0.127696 0.003906 10110100\n", + "1 [1, 3, 2, 0] 1 0.195372 0.195372 0.003906 01111000\n", + "2 [1, 0, 2, 3] 1 0.214139 0.214139 0.003906 01001011\n", + "3 [1, 2, 3, 0] 1 0.219697 0.219697 0.003906 01101100\n", + "4 [1, 0, 3, 2] 1 0.228818 0.228818 0.003906 01001110\n", + "5 [3, 1, 2, 0] 1 0.252552 0.252552 0.003906 11011000\n", + "6 [3, 1, 0, 2] 1 0.258248 0.258248 0.003906 11010010\n", + "7 [1, 3, 0, 2] 1 0.278771 0.278771 0.003906 01110010\n", + "8 [1, 2, 0, 3] 1 0.295269 0.295269 0.003906 01100011\n", + "9 [2, 0, 3, 1] 1 0.296861 0.296861 0.003906 10001101\n", + "10 [2, 0, 1, 3] 1 0.345756 0.345756 0.003906 10000111\n", + "11 [3, 2, 1, 0] 1 0.349391 0.349391 0.003906 11100100\n", + "12 [2, 1, 3, 0] 1 0.355617 0.355617 0.003906 10011100\n", + "13 [2, 1, 0, 3] 1 0.377810 0.377810 0.003906 10010011\n", + "14 [2, 3, 0, 1] 1 0.385972 0.385972 0.003906 10110001\n", + "15 [3, 0, 1, 2] 1 0.428487 0.428487 0.003906 11000110\n", + "16 [0, 3, 1, 2] 1 0.431494 0.431494 0.003906 00110110\n", + "17 [0, 2, 3, 1] 1 0.432705 0.432705 0.003906 00101101\n", + "18 [3, 2, 0, 1] 1 0.433124 0.433124 0.003906 11100001\n", + "19 [0, 1, 3, 2] 1 0.496279 0.496279 0.003906 00011110\n", + "20 [0, 1, 2, 3] 1 0.509351 0.509351 0.003906 00011011\n", + "21 [3, 0, 2, 1] 1 0.515130 0.515130 0.003906 11001001\n", + "22 [0, 2, 1, 3] 1 0.567032 0.567032 0.003906 00100111\n", + "23 [0, 3, 2, 1] 1 0.610673 0.610673 0.003906 00111001\n", + "24 [0, 0, 0, 0] 0 0.000000 NaN 0.003906 00000000\n", + "25 [0, 0, 0, 1] 0 0.000000 NaN 0.003906 00000001" ] - }, + }, + "execution_count": 5, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "df = build_route_phase_table(statevector=statevector, layout=layout, cost_matrix=cost_matrix)\n", + "df['bitstring'] = df['tour'].apply(lambda tour: route_to_bitstring(tour, layout.bits_per_step))\n", + "df['tour_key'] = df['tour'].apply(tuple)\n", + "df = df.sort_values(by=['validity', 'expected_phi', 'tour_key'], ascending=[False, True, True]).reset_index(drop=True)\n", + "df = df.drop(columns=['tour_key'])\n", + "df.to_csv(csv_out, index=False)\n", + "df.head(26)" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "abc860ce", + "metadata": {}, + "outputs": [], + "source": [ + "# fig_height = max(8, 0.18 * len(df))\n", + "# fig, ax = plt.subplots(figsize=(16, fig_height))\n", + "# ax.axis('off')\n", + "# table = ax.table(cellText=df.values, colLabels=df.columns, loc='center', cellLoc='center')\n", + "# table.auto_set_font_size(False)\n", + "# table.set_fontsize(6)\n", + "# table.scale(1.0, 1.1)\n", + "# fig.tight_layout()\n", + "# fig.savefig(table_plot_out, dpi=200, bbox_inches='tight')\n", + "# plt.show()\n", + "# plt.close(fig)" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "8bacfb9a", + "metadata": {}, + "outputs": [ { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "print(f'Saved table CSV: {csv_out}')\n", - "print(f'Saved full table plot: {table_plot_out}')\n", - "print(f'Saved feasible/infeasible phase plot: {validity_plot_out}')\n", - "print(f'Saved circuit plot: {circuit_plot_out}')" + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAABjUAAAJOCAYAAAD/KYUYAAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjgsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvwVt1zgAAAAlwSFlzAAAPYQAAD2EBqD+naQAAfkhJREFUeJzs3XuYVXW9P/D3bBhgRu4yDF5QvIGRCop5yUxPkahlx3Miy6wBKhW8ZXS1U6JdJLXMMsOsFNFOmXQ7vzLLa6WSmqZlKKipmMpFQUCH+96/PzrMceTizACzZ8Pr9Tw8stf6rr0+a+29Pw77PWt9q0qlUikAAAAAAAAdXKHcBQAAAAAAALSEUAMAAAAAAKgIQg0AAAAAAKAiCDUAAAAAAICKINQAAAAAAAAqglADAAAAAACoCEINAAAAAACgIgg1AAAAAACAiiDUAAAAAAAAKoJQAwBgKzF16tRUVVXlz3/+c7lLabHzzjsvVVVVeeGFF8pdSod35JFHZp999nndcU899VSqqqoyderULV/URgwaNChjx44t2/6feeaZdOvWLXfddVez5ddee2323nvvVFdXp3fv3uUpLsnYsWMzaNCgpsdrX7evfe1rr7vt2s9NJWrp+6KqqirnnXdem/axevXqfPrTn87AgQNTKBRy/PHHt+l5NofXHm9r+vSRRx6ZI488stX7XLVqVQYOHJjvfOc7rd4WAKASCDUAADq4tV+Crf3TrVu3DB48OGeccUbmzZtX7vJ4jf/+7//OpZdeWu4ymrnxxhvb/AXxzJkzc9555+Wpp57arDVtaV/84hdz8MEH57DDDmta9uijj2bs2LHZY4898r3vfS9XXnllGStkS7nqqqty8cUXZ/To0bnmmmvy8Y9/vNwltavq6upMnDgxX/nKV7J8+fJylwMAsNl1LncBAAC0zBe/+MXstttuWb58ee68885MmTIlN954Yx5++OHU1taWuzz+13//93/n4Ycfztlnn12W/e+6665ZtmxZqqurm5bdeOONufzyy9sUbMycOTPnn39+jjzyyGZXFryeWbNmpVAoz+9QLViwINdcc02uueaaZsvvuOOOFIvFfPOb38yee+5ZltrW+t73vpdisVjWGjqyZcuWpXPntv1z9bbbbstOO+2Ub3zjG5u5qtYr1+dg3Lhx+exnP5v//u//zoc//OF23z8AwJbkSg0AgApxzDHH5IMf/GA++tGPZurUqTn77LPz5JNP5pe//GW5S6s4pVIpy5YtK3cZW8Taq3k6derU7vt+9Xnt2rVrs2ClPV133XXp3LlzjjvuuGbL58+fnyRlve3UWtXV1enatWu5y+iwunXr1uZQY/78+R3iNU7K9zno3bt3jjrqqLLfhg4AYEsQagAAVKi3ve1tSZInn3yy2fIVK1Zk4sSJqaury3bbbZf/+I//yIIFC5qN+eUvf5l3vvOd2XHHHdO1a9fsscce+dKXvpQ1a9Y0G/fYY4/lPe95TwYMGJBu3bpl5513zvvf//4sXry42bjrrrsuI0aMSE1NTfr27Zv3v//9eeaZZ1p8LC+88EJOOOGE9OzZM9tvv30+9rGPNbttyhFHHJFhw4atd9shQ4Zk1KhRG33+QYMG5V3veld++9vf5sADD0xNTU2++93vJkn+8Y9/5L3vfW/69u2b2traHHLIIfn1r3/dbPu1twB77S2Y7rjjjlRVVeWOO+5I8q974P/617/O008/3XS7sFdf3bBixYpMmjQpe+65Z7p27ZqBAwfm05/+dFasWNHCM5Xcf//9efOb35yamprstttuueKKK5qtf+2cGmPHjs3ll1+eJM1uY7bWj3/844wYMSI9evRIz549s+++++ab3/xm03G/973vTZL827/9W9O2a493Y+d1Q3MJ3HXXXa/7/iwWiznvvPOy4447pra2Nv/2b/+WmTNntng+hl/84hc5+OCD071796ZlgwYNyqRJk5IkdXV168zZ8Jvf/CaHH354tttuu/To0SPvfOc78/e//73Z8/71r3/N2LFjs/vuu6dbt24ZMGBAPvzhD+fFF19sNm7p0qU5++yzM2jQoHTt2jX9+/fPO97xjjzwwANNY147p8arfeMb38iuu+6ampqaHHHEEXn44Ydf95iTtn8On3766Zx22mkZMmRIampqsv322+e9733vOu/31ryGpVIpX/7yl7Pzzjs3vYavPZ8b89rXZ+08Io8//njGjh2b3r17p1evXhk3blwaGxuT/N97//bbb8/f//73dd6vxWIxl156ad74xjemW7duqa+vz6mnnppFixY12/fm7I8bes82Njbm1FNPzfbbb5+ePXumoaFhnTrWpzU95B3veEfuvPPOLFy48HWfFwCgkrj9FABAhXriiSeSJNtvv32z5WeeeWb69OmTSZMm5amnnsqll16aM844I9dff33TmKlTp6Z79+6ZOHFiunfvnttuuy3nnntulixZkosvvjhJsnLlyowaNSorVqzImWeemQEDBuTZZ5/Nr371q7z00kvp1atXkuQrX/lKvvCFL+SEE07IRz/60SxYsCCXXXZZ3vrWt+Yvf/lLi35j+oQTTsigQYMyefLk/OlPf8q3vvWtLFq0KNOmTUuSfOhDH8rJJ5+chx9+uNlk2ffdd19mz56dz3/+86+7j1mzZuXEE0/MqaeempNPPjlDhgzJvHnz8uY3vzmNjY0566yzsv322+eaa67Ju9/97kyfPj3/8R//8brP+2r/9V//lcWLF+ef//xn061v1n6xXiwW8+53vzt33nlnTjnllLzhDW/I3/72t3zjG9/I7Nmz84tf/OJ1n3/RokU59thjc8IJJ+TEE0/MT37yk0yYMCFdunTZ4C1mTj311Dz33HO5+eabc+211zZbd/PNN+fEE0/M29/+9lx44YVJkkceeSR33XVXPvaxj+Wtb31rzjrrrHzrW9/K5z73ubzhDW9Ikqb/bui8bkxL3p/nnHNOLrroohx33HEZNWpUHnrooYwaNapF8wOsWrUq9913XyZMmNBs+aWXXppp06bl5z//eaZMmZLu3btnv/32S/KvycPHjBmTUaNG5cILL0xjY2OmTJmSt7zlLfnLX/7SFD7cfPPN+cc//pFx48ZlwIAB+fvf/54rr7wyf//73/OnP/2pKSwaP358pk+fnjPOOCNDhw7Niy++mDvvvDOPPPJIDjjggI3WP23atCxdujSnn356li9fnm9+85t529velr/97W+pr6/f4Hab8jm87777cvfdd+f9739/dt555zz11FOZMmVKjjzyyMycOXOd29u15DU899xz8+UvfznHHntsjj322DzwwAM56qijsnLlyo0e/+s54YQTsttuu2Xy5Ml54IEH8v3vfz/9+/fPhRdemLq6ulx77bX5yle+kpdffjmTJ09O8n/v11NPPTVTp07NuHHjctZZZ+XJJ5/Mt7/97fzlL3/JXXfd1XRFxebsjxtyxhlnpHfv3jnvvPMya9asTJkyJU8//XRTULo+re0hI0aMSKlUyt133513vetdm3LaAQA6lhIAAB3a1VdfXUpSuuWWW0oLFiwoPfPMM6Uf//jHpe23375UU1NT+uc//9ls3MiRI0vFYrFp+49//OOlTp06lV566aWmZY2Njevs59RTTy3V1taWli9fXiqVSqW//OUvpSSlG264YYO1PfXUU6VOnTqVvvKVrzRb/re//a3UuXPndZa/1qRJk0pJSu9+97ubLT/ttNNKSUoPPfRQqVQqlV566aVSt27dSp/5zGeajTvrrLNK2223Xenll1/e6H523XXXUpLSTTfd1Gz52WefXUpS+uMf/9i0bOnSpaXddtutNGjQoNKaNWtKpdL/ndsnn3yy2fa33357KUnp9ttvb1r2zne+s7TrrruuU8O1115bKhQKzfZVKpVKV1xxRSlJ6a677troMRxxxBGlJKWvf/3rTctWrFhRGj58eKl///6llStXlkqlUunJJ58sJSldffXVTeNOP/300vp+9P/Yxz5W6tmzZ2n16tUb3O8NN9ywzjGutaHzunbdmDFjmh639P05d+7cUufOnUvHH398s+c777zzSkmaPef6PP7446Ukpcsuu2yddWvfbwsWLGhatnTp0lLv3r1LJ598crOxc+fOLfXq1avZ8vV9bn70ox+VkpT+8Ic/NC3r1atX6fTTT99onWPGjGn2Pln7ur36M10qlUr33HNPKUnp4x//+DrHsdamfg7Xd1wzZswoJSlNmzataVlLX8P58+eXunTpUnrnO9/ZbNznPve5Fr2GpVKplKQ0adKkdY75wx/+cLNx//Ef/1Hafvvtmy074ogjSm984xubLfvjH/9YSlL64Q9/2Gz5TTfdtM7yzdUfS6UNfw5GjBjR9JktlUqliy66qJSk9Mtf/rLZcRxxxBFNj1vbQ5577rlSktKFF1640RoBACqN208BAFSIkSNHpq6uLgMHDsz73//+dO/ePT//+c+z0047NRt3yimnNPtN38MPPzxr1qzJ008/3bSspqam6e9Lly7NCy+8kMMPPzyNjY159NFHk6TpN41/+9vfNt3e5bV+9rOfpVgs5oQTTsgLL7zQ9GfAgAHZa6+9cvvtt7fo2E4//fRmj88888wk/5rgem0t//7v/54f/ehHKZVKSZI1a9bk+uuvz/HHH5/tttvudfex2267rXObqhtvvDEHHXRQ3vKWtzQt6969e0455ZQ89dRTmTlzZovqb4kbbrghb3jDG7L33ns3O1drbyPWknPVuXPnnHrqqU2Pu3TpklNPPTXz58/P/fff3+qaevfunVdeeSU333xzq7dda33ndWNe7/156623ZvXq1TnttNOabbf2PfF61t4Kqk+fPi0af/PNN+ell17KiSee2Ox16dSpUw4++OBmr8urPzfLly/PCy+8kEMOOSRJmt1aqnfv3rnnnnvy3HPPtaiGVzv++OObfaYPOuigHHzwwU2fhfXZ1M/hq49r1apVefHFF7Pnnnumd+/ezY5rrdd7DW+55ZasXLkyZ555ZrNxZ5999use/+sZP358s8eHH354XnzxxSxZsmSj291www3p1atX3vGOdzQ7RyNGjEj37t03+DpvSn/cmFNOOaXZXBsTJkxI586dN/o6t7aHrP0MvPDCC62uDwCgI3P7KQCACnH55Zdn8ODB6dy5c+rr6zNkyJAUCuv+jsouu+zS7PHaL7Zefb/2v//97/n85z+f2267bZ0vA9feD3633XbLxIkTc8kll+SHP/xhDj/88Lz73e/OBz/4waYv9B577LGUSqXstdde6625pRPkvnb7PfbYI4VCodk9/RsaGnL99dfnj3/8Y9761rfmlltuybx58/KhD32oRfvYbbfd1ln29NNP5+CDD15n+drb1Tz99NPNbne1KR577LE88sgjqaurW+/6tZNYb8yOO+64ToAzePDgJP+aT2DtF+wtddppp+UnP/lJjjnmmOy000456qijcsIJJ+Too49u8XOs77xuzOu9P9d+Mb7nnns2G9e3b98WBxVJmsKv1/PYY48l+b85al6rZ8+eTX9fuHBhzj///Pz4xz9e5/V69TwKF110UcaMGZOBAwdmxIgROfbYY9PQ0JDdd9/9detZ32dp8ODB+clPfrLRY9iUz+GyZcsyefLkXH311Xn22WebnbvXzp+TtPw1fG09dXV1rXoN12dj+371a/Vajz32WBYvXpz+/fuvd/2rX8/N1R835rXnpnv37tlhhx3WmcfktcfQmh6y9nXc0O2sAAAqlVADAKBCHHTQQTnwwANfd1ynTp3Wu3ztF1wvvfRSjjjiiPTs2TNf/OIXs8cee6Rbt2554IEH8pnPfCbFYrFpm69//esZO3ZsfvnLX+Z3v/tdzjrrrKZ5L3beeecUi8VUVVXlN7/5zXr3++qJmltjfV/CjRo1KvX19bnuuuvy1re+Ndddd10GDBiQkSNHtug5X/3b15ujniTrTBy8McViMfvuu28uueSS9a4fOHBgm2rbFP3798+DDz6Y3/72t/nNb36T3/zmN7n66qvT0NCQa665pkXP0drz+nrvz021do6Zlky6nKTp/X7ttddmwIAB66zv3Pn//sl0wgkn5O67786nPvWpDB8+PN27d0+xWMzRRx/d7HNzwgkn5PDDD8/Pf/7z/O53v8vFF1+cCy+8MD/72c9yzDHHbMrhbfAYNuVzeOaZZ+bqq6/O2WefnUMPPTS9evVKVVVV3v/+9zc7rrW29Gu4MW3dd7FYTP/+/fPDH/5wvevXBgWbsz9ubq3tIWs/A/369dvstQAAlJNQAwBgG3PHHXfkxRdfzM9+9rO89a1vbVr+5JNPrnf8vvvum3333Tef//znc/fdd+ewww7LFVdckS9/+cvZY489UiqVsttuuzVdMdAWjz32WLPf+H/88cdTLBabJmhO/vVl5gc+8IFMnTo1F154YX7xi1/k5JNP3uCXnC2x6667ZtasWessX3uLmV133TXJ//02+EsvvdRs3Ktv6bXWhgKQPfbYIw899FDe/va3t/k3p5977rm88sorza7WmD17dpI0O1ctrSn51y2sjjvuuBx33HEpFos57bTT8t3vfjdf+MIXsueee7b7b3mvPeePP/54s/fEiy++2KKgYpdddklNTc0G38+vtcceeyT5V8CzsYBs0aJFufXWW3P++efn3HPPbVq+9kqP19phhx1y2mmn5bTTTsv8+fNzwAEH5Ctf+crrhhrre77Zs2dv9PXd1M/h9OnTM2bMmHz9619vWrZ8+fJ13u8ttfY1fOyxx5pdnbJgwYIWh02b2x577JFbbrklhx122EaDuM3ZHzfmsccey7/92781PX755Zfz/PPP59hjj93oMbSmh6ytee2VZwAAWwtzagAAbGPWhgCv/s3mlStX5jvf+U6zcUuWLMnq1aubLdt3331TKBSyYsWKJMl//ud/plOnTjn//PPX+U3pUqnUNL/B67n88subPb7sssuSZJ0vgD/0oQ9l0aJFOfXUU/Pyyy/ngx/8YIuef0OOPfbY3HvvvZkxY0bTsldeeSVXXnllBg0alKFDhyb5vy++//CHPzSNW7NmTa688sp1nnO77bZb7y17TjjhhDz77LP53ve+t866ZcuW5ZVXXnndelevXp3vfve7TY9XrlyZ7373u6mrq8uIESM2uN3aEOS1X1K/9vUpFArZb7/9kqTpNd7QtlvK29/+9nTu3DlTpkxptvzb3/52i7avrq7OgQcemD//+c8tGj9q1Kj07NkzF1xwQVatWrXO+gULFiRZ/+cmSS699NJmj9esWbPO69+/f//suOOOTed0Y37xi1/k2WefbXp877335p577tloGLKpn8NOnTqts91ll13WqiuRXm3kyJGprq7OZZdd1ux5X3uu2tMJJ5yQNWvW5Etf+tI661avXt30/t6c/XFjrrzyymbvtylTpmT16tUbfZ1b20Puv//+VFVV5dBDD33degAAKokrNQAAtjFvfvOb06dPn4wZMyZnnXVWqqqqcu21167zpeZtt92WM844I+9973szePDgrF69Otdee206deqU97znPUn+9WX/l7/85Zxzzjl56qmncvzxx6dHjx558skn8/Of/zynnHJKPvnJT75uTU8++WTe/e535+ijj86MGTNy3XXX5QMf+ECGDRvWbNz++++fffbZp2nC3AMOOGCTzsVnP/vZ/OhHP8oxxxyTs846K3379s0111yTJ598Mj/96U+b5ix54xvfmEMOOSTnnHNOFi5cmL59++bHP/7xOl9qJsmIESNy/fXXZ+LEiXnTm96U7t2757jjjsuHPvSh/OQnP8n48eNz++2357DDDsuaNWvy6KOP5ic/+Ul++9vfvu7txXbcccdceOGFeeqppzJ48OBcf/31efDBB3PllVdudN6EtYHHWWedlVGjRqVTp055//vfn49+9KNZuHBh3va2t2XnnXfO008/ncsuuyzDhw9v+u3u4cOHp1OnTrnwwguzePHidO3aNW9729s2ODfBpqqvr8/HPvaxfP3rX296Tzz00EP5zW9+k379+rXoN9T//d//Pf/1X/+VJUuWbHSeheRfc2ZMmTIlH/rQh3LAAQfk/e9/f+rq6jJnzpz8+te/zmGHHZZvf/vb6dmzZ9761rfmoosuyqpVq7LTTjvld7/73Tq/wb906dLsvPPOGT16dIYNG5bu3bvnlltuyX333dfsSogN2XPPPfOWt7wlEyZMyIoVK3LppZdm++23z6c//ekNbrOpn8N3vetdufbaa9OrV68MHTo0M2bMyC233NJ0K6/Wqquryyc/+clMnjw573rXu3LsscfmL3/5S9NrWA5HHHFETj311EyePDkPPvhgjjrqqFRXV+exxx7LDTfckG9+85sZPXr0Zu2PG7Ny5cq8/e1vzwknnJBZs2blO9/5Tt7ylrfk3e9+9wa3aW0Pufnmm3PYYYe1+XUEAOiwSgAAdGhXX311KUnpvvvua9O422+/vZSkdPvttzctu+uuu0qHHHJIqaamprTjjjuWPv3pT5d++9vfNhv3j3/8o/ThD3+4tMcee5S6detW6tu3b+nf/u3fSrfccss6+/7pT39aestb3lLabrvtStttt11p7733Lp1++umlWbNmbbTmSZMmlZKUZs6cWRo9enSpR48epT59+pTOOOOM0rJly9a7zUUXXVRKUrrgggs2+tyvtuuuu5be+c53rnfdE088URo9enSpd+/epW7dupUOOuig0q9+9av1jhs5cmSpa9eupfr6+tLnPve50s0337zOuX355ZdLH/jAB0q9e/cuJSntuuuuTetWrlxZuvDCC0tvfOMbS127di316dOnNGLEiNL5559fWrx48UaP4Ygjjii98Y1vLP35z38uHXrooaVu3bqVdt1119K3v/3tZuOefPLJUpLS1Vdf3bRs9erVpTPPPLNUV1dXqqqqKq39Z8D06dNLRx11VKl///6lLl26lHbZZZfSqaeeWnr++eebPef3vve90u67717q1KlTs+Pd2HndddddS2PGjGl63Jr35+rVq0tf+MIXSgMGDCjV1NSU3va2t5UeeeSR0vbbb18aP378Rs9TqVQqzZs3r9S5c+fStdde22z52vfbggUL1tnm9ttvL40aNarUq1evUrdu3Up77LFHaezYsaU///nPTWP++c9/lv7jP/6j1Lt371KvXr1K733ve0vPPfdcKUlp0qRJpVKpVFqxYkXpU5/6VGnYsGGlHj16lLbbbrvSsGHDSt/5znea7W/MmDHN3htrX7eLL7649PWvf700cODAUteuXUuHH3546aGHHlrvcbxWWz+HixYtKo0bN67Ur1+/Uvfu3UujRo0qPfroo5v0Gq5Zs6Z0/vnnl3bYYYdSTU1N6cgjjyw9/PDD6zznhrz6nL76mF/72q2t6cknn2xatvazsj5XXnllacSIEaWamppSjx49Svvuu2/p05/+dOm5555rGrM5++OGzuHvf//70imnnFLq06dPqXv37qWTTjqp9OKLLzbb9ogjjigdccQRzZa1tIe89NJLpS5dupS+//3vb+gUAwBUrKpSqR1mcwMAgM3km9/8Zj7+8Y/nqaeeyi677FLucmgnL730Uvr06ZMvf/nL+a//+q/XHf+Rj3wks2fPzh//+Md2qA46lksvvTQXXXRRnnjiiY3OIQIAUInMqQEAQMUolUr5wQ9+kCOOOEKgsRVbtmzZOsvWzsdw5JFHtug5Jk2alPvuuy933XXXZqwMOr5Vq1blkksuyec//3mBBgCwVTKnBgAAHd4rr7yS//mf/8ntt9+ev/3tb/nlL39Z7pLYgq6//vpMnTo1xx57bLp3754777wzP/rRj3LUUUflsMMOa9Fz7LLLLlm+fPkWrhQ6nurq6syZM6fcZQAAbDFCDQAAOrwFCxbkAx/4QHr37p3Pfe5zG51Ml8q33377pXPnzrnooouyZMmSpsnDv/zlL5e7NAAAoMzMqQEAAAAAAFQEc2oAAAAAAAAVQagBAAAAAABUhG1uTo1isZjnnnsuPXr0SFVVVbnLAQAAAACAbV6pVMrSpUuz4447plDY8PUY21yo8dxzz2XgwIHlLgMAAAAAAHiNZ555JjvvvPMG129zoUaPHj2S/OvE9OzZs8zVdAzFYjGLFi1Knz59NpqAAbya3gG0hd4BtJa+AbSF3gG0hd5RXkuWLMnAgQObvsPfkG0u1Fh7y6mePXsKNf5XsVjM6tWr07NnTx9WoMX0DqAt9A6gtfQNoC30DqAt9I6O4fWmjfDKAAAAAAAAFUGoAQAAAAAAVAShBgAAAAAAUBG2uTk1AAAAAACoLGvWrMmqVau26D6KxWJWrVqV5cuXm1NjC6iurk6nTp02+XmEGgAAAAAAdEilUilz587NSy+91C77KhaLWbRo0etOVk3b9O7dOwMGDNik8yvUAAAAAACgQ1obaPTv3z+1tbVbNGwolUpZvXp1OnfuLNTYzEqlUhobGzN//vwkyQ477NDm5xJqAAAAAADQ4axZs6Yp0Nh+++23+P6EGltWTU1NkmT+/Pnp379/m29F5cZgAAAAAAB0OGvn0KitrS1zJWwua1/LTZkfRagBAAAAAECH5aqJrcfmeC2FGgAAAAAAQEUQagAAAAAAwGZUKpVyyimnpG/fvqmqqsqDDz64Rfd33nnnZfjw4U2Px44dm+OPP36j2xx55JE5++yzt2hdW4JQAwAAAAAANqObbropU6dOza9+9as8//zz2Weffbbo/j75yU/m1ltv3aL76Cg6l7sAAAAAAADYmjzxxBPZYYcd8uY3v7ld9te9e/d07969XfZVbq7UAAAAAABgq9W4qjFXPHRFRt4wMsOmDcvIG0bmioeuSOOqxi2yv7Fjx+bMM8/MnDlzUlVVlUGDBqVYLGby5MnZbbfdUlNTk2HDhmX69OlN26xZsyYf+chHmtYPGTIk3/zmN5s97x133JGDDjoo2223XXr37p3DDjssTz/9dJJ1bz+11vnnn5+6urr07Nkz48ePz8qVKzdY94oVK/LJT34yO+20U7bbbrscfPDBueOOOzbLOdmcXKkBAAAAAMBWqXFVY8beNDazFs5KMcUkybzGeZny4JTcNue2TD16amqrazfrPr/5zW9mjz32yJVXXpn77rsvnTp1yuTJk3PdddfliiuuyF577ZU//OEP+eAHP5i6urocccQRKRaL2XnnnXPDDTdk++23z913351TTjklO+ywQ0444YSsXr06xx9/fE4++eT86Ec/ysqVK3Pvvfemqqpqg3Xceuut6datW+6444489dRTGTduXLbffvt85StfWe/4M844IzNnzsyPf/zj7Ljjjvn5z3+eo48+On/729+y1157bdZztCmEGgAAAAAAbJWmzZzWLNBYq5hiZi2clWkzp2X8sPGbdZ+9evVKjx490qlTpwwYMCArVqzIBRdckFtuuSWHHnpokmT33XfPnXfeme9+97s54ogjUl1dnfPPP7/pOXbbbbfMmDEjP/nJT3LCCSdkyZIlWbx4cd71rndljz32SJK84Q1v2GgdXbp0yVVXXZXa2tq88Y1vzBe/+MV86lOfype+9KUUCs1v4jRnzpxcffXVmTNnTnbcccck/5qn46abbsrVV1+dCy64YHOeok0i1IBtSOOqxkybOS3TZ0/PgmULUldTl9GDR6dhaMNmT6QBAAAAoNymz56+TqCxVjHFTJ89fbOHGq/1+OOPp7GxMe94xzuaLV+5cmX233//pseXX355rrrqqsyZMyfLli3LypUrm24p1bdv34wdOzajRo3KO97xjowcOTInnHBCdthhhw3ud9iwYamt/b/v/A499NC8/PLLeeaZZ7Lrrrs2G/u3v/0ta9asyeDBg5stX7FiRbbffvu2HvoWIdSAbUQ5LrUDAAAAgHJasGzBJq3fHF5++eUkya9//evstNNOzdZ17do1SfLjH/84n/zkJ/P1r389hx56aHr06JGLL74499xzT9PYq6++OmeddVZuuummXH/99fn85z+fm2++OYcccshmqbFTp065//7706lTp2brOtoE5EIN2EaU41I7AAAAACinupq6zGuct9H1W9rQoUPTtWvXzJkzJ0ccccR6x9x1111585vfnNNOO61p2RNPPLHOuP333z/7779/zjnnnBx66KH57//+7w2GGg899FCWLVuWmpqaJMmf/vSndO/ePQMHDlzv865Zsybz58/P4Ycf3pbDbDeF1x8CbA1acqkdAAAAAGxNRg8encIGvgYvpJDRg0dv8Rp69OiRT37yk/n4xz+ea665Jk888UQeeOCBXHbZZbnmmmuSJHvttVf+/Oc/57e//W1mz56dL3zhC7nvvvuanuPJJ5/MOeeckxkzZuTpp5/O7373uzz22GMbnVdj5cqV+chHPpKZM2fmxhtvzKRJk3LGGWesM59GkgwePDgnnXRSGhoa8rOf/SxPPvlk7r333kyePDm//vWvN/9J2QSu1IBtREe41A4AAAAA2lPD0IbcNue2de5gUkghQ/oOScPQhnap40tf+lLq6uoyefLk/OMf/0jv3r1zwAEH5HOf+1yS5NRTT81f/vKXvO9970tVVVVOPPHEnHbaafnNb36TJKmtrc2jjz6aa665Ji+++GJ22GGHnH766Tn11FM3uM+3v/3t2WuvvfLWt741K1asyIknnpjzzjtvg+OvvvrqfPnLX84nPvGJPPvss+nXr18OOeSQvOtd79qs52JTVZVKpVK5i2hPS5YsSa9evbJ48eL07Nmz3OV0CMViMQsXLkzfvn3Xm9KxdRh5w8iNXmpXX1ufW957SztWRKXTO4C20DuA1tI3gLbQO2DrsHz58jz55JPZbbfd0q1btzY/T+OqxkybOS3TZ0/PgmULUldTl9GDR6dhaEOzOWZLpVJWr16dzp07p6qqanMcAq+xsde0pd/du1IDthGjB4/OlAenrPcWVO11qR0AAAAAtLfa6tqMHzbefLJbCVE1bCMahjZkSN8h69xDsL0vtQMAAAAAaKuyhxqXX355Bg0alG7duuXggw/Ovffeu9HxL730Uk4//fTssMMO6dq1awYPHpwbb7yxnaqFylVbXZupR0/NhOETUl9bn0JVIfW19ZkwfEKmHj212aV2AAAAAAAdUVlvP3X99ddn4sSJueKKK3LwwQfn0ksvzahRozJr1qz0799/nfErV67MO97xjvTv3z/Tp0/PTjvtlKeffjq9e/du/+KhArnUDgAAAACoZGUNNS655JKcfPLJGTduXJLkiiuuyK9//etcddVV+exnP7vO+KuuuioLFy7M3Xffnerq6iTJoEGD2rNkAAAAAACgTMoWaqxcuTL3339/zjnnnKZlhUIhI0eOzIwZM9a7zf/8z//k0EMPzemnn55f/vKXqaurywc+8IF85jOfSadOnda7zYoVK7JixYqmx0uWLEmSFIvFFIvrTpi8LSoWiymVSs4H0Cp6B9AWegfQWvoG0BZ6B2wd1n6W1/5pT+29v23F2tdyfd/Pt7Rnly3UeOGFF7JmzZrU19c3W15fX59HH310vdv84x//yG233ZaTTjopN954Yx5//PGcdtppWbVqVSZNmrTebSZPnpzzzz9/neWLFi3K6tWrN/1AtgLFYjFLly5NqVRKoVD2aVaACqF3AG2hdwCtpW8AbaF3wNZh1apVKRaLWb16dbt9l7tmzZp22c+2avXq1SkWi1m8eHEaGxubrVu6dGmLnqOst59qrWKxmP79++fKK69Mp06dMmLEiDz77LO5+OKLNxhqnHPOOZk4cWLT4yVLlmTgwIHp06dPevbs2V6ld2jFYjFVVVXp06eP/9EDLaZ3AG2hdwCtpW8AbaF3wNZh+fLlWbRoUTp37pzOndvvq+z23Ne2pnPnzikUCunVq1e6deu2zroWPceWKKwl+vXrl06dOmXevHnNls+bNy8DBgxY7zY77LBDqqurm91q6g1veEPmzp2blStXpkuXLuts07Vr13Tt2nWd5YVCwf/UXqWqqso5AVpN7wDaQu8AWkvfANpC74DKVygUUlVV1fRnS3v1LafaY3/borWv5fr6c0v7ddm6epcuXTJixIjceuutTcuKxWJuvfXWHHrooevd5rDDDsvjjz/e7N5as2fPzg477LDeQAMAAAAAANrbkUcembPPPrvF4x999NEccsgh6datW4YPH77F6nq1QYMG5dJLL216XFVVlV/84hcbHP/UU0+lqqoqDz744BavbWPKeh3NxIkTM2bMmBx44IE56KCDcumll+aVV17JuHHjkiQNDQ3ZaaedMnny5CTJhAkT8u1vfzsf+9jHcuaZZ+axxx7LBRdckLPOOquchwEAAAAAAE1+9rOfpbq6usXjJ02alO222y6zZs1K9+7dt2Bl/+e+++7Ldttt1y772pzKGmq8733vy4IFC3Luuedm7ty5GT58eG666aamycPnzJnT7JKTgQMH5re//W0+/vGPZ7/99stOO+2Uj33sY/nMZz5TrkMAAAAAAIBm+vbt26rxTzzxRN75zndm11133UIVrauurq7d9rU5lf2mgmeccUaefvrprFixIvfcc08OPvjgpnV33HFHpk6d2mz8oYcemj/96U9Zvnx5nnjiiXzuc59rNscGAAAAAACU06tvPzVo0KBccMEF+fCHP5wePXpkl112yZVXXtk0tqqqKvfff3+++MUvpqqqKuedd16S5JlnnskJJ5yQ3r17p2/fvvn3f//3PPXUU03b3XfffXnHO96Rfv36pVevXjniiCPywAMPNK0vlUo577zzsssuu6Rr167Zcccdm9316LW3n0qS559/Psccc0xqamqy++67Z/r06Rs9zocffjjHHHNMunfvnvr6+nzoQx/KCy+80LaT1kJlDzUAAAAAAGBLWj5zZuZ/81t59lOfyvxvfivLZ85s1/1//etfz4EHHpi//OUvOe200zJhwoTMmjUryb+ChDe+8Y35xCc+keeffz6f/OQns2rVqowaNSo9evTIH//4x9x1113p3r17jj766KxcuTJJsnTp0owZMyZ33nln/vSnP2WvvfbKsccem6VLlyZJfvrTn+Yb3/hGvvvd7+axxx7LL37xi+y7774brfMLX/hC3vOe9+Shhx7KSSedlPe///155JFH1jv2pZdeytve9rbsv//++fOf/5ybbrop8+bNywknnLAZz9y6ynr7KQAAAAAA2JKWz5yZBZd9O2sWL06hR4+suueerHj00dSdeUa6DR3aLjUce+yxOe2005Ikn/nMZ/KNb3wjt99+e4YMGZIBAwakc+fO6d69ewYMGJAkue6661IsFvP9738/VVVVSZKrr746vXv3zh133JGjjjoqb3vb25rt48orr0zv3r3z+9//Pu9617syZ86cDBgwICNHjkx1dXV22WWXHHTQQRut873vfW8++tGPJkm+9KUv5eabb85ll12W73znO+uM/fa3v539998/F1xwQdOyq666KgMHDszs2bMzePDgtp+wjXClBgAAAAAAW60lN9+SNYsXp8see6S6vj5d9tgjaxa/lCW33NpuNey3335Nf6+qqsqAAQMyf/78DY5/6KGH8vjjj6dHjx7p3r17unfvnr59+zZNy5Ak8+bNy8knn5y99torvXr1Ss+ePfPyyy9nzpw5Sf4VUCxbtiy77757Tj755Pz85z/P6tWrN1rnoYceus7jDV2p8dBDD+X2229vqq979+7Ze++9k6Spxi3BlRoAAAAAAGy1Vv3zmRR69Gi64qGqqiqFHj2z6pk57VZDdXV1s8dVVVUpFosbHP/yyy9nxIgR+eEPf7jOurUTfI8ZMyYvvvhivvnNb2bXXXdN165dc+ihhzbdnmrgwIGZNWtWbrnlltx888057bTTcvHFF+f3v//9OvW0xcsvv5zjjjsuF1544Trrdthhh01+/g0RagAAAAAAsNWq3nlgVt1zT0r9+6eqqiqlUinFpUtSs88by13aBh1wwAG5/vrr079///Ts2XO9Y+6666585zvfybHHHpvkXxOLv3aS7pqamhx33HE57rjjcvrpp2fvvffO3/72txxwwAHrfc4//elPaWhoaPZ4//3332CNP/3pTzNo0KB07tx+UYPbTwEAAAAAsNXq+Y6R6dSrV1Y+8XhWzZuXlU88nk69eqfH299e7tI26KSTTkq/fv3y7//+7/njH/+YJ598MnfccUfOOuus/POf/0yS7LXXXrn22mvzyCOP5J577slJJ52UmpqapueYOnVqfvCDH+Thhx/OP/7xj1x33XWpqanJrrvuusH93nDDDbnqqqsye/bsTJo0Kffee2/OOOOM9Y49/fTTs3Dhwpx44om577778sQTT+S3v/1txo0blzVr1mzeE/IqQg0AAAAAALZa3YYOTd2ZZ6T2kEPTqWeP1B5yaLtOEt4WtbW1+cMf/pBddtkl//mf/5k3vOEN+chHPpLly5c3Xbnxgx/8IIsWLcoBBxyQD33oQznrrLPSv3//pufo3bt3vve97+Wwww7Lfvvtl1tuuSX/7//9v2y//fYb3O/555+fH//4x9lvv/0ybdq0/OhHP8rQDZynHXfcMXfddVfWrFmTo446Kvvuu2/OPvvs9O7dO4XCloseqkqlUmmLPXsHtGTJkvTq1SuLFy/e4GU725pisZiFCxemb9++W/TNBmxd9A6gLfQOoLX0DaAt9A7YOixfvjxPPvlkdtttt3Tr1m2L769UKmX16tXp3Llz0/wbbF4be01b+t29rg4AAAAAAFQEoQYAAAAAAFARhBoAAAAAAEBFEGoAAAAAAAAVQagBAAAAAABUBKEGAAAA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xvve974sWLAg5557bubOnZvhw4fnpptuapo8fM6cOc0SmldeeSWnnXZa/vnPf6ampiZ77713rrvuurzvfe8r1yHQDupq6jKvcd7rD/xfhRQyevDoLVgRAAAAAADtrapUKpXKXUR7WrJkSXr16pXFixebU+N/FYvFLFy4MH379u2wl1Vd8dAVmfLglBbNqVFIIUP6DsnUo6emtrq2HaqDbVMl9A6g49E7gNbSN4C20DuAttA7yqul39236EqNv/71r9lnn31SKBTy17/+daNj99tvv9ZVCi3QMLQht825LbMWzmoWbFSlKtvXbJ9CCnlh+Qupq6nL6MGj0zC0QaABAAAAALCVaVGoMXz48MydOzf9+/fP8OHDU1VVlVdf4LH2cVVVVdasWbPFimXbVVtdm6lHT820mdMyffb0LFi2QIABAAAAALCNaVGo8eSTT6aurq7p71AOtdW1GT9sfMYPG1/uUgAAAAAAKIMWhRq77rrrev8OAAAAAADQXloUarzWY489lttvvz3z589Psdh84uZzzz13sxQGAAAAAADwaq0ONb73ve9lwoQJ6devXwYMGJCqqqqmdVVVVUINNlnjqkZzZwAAAAAAsI5Whxpf/vKX85WvfCWf+cxntkQ9bOMaVzVm7E1jM2vhrBTzr6uA5jXOy5QHp+S2Obdl6tFTBRsAAAAAANuoQms3WLRoUd773vduiVog02ZOaxZorFVMMbMWzsq0mdPKVBkAAAAAAOXW6lDjve99b373u99tiVog02dPXyfQWKuYYqbPnt7OFQEAAAAA0FG06PZT3/rWt5r+vueee+YLX/hC/vSnP2XfffdNdXV1s7FnnXXW5q2QbcqCZQs2aT0AAAAAAFuvFoUa3/jGN5o97t69e37/+9/n97//fbPlVVVVQg02SV1NXeY1ztvoegAAAAAAtk0tCjWefPLJLV0HJElGDx6dKQ9OWe8tqAopZPTg0WWoCgAAAACAjqDVc2q8WqlUSqlU2ly1QBqGNmRI3yEpvOatWUghQ/oOScPQhjJVBgAAAABAubUp1PjBD36QffbZJ926dUu3bt2yzz775Pvf//7mro1tUG11baYePTUThk9IfW19ClWF1NfWZ8LwCZl69NTUVteWu0QAAAAAAMqkRbeferVzzz03l1xySc4888wceuihSZIZM2bk4x//eObMmZMvfvGLm71I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lQwbNiyXX375etdfdNFF+da3vpUrrrgi99xzT7bbbruMGjUqy5cvbxpz0kkn5e9//3tuvvnm/OpXv8of/vCHnHLKKe11CEAZvF7vSJKjjz662c8hP/rRj5qt1ztg2/L73/8+p59+ev70pz/l5ptvzqpVq3LUUUfllVdeaRrzev9GWbNmTd75zndm5cqVufvuu3PNNddk6tSpOffcc8txSEA7aEnvSJKTTz652c8dF110UdM6vaODKbFNO+igg0qnn3560+M1a9aUdtxxx9LkyZPLWBXQUUyaNKk0bNiw9a576aWXStXV1aUbbrihadkjjzxSSlKaMWNGO1UIdDRJSj//+c+bHheLxdKAAQNKF198cdOyl156qdS1a9fSj370o1KpVCrNnDmzlKR03333NY35zW9+U6qqqio9++yz7VY7UD6v7R2lUqk0ZsyY0r//+79vcBu9A5g/f34pSen3v/99qVRq2b9RbrzxxlKhUCjNnTu3acyUKVNKPXv2LK1YsaJ9DwAoi9f2jlKpVDriiCNKH/vYxza4jd7RsbhSYxu2cuXK3H///Rk5cmTTskKhkJEjR2bGjBllrAzoSB577LHsuOOO2X333XPSSSdlzpw5SZL7778/q1atatZD9t577+yyyy56CNDkySefzNy5c5v1il69euXggw9u6hUzZsxI7969c+CBBzaNGTlyZAqFQu655552rxnoOO644470798/Q4YMyYQJE/Liiy82rdM7gMWLFydJ+vbtm6Rl/0aZMWNG9t1339TX1zeNGTVqVJYsWZK///3v7Vg9UC6v7R1r/fCHP0y/fv2yzz775JxzzkljY2PTOr2jY+lc7gIonxdeeCFr1qxp9mFMkvr6+jz66KNlqgroSA4++OBMnTo1Q4YMyfPPP5/zzz8/hx9+eB5++OHMnTs3Xbp0Se/evZttU19fn7lz55anYKDDWdsP1vfzxtp1c+fOTf/+/Zut79y5c/r27aufwDbs6KOPzn/+539mt912yxNPPJHPfe5zOeaYYzJjxox06tRJ74BtXLFYzNlnn53DDjss++yzT5K06N8oc+fOXe/PJWvXAVu39fWOJPnABz6QXXfdNTvuuGP++te/5jOf+UxmzZqVn/3sZ0n0jo5GqAHABh1zzDFNf99vv//f3r3HZFn/fxx/3XESDzeIoPftERMRKbGEUCyBxLNz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", + "text/plain": [ + "
" ] + }, + "metadata": {}, + "output_type": "display_data" } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3", - "language": "python", - "name": "python3" - }, - "language_info": { - "name": "python", - "version": "3.12" + ], + "source": [ + "df_plot = df.copy()\n", + "df_plot['row'] = np.arange(len(df_plot))\n", + "\n", + "valid = df_plot[df_plot['validity'] == 1]\n", + "invalid = df_plot[df_plot['validity'] == 0]\n", + "\n", + "fig, ax = plt.subplots(figsize=(16, 6))\n", + "ax.scatter(valid['row'], valid['phi'], s=30, label='feasible', color='tab:green')\n", + "ax.scatter(invalid['row'], invalid['phi'], s=16, label='infeasible', color='tab:red', alpha=0.6)\n", + "ax.set_xlabel('bitstring row index')\n", + "ax.set_ylabel('phi (rad)')\n", + "ax.set_title('Phase by route bitstring (feasible and infeasible)')\n", + "ax.legend()\n", + "ax.grid(alpha=0.25)\n", + "fig.tight_layout()\n", + "fig.savefig(validity_plot_out, dpi=200, bbox_inches='tight')\n", + "plt.show()\n", + "plt.close(fig)" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "6db27c39", + "metadata": {}, + "outputs": [], + "source": [ + "fig = circuit_drawer(compiled, output='mpl', fold=80, idle_wires=False)\n", + "fig.savefig(circuit_plot_out, dpi=200, bbox_inches='tight')\n", + "plt.show()\n", + "plt.close(fig)" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "9d10c013", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Saved table CSV: qiskit_tsp_table.csv\n", + "Saved full table plot: qiskit_tsp_table.png\n", + "Saved feasible/infeasible phase plot: qiskit_tsp_validity_phi.png\n", + "Saved circuit plot: qiskit_tsp_circuit.png\n" + ] } + ], + "source": [ + "print(f'Saved table CSV: {csv_out}')\n", + "print(f'Saved full table plot: {table_plot_out}')\n", + "print(f'Saved feasible/infeasible phase plot: {validity_plot_out}')\n", + "print(f'Saved circuit plot: {circuit_plot_out}')" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" }, - "nbformat": 4, - "nbformat_minor": 5 -} \ No newline at end of file + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.4" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/src/classical_funcs.py b/src/classical_funcs.py index e01bf9d..6baeda6 100644 --- a/src/classical_funcs.py +++ b/src/classical_funcs.py @@ -36,4 +36,4 @@ def generate_classical_problem(n): all_walks = generate_all_walks(n) all_costs = find_all_cost(cost_matrix, all_walks) best_walk, best_cost = find_best_walk(all_walks, all_costs) - return cost_matrix, all_walks, all_costs, best_walk, best_cost \ No newline at end of file + return cost_matrix, all_walks, all_costs, best_walk, best_cost