diff --git a/.gitignore b/.gitignore index e43b0f9..6e8821b 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1 @@ -.DS_Store +.DS_Store diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..81628fc --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,4 @@ +{ + "python-envs.defaultEnvManager": "ms-python.python:conda", + "python-envs.defaultPackageManager": "ms-python.python:conda" +} \ No newline at end of file diff --git a/Comparison.py b/Comparison.py new file mode 100644 index 0000000..088e757 --- /dev/null +++ b/Comparison.py @@ -0,0 +1,127 @@ +from qiskit import QuantumCircuit, transpile +from qiskit.transpiler import CouplingMap +from qiskit.quantum_info import Statevector, state_fidelity +from qiskit_aer import AerSimulator +from qiskit_aer.noise import NoiseModel, depolarizing_error + +# ------------------------------------------------- +# 1. Build GHZ +# ------------------------------------------------- +def build_ghz_circuit(n_qubits: int = 6) -> QuantumCircuit: + qc = QuantumCircuit(n_qubits) + qc.h(0) + for i in range(n_qubits - 1): + qc.cx(i, i + 1) + return qc + + +# ------------------------------------------------- +# 2. Same "platform style": fixed basis + fixed coupling map +# Use a 6-qubit connected chain, so memory stays small. +# ------------------------------------------------- +def get_platform_spec(n_qubits: int): + basis_gates = ["rz", "sx", "x", "ecr"] + + edges = [] + for i in range(n_qubits - 1): + edges.append([i, i + 1]) + edges.append([i + 1, i]) + + coupling = CouplingMap(edges) + return basis_gates, coupling + + +# ------------------------------------------------- +# 3. Build two noise models: low / high +# ------------------------------------------------- +def make_noise_model(p1: float, p2: float) -> NoiseModel: + noise_model = NoiseModel() + + err_1q = depolarizing_error(p1, 1) + err_2q = depolarizing_error(p2, 2) + + for gate in ["rz", "sx", "x"]: + noise_model.add_all_qubit_quantum_error(err_1q, gate) + + noise_model.add_all_qubit_quantum_error(err_2q, ["ecr"]) + + return noise_model + + +# ------------------------------------------------- +# 4. Noisy density-matrix simulation +# ------------------------------------------------- +def simulate_density_matrix(circuit: QuantumCircuit, noise_model: NoiseModel): + sim = AerSimulator(method="density_matrix", noise_model=noise_model) + + circ = circuit.copy() + circ.save_density_matrix() + + result = sim.run(circ).result() + rho = result.data(0)["density_matrix"] + return rho + + +# ------------------------------------------------- +# 5. Main +# ------------------------------------------------- +def main(): + n_qubits = 10 + ghz = build_ghz_circuit(n_qubits) + + basis_gates, coupling = get_platform_spec(n_qubits) + + ghz_t = transpile( + ghz, + basis_gates=basis_gates, + coupling_map=coupling, + optimization_level=3, + seed_transpiler=42, + ) + + # Ideal target + ideal_state = Statevector.from_instruction(ghz_t) + + # Low-noise version + noise_low = make_noise_model( + p1=0.0020, + p2=0.020, + ) + + # High-noise version + noise_high = make_noise_model( + p1=0.0035, + p2=0.020, + ) + + rho_low = simulate_density_matrix(ghz_t, noise_low) + rho_high = simulate_density_matrix(ghz_t, noise_high) + + fid_low = state_fidelity(ideal_state, rho_low) + fid_high = state_fidelity(ideal_state, rho_high) + + print("=== Transpiled GHZ circuit ===") + print(ghz_t) + print() + + ops = ghz_t.count_ops() + + total_gates = sum(ops.values()) + two_q_gates = sum(v for k, v in ops.items() if k in ["cx", "cz", "swap", "ecr"]) + one_q_gates = total_gates - two_q_gates + + print("num_qubits :", ghz_t.num_qubits) + print("depth :", ghz_t.depth()) + print("1Q gates :", one_q_gates) + print("2Q gates :", two_q_gates) + print("total gates:", total_gates) + print() + + print("=== Fidelity comparison ===") + print(f"Low-noise fidelity : {fid_low:.6f}") + print(f"High-noise fidelity : {fid_high:.6f}") + print(f"Δ Fidelity : {fid_low - fid_high:.6f}") + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/Example_GHZ.py b/Example_GHZ.py index 037408e..a58c3b8 100644 --- a/Example_GHZ.py +++ b/Example_GHZ.py @@ -1,166 +1,186 @@ -# ========================= -# Python path setup -# ========================= -import sys -from pathlib import Path -sys.path.insert(0, str(Path(__file__).parent / "src")) - -import numpy as np -import collections -from typing import List, Tuple -import matplotlib.pyplot as plt - -from qiskit import QuantumCircuit, ClassicalRegister, transpile -from qiskit.circuit import Instruction, CircuitInstruction -from qiskit.quantum_info import Statevector -from qiskit_aer import AerSimulator -from qiskit.visualization import plot_histogram -from qiskit_aer.noise import NoiseModel - -# ========================= -# Fake backends (heterogeneous QPUs) -# ========================= -from qiskit_ibm_runtime.fake_provider import ( - FakeVigoV2, - FakeLagosV2, - FakeCasablancaV2, - FakeYorktownV2, - FakeManilaV2, - FakeNairobiV2, - FakeMumbaiV2, - FakeKolkataV2, - FakeGuadalupeV2, - FakeAlmadenV2, - FakeAthensV2, - FakeCambridgeV2 -) - -# ========================= -# Distributed Quantum Computing Simulator -# ========================= -from dqc_simulator import DQCCircuit, DQCQPU, QPUManager -from dqc_simulator.backend import IonQ - - -# ============================================================ -# Fidelity metric: Hellinger fidelity for GHZ state -# ============================================================ -def score(exp_probs: dict) -> float: - """ - Compute Hellinger fidelity for an n-qubit GHZ state. - - Args: - exp_probs: Experimental probability distribution - e.g., {'000...0': p0, '111...1': p1} - - Returns: - fidelity in [0, 1] - """ - n = len(next(iter(exp_probs))) - - # Ideal GHZ distribution - ideal_probs = { - '0' * n: 0.5, - '1' * n: 0.5 - } - - # Hellinger fidelity - fidelity = sum( - np.sqrt(ideal_probs.get(k, 0)) * np.sqrt(p) - for k, p in exp_probs.items() - ) - return fidelity - - -# ============================================================ -# Step 1: Construct global GHZ circuit (12 qubits) -# ============================================================ -numbits = 12 -qc0 = QuantumCircuit(numbits, numbits) - -# GHZ state preparation -qc0.h(0) -for i in range(numbits - 1): - qc0.cx(i, i + 1) - -# Measurement on all qubits -for i in range(numbits): - qc0.measure(i, i) - - -# ============================================================ -# Step 2: Transform into Distributed Quantum Circuit -# ============================================================ -qc = DQCCircuit(qc0) - -# Partition the circuit across 4 QPUs -Partition = [3, 3, 3, 3] - - -# ============================================================ -# Step 3: Configure heterogeneous QPU group -# ============================================================ -QPUGROUP = QPUManager() - -# Add 4 QPUs (all FakeVigoV2 here, but framework supports heterogeneity) -QPUGROUP.add_qpu(DQCQPU(0, "FakeVigoV2")) -QPUGROUP.add_qpu(DQCQPU(1, "FakeVigoV2")) -QPUGROUP.add_qpu(DQCQPU(2, "FakeVigoV2")) -QPUGROUP.add_qpu(DQCQPU(3, "FakeVigoV2")) - -# Define inter-QPU network topology -dis = 5 # communication distance / cost -QPUGROUP.add_coonnection(0, 1, distance=dis) -QPUGROUP.add_coonnection(1, 2, distance=dis) -QPUGROUP.add_coonnection(2, 3, distance=dis) - - -# ============================================================ -# Step 4: Distributed execution with communication noise -# ============================================================ -result_qc = qc.Execution( - Partition, - QPUGROUP, - comm_noise=True -) - -# Obtain combined noise model (local + communication) -noise_model = qc.get_noise_model() - -# Backend simulator -sim = AerSimulator(noise_model=noise_model) - -# Transpile for backend -compiled = transpile(result_qc, sim) - -# Run simulation -job = sim.run(compiled, shots=10_000) -result = job.result() - - -# ============================================================ -# Step 5: Post-processing measurement results -# ============================================================ -counts = result.get_counts() - -# Only keep the first 12 bits (global logical qubits) -counts_res = {} -for bitstring, cnt in counts.items(): - bits = bitstring[:12] - counts_res[bits] = counts_res.get(bits, 0) + cnt - - -# ============================================================ -# Step 6: Visualization and PDF export -# ============================================================ - -# --- 6.1 Histogram --- -fig1 = plot_histogram(counts_res) -fig1.savefig("GHZ_12qubit_DQC_histogram.pdf") - - -# --- 6.2 Circuit diagram --- -fig2 = result_qc.draw("mpl", scale=0.7, fold=100) -fig2.savefig("GHZ_12qubit_DQC_circuit.pdf") - -plt.show() +# ========================= +# Python path setup +# ========================= +import sys +from pathlib import Path +sys.path.insert(0, str(Path(__file__).parent / "src")) + +import numpy as np +import collections +from typing import List, Tuple +import matplotlib.pyplot as plt + +from qiskit import QuantumCircuit, ClassicalRegister, transpile +from qiskit.circuit import Instruction, CircuitInstruction +from qiskit.quantum_info import Statevector +from qiskit_aer import AerSimulator +from qiskit.visualization import plot_histogram +from qiskit_aer.noise import NoiseModel + +# ========================= +# Fake backends (heterogeneous QPUs) +# ========================= +from qiskit_ibm_runtime.fake_provider import ( + FakeVigoV2, + FakeLagosV2, + FakeCasablancaV2, + FakeYorktownV2, + FakeManilaV2, + FakeNairobiV2, + FakeMumbaiV2, + FakeKolkataV2, + FakeGuadalupeV2, + FakeAlmadenV2, + FakeAthensV2, + FakeCambridgeV2 +) + +# ========================= +# Distributed Quantum Computing Simulator +# ========================= +from dqc_simulator import DQCCircuit, DQCQPU, QPUManager +from dqc_simulator.backend import IonQ +from dqc_simulator import extract_feature_vector, extract_feature_groups + +# ============================================================ +# Fidelity metric: Hellinger fidelity for GHZ state +# ============================================================ +def score(exp_probs: dict) -> float: + """ + Compute Hellinger fidelity for an n-qubit GHZ state. + + Args: + exp_probs: Experimental probability distribution + e.g., {'000...0': p0, '111...1': p1} + + Returns: + fidelity in [0, 1] + """ + n = len(next(iter(exp_probs))) + + # Ideal GHZ distribution + ideal_probs = { + '0' * n: 0.5, + '1' * n: 0.5 + } + + # Hellinger fidelity + fidelity = sum( + np.sqrt(ideal_probs.get(k, 0)) * np.sqrt(p) + for k, p in exp_probs.items() + ) + return fidelity + + +# ============================================================ +# Step 1: Construct global GHZ circuit (12 qubits) +# ============================================================ +numbits = 12 +qc0 = QuantumCircuit(numbits, numbits) + +# GHZ state preparation +qc0.h(0) +for i in range(numbits - 1): + qc0.cx(i, i + 1) + +# Measurement on all qubits +for i in range(numbits): + qc0.measure(i, i) + +for instr in qc0.data: + print(instr) + +# ============================================================ +# Step 2: Transform into Distributed Quantum Circuit +# ============================================================ +qc = DQCCircuit(qc0) + +# Partition the circuit across 4 QPUs +Partition = [3, 3, 3, 3] + + +# ============================================================ +# Step 3: Configure heterogeneous QPU group +# ============================================================ +QPUGROUP = QPUManager() + +# Add 4 QPUs (all FakeVigoV2 here, but framework supports heterogeneity) +QPUGROUP.add_qpu(DQCQPU(0, "FakeVigoV2")) +QPUGROUP.add_qpu(DQCQPU(1, "FakeVigoV2")) +QPUGROUP.add_qpu(DQCQPU(2, "FakeVigoV2")) +QPUGROUP.add_qpu(DQCQPU(3, "FakeVigoV2")) + +# Define inter-QPU network topology +dis = 5 # communication distance / cost +QPUGROUP.add_coonnection(0, 1, distance=dis) +QPUGROUP.add_coonnection(1, 2, distance=dis) +QPUGROUP.add_coonnection(2, 3, distance=dis) + + +# ============================================================ +# Step 4: Distributed execution with communication noise +# ============================================================ +result_qc = qc.Execution( + Partition, + QPUGROUP, + comm_noise=True +) + +# Obtain combined noise model (local + communication) +noise_model = qc.get_noise_model() + +# Backend simulator +sim = AerSimulator(noise_model=noise_model) + +# Transpile for backend +compiled = transpile(result_qc, sim) + +feature_vector = extract_feature_vector(compiled) +feature_groups = extract_feature_groups(compiled) + +print("Feature vector:", feature_vector) +print("Communication count (kraus):", feature_vector.get("comm_count")) +print( + "Communication noise mean/max/min/std:", + feature_groups["group_c_noise_strength"]["comm_noise_mean"], + feature_groups["group_c_noise_strength"]["comm_noise_max"], + feature_groups["group_c_noise_strength"]["comm_noise_min"], + feature_groups["group_c_noise_strength"]["comm_noise_std"], +) +# for instr in compiled.data: +# print(instr) + +# compiled.draw("mpl", scale=0.7, fold=100) +# plt.show() + +# # Run simulation +# job = sim.run(compiled, shots=10_000) +# result = job.result() + + +# # ============================================================ +# # Step 5: Post-processing measurement results +# # ============================================================ +# counts = result.get_counts() + +# # Only keep the first 12 bits (global logical qubits) +# counts_res = {} +# for bitstring, cnt in counts.items(): +# bits = bitstring[:12] +# counts_res[bits] = counts_res.get(bits, 0) + cnt + + +# # ============================================================ +# # Step 6: Visualization and PDF export +# # ============================================================ + +# # --- 6.1 Histogram --- +# fig1 = plot_histogram(counts_res) +# fig1.savefig("GHZ_12qubit_DQC_histogram.pdf") + + +# # --- 6.2 Circuit diagram --- +# fig2 = result_qc.draw("mpl", scale=0.7, fold=100) +# fig2.savefig("GHZ_12qubit_DQC_circuit.pdf") + +# plt.show() diff --git a/Example_Transpile_DAG_12Q.py b/Example_Transpile_DAG_12Q.py new file mode 100644 index 0000000..6eb8596 --- /dev/null +++ b/Example_Transpile_DAG_12Q.py @@ -0,0 +1,483 @@ +from collections import Counter, defaultdict +from statistics import mean +from math import pi + +from qiskit import QuantumCircuit, transpile +from qiskit.converters import circuit_to_dag +from qiskit.dagcircuit import DAGOpNode +from qiskit_ibm_runtime.fake_provider import FakeKolkataV2 + + +def build_common_ansatz(num_qubits: int = 12, layers: int = 3) -> QuantumCircuit: + """Create a common hardware-efficient ansatz circuit.""" + qc = QuantumCircuit(num_qubits, num_qubits, name="HEA_12Q") + + for q in range(num_qubits): + qc.h(q) + + for layer in range(layers): + for q in range(num_qubits): + theta = (layer + 1) * (q + 1) * pi / (num_qubits + 1) + phi = (layer + 1) * (q + 2) * pi / (num_qubits + 3) + qc.ry(theta, q) + qc.rz(phi, q) + + # Ring entanglement + for q in range(num_qubits - 1): + qc.cx(q, q + 1) + qc.cx(num_qubits - 1, 0) + + qc.barrier() + qc.measure(range(num_qubits), range(num_qubits)) + return qc + + +def safe_stats(values): + """Return simple statistics for a numeric list.""" + if not values: + return { + "count": 0, + "sum": 0.0, + "mean": 0.0, + "max": 0.0, + "min": 0.0, + } + return { + "count": len(values), + "sum": float(sum(values)), + "mean": float(mean(values)), + "max": float(max(values)), + "min": float(min(values)), + } + + +def get_inst_props(target, op_name, qargs): + """ + Safely read instruction properties from backend.target. + Returns (error, duration), both may be None. + """ + try: + if op_name not in target: + return None, None + props_map = target[op_name] + props = props_map.get(tuple(qargs), None) + except Exception: + props = None + + if props is None: + return None, None + + err = getattr(props, "error", None) + dur = getattr(props, "duration", None) + return err, dur + + +def circuit_metrics(circ: QuantumCircuit) -> dict: + """Basic circuit-level metrics.""" + ops = circ.count_ops() + two_qubit_gates = sum( + 1 + for inst in circ.data + if len(inst.qubits) == 2 and inst.operation.name != "barrier" + ) + + measure_count = sum(1 for inst in circ.data if inst.operation.name == "measure") + one_qubit_gates = sum( + 1 + for inst in circ.data + if len(inst.qubits) == 1 and inst.operation.name not in ("measure", "barrier") + ) + + return { + "num_qubits": circ.num_qubits, + "num_clbits": circ.num_clbits, + "size": circ.size(), + "depth": circ.depth(), + "width": circ.width(), + "num_nonlocal_gates": circ.num_nonlocal_gates(), + "one_qubit_gate_count": one_qubit_gates, + "two_qubit_gate_count": two_qubit_gates, + "measure_count": measure_count, + "op_histogram": dict(ops), + } + + +def dag_metrics(dag) -> dict: + """Extract DAG-related structural metrics.""" + op_nodes = list(dag.op_nodes()) + op_name_counter = Counter(node.op.name for node in op_nodes) + + qubit_activity = Counter() + for node in op_nodes: + for q in node.qargs: + qubit_activity[str(q)] += 1 + + longest_path_nodes = [node for node in dag.longest_path() if isinstance(node, DAGOpNode)] + longest_path_ops = [node.op.name for node in longest_path_nodes] + critical_path_2q_count = sum(1 for node in longest_path_nodes if len(node.qargs) == 2) + critical_path_1q_count = sum( + 1 + for node in longest_path_nodes + if len(node.qargs) == 1 and node.op.name != "measure" + ) + critical_path_measure_count = sum(1 for node in longest_path_nodes if node.op.name == "measure") + + layer_two_qubit_counts = [] + layer_total_op_counts = [] + + for layer in dag.layers(): + layer_dag = layer["graph"] + layer_ops = list(layer_dag.op_nodes()) + cnt_2q = sum(1 for n in layer_ops if len(n.qargs) == 2) + layer_two_qubit_counts.append(cnt_2q) + layer_total_op_counts.append(len(layer_ops)) + + busy_values = list(qubit_activity.values()) + + return { + "dag_depth": dag.depth(), + "dag_size": dag.size(), + "num_dag_op_nodes": len(op_nodes), + "dag_op_histogram": dict(op_name_counter), + "active_wires": len(list(dag.wires)), + "longest_path_op_count": len(longest_path_ops), + "longest_path_ops_preview": longest_path_ops[:30], + "critical_path_1q_count": critical_path_1q_count, + "critical_path_2q_count": critical_path_2q_count, + "critical_path_measure_count": critical_path_measure_count, + "critical_path_ratio": len(longest_path_ops) / dag.size() if dag.size() else 0.0, + "max_2q_gates_in_one_layer": max(layer_two_qubit_counts) if layer_two_qubit_counts else 0, + "avg_2q_gates_per_layer": ( + sum(layer_two_qubit_counts) / len(layer_two_qubit_counts) + if layer_two_qubit_counts + else 0.0 + ), + "avg_ops_per_layer": ( + sum(layer_total_op_counts) / len(layer_total_op_counts) + if layer_total_op_counts + else 0.0 + ), + "parallelism_score": dag.size() / dag.depth() if dag.depth() else 0.0, + "qubit_activity_stats": safe_stats(busy_values), + "top_busy_qubits": qubit_activity.most_common(8), + } + + +def noise_time_metrics(circ: QuantumCircuit, backend) -> dict: + """ + Extract noise- and time-related features from a transpiled circuit. + Assumes `circ` is already transpiled for `backend`. + """ + target = backend.target + + one_q_errors = [] + two_q_errors = [] + meas_errors = [] + + one_q_durations = [] + two_q_durations = [] + meas_durations = [] + + used_physical_qubits = set() + used_2q_edges = Counter() + op_error_records = [] + op_duration_records = [] + + qubit_gate_time = defaultdict(float) + qubit_gate_error_sum = defaultdict(float) + qubit_op_count = defaultdict(int) + + swap_count = 0 + + for inst in circ.data: + op = inst.operation + name = op.name + + if name == "barrier": + continue + + if name == "swap": + swap_count += 1 + + qargs = [circ.find_bit(q).index for q in inst.qubits] + + for q in qargs: + used_physical_qubits.add(q) + qubit_op_count[q] += 1 + + err, dur = get_inst_props(target, name, qargs) + + if err is not None: + op_error_records.append(err) + if dur is not None: + op_duration_records.append(dur) + + if dur is not None: + for q in qargs: + qubit_gate_time[q] += dur + + if err is not None: + for q in qargs: + qubit_gate_error_sum[q] += err + + if name == "measure": + if err is not None: + meas_errors.append(err) + if dur is not None: + meas_durations.append(dur) + + elif len(qargs) == 1: + if err is not None: + one_q_errors.append(err) + if dur is not None: + one_q_durations.append(dur) + + elif len(qargs) == 2: + if err is not None: + two_q_errors.append(err) + if dur is not None: + two_q_durations.append(dur) + used_2q_edges[tuple(qargs)] += 1 + + # Estimated circuit duration + try: + estimated_duration = circ.estimate_duration(target=target) + except Exception: + estimated_duration = None + + # T1 / T2 + t1_values = [] + t2_values = [] + per_qubit_noise_time = {} + + for q in sorted(used_physical_qubits): + try: + qp = backend.qubit_properties(q) + except Exception: + qp = None + + t1 = getattr(qp, "t1", None) if qp is not None else None + t2 = getattr(qp, "t2", None) if qp is not None else None + + if t1 is not None: + t1_values.append(t1) + if t2 is not None: + t2_values.append(t2) + + acc_time = qubit_gate_time.get(q, 0.0) + acc_err = qubit_gate_error_sum.get(q, 0.0) + + per_qubit_noise_time[q] = { + "t1": t1, + "t2": t2, + "acc_gate_time": acc_time, + "acc_gate_error_sum": acc_err, + "op_count": qubit_op_count.get(q, 0), + "time_over_t1": (acc_time / t1) if (t1 is not None and t1 > 0) else None, + "time_over_t2": (acc_time / t2) if (t2 is not None and t2 > 0) else None, + } + + time_over_t1 = [ + x["time_over_t1"] + for x in per_qubit_noise_time.values() + if x["time_over_t1"] is not None + ] + time_over_t2 = [ + x["time_over_t2"] + for x in per_qubit_noise_time.values() + if x["time_over_t2"] is not None + ] + + qubit_gate_time_values = [per_qubit_noise_time[q]["acc_gate_time"] for q in per_qubit_noise_time] + qubit_gate_err_values = [per_qubit_noise_time[q]["acc_gate_error_sum"] for q in per_qubit_noise_time] + + return { + "estimated_duration_sec": estimated_duration, + "swap_count": swap_count, + + "used_physical_qubits": sorted(used_physical_qubits), + "num_used_physical_qubits": len(used_physical_qubits), + "used_2q_edges_top": used_2q_edges.most_common(10), + + "all_op_error_stats": safe_stats(op_error_records), + "all_op_duration_stats": safe_stats(op_duration_records), + + "one_q_error_stats": safe_stats(one_q_errors), + "two_q_error_stats": safe_stats(two_q_errors), + "meas_error_stats": safe_stats(meas_errors), + + "one_q_duration_stats": safe_stats(one_q_durations), + "two_q_duration_stats": safe_stats(two_q_durations), + "meas_duration_stats": safe_stats(meas_durations), + + "t1_stats": safe_stats(t1_values), + "t2_stats": safe_stats(t2_values), + + "time_over_t1_stats": safe_stats(time_over_t1), + "time_over_t2_stats": safe_stats(time_over_t2), + + "per_qubit_gate_time_stats": safe_stats(qubit_gate_time_values), + "per_qubit_gate_error_stats": safe_stats(qubit_gate_err_values), + + "per_qubit_noise_time": per_qubit_noise_time, + } + + +def build_feature_groups( + circuit_metrics: dict, + dag_info: dict, + noise_time_info: dict, +) -> dict: + group1_compiled_circuit_structure = { + "trans_num_qubits": circuit_metrics["num_qubits"], + "trans_num_clbits": circuit_metrics["num_clbits"], + "trans_size": circuit_metrics["size"], + "trans_depth": circuit_metrics["depth"], + "trans_width": circuit_metrics["width"], + "trans_num_nonlocal_gates": circuit_metrics["num_nonlocal_gates"], + "trans_one_qubit_gate_count": circuit_metrics["one_qubit_gate_count"], + "trans_two_qubit_gate_count": circuit_metrics["two_qubit_gate_count"], + "trans_measure_count": circuit_metrics["measure_count"], + "trans_two_qubit_gate_fraction": ( + circuit_metrics["two_qubit_gate_count"] / circuit_metrics["size"] + if circuit_metrics["size"] else 0.0 + ), + + "dag_depth": dag_info["dag_depth"], + "dag_size": dag_info["dag_size"], + "num_dag_op_nodes": dag_info["num_dag_op_nodes"], + "active_wires": dag_info["active_wires"], + "longest_path_op_count": dag_info["longest_path_op_count"], + "critical_path_1q_count": dag_info["critical_path_1q_count"], + "critical_path_2q_count": dag_info["critical_path_2q_count"], + "critical_path_measure_count": dag_info["critical_path_measure_count"], + "critical_path_ratio": dag_info["critical_path_ratio"], + "max_2q_gates_in_one_layer": dag_info["max_2q_gates_in_one_layer"], + "avg_2q_gates_per_layer": dag_info["avg_2q_gates_per_layer"], + "avg_ops_per_layer": dag_info["avg_ops_per_layer"], + "parallelism_score": dag_info["parallelism_score"], + "qubit_activity_mean": dag_info["qubit_activity_stats"]["mean"], + "qubit_activity_max": dag_info["qubit_activity_stats"]["max"], + } + + group2_layout_routing = { + "swap_count": noise_time_info["swap_count"], + "num_used_physical_qubits": noise_time_info["num_used_physical_qubits"], + } + + group3_hardware_noise = { + "one_q_error_mean": noise_time_info["one_q_error_stats"]["mean"], + "one_q_error_max": noise_time_info["one_q_error_stats"]["max"], + "one_q_error_sum": noise_time_info["one_q_error_stats"]["sum"], + + "two_q_error_mean": noise_time_info["two_q_error_stats"]["mean"], + "two_q_error_max": noise_time_info["two_q_error_stats"]["max"], + "two_q_error_sum": noise_time_info["two_q_error_stats"]["sum"], + + "meas_error_mean": noise_time_info["meas_error_stats"]["mean"], + "meas_error_max": noise_time_info["meas_error_stats"]["max"], + "meas_error_sum": noise_time_info["meas_error_stats"]["sum"], + + "all_op_error_mean": noise_time_info["all_op_error_stats"]["mean"], + "all_op_error_max": noise_time_info["all_op_error_stats"]["max"], + "all_op_error_sum": noise_time_info["all_op_error_stats"]["sum"], + + "t1_mean_used": noise_time_info["t1_stats"]["mean"], + "t1_min_used": noise_time_info["t1_stats"]["min"], + "t2_mean_used": noise_time_info["t2_stats"]["mean"], + "t2_min_used": noise_time_info["t2_stats"]["min"], + + "per_qubit_gate_error_mean": noise_time_info["per_qubit_gate_error_stats"]["mean"], + "per_qubit_gate_error_max": noise_time_info["per_qubit_gate_error_stats"]["max"], + } + + group4_time_decoherence = { + "estimated_duration_sec": ( + noise_time_info["estimated_duration_sec"] + if noise_time_info["estimated_duration_sec"] is not None + else -1.0 + ), + + "one_q_duration_sum": noise_time_info["one_q_duration_stats"]["sum"], + "two_q_duration_sum": noise_time_info["two_q_duration_stats"]["sum"], + "meas_duration_sum": noise_time_info["meas_duration_stats"]["sum"], + + "all_op_duration_mean": noise_time_info["all_op_duration_stats"]["mean"], + "all_op_duration_max": noise_time_info["all_op_duration_stats"]["max"], + "all_op_duration_sum": noise_time_info["all_op_duration_stats"]["sum"], + + "time_over_t1_mean": noise_time_info["time_over_t1_stats"]["mean"], + "time_over_t1_max": noise_time_info["time_over_t1_stats"]["max"], + "time_over_t2_mean": noise_time_info["time_over_t2_stats"]["mean"], + "time_over_t2_max": noise_time_info["time_over_t2_stats"]["max"], + + "per_qubit_gate_time_mean": noise_time_info["per_qubit_gate_time_stats"]["mean"], + "per_qubit_gate_time_max": noise_time_info["per_qubit_gate_time_stats"]["max"], + } + + return { + "group1_compiled_circuit_structure": group1_compiled_circuit_structure, + "group2_layout_routing": group2_layout_routing, + "group3_hardware_noise": group3_hardware_noise, + "group4_time_decoherence": group4_time_decoherence, + } + +def build_flattened_feature_vector(feature_groups: dict) -> dict: + flat = {} + for group_name in [ + "group1_compiled_circuit_structure", + "group2_layout_routing", + "group3_hardware_noise", + "group4_time_decoherence", + ]: + flat.update(feature_groups[group_name]) + return flat + + +def pretty_print_dict(title: str, d: dict) -> None: + print(f"\n=== {title} ===") + for k, v in d.items(): + print(f"{k}: {v}") + + +def main() -> None: + # 1) Build original circuit + qc = build_common_ansatz(num_qubits=12, layers=3) + + # 2) Backend + backend = FakeKolkataV2() + + # 3) Transpile + tqc = transpile( + qc, + backend=backend, + optimization_level=3, + seed_transpiler=7, + ) + + # 4) Convert to DAG + dag = circuit_to_dag(tqc) + + # 5) Extract metrics + circuit_info = circuit_metrics(tqc) + dag_info = dag_metrics(dag) + noise_time_info = noise_time_metrics(tqc, backend) + + # 6) Build grouped features + feature_groups = build_feature_groups( + circuit_info, + dag_info, + noise_time_info, + ) + + feature_vector = build_flattened_feature_vector(feature_groups) + + pretty_print_dict("Group 1: Compiled Circuit Structure", feature_groups["group1_compiled_circuit_structure"]) + pretty_print_dict("Group 2: Layout / Routing", feature_groups["group2_layout_routing"]) + pretty_print_dict("Group 3: Hardware Noise", feature_groups["group3_hardware_noise"]) + pretty_print_dict("Group 4: Time / Decoherence", feature_groups["group4_time_decoherence"]) + pretty_print_dict("Flattened Feature Vector (for ML)", feature_vector) + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/Experiment_hdfm.py b/Experiment_hdfm.py new file mode 100644 index 0000000..d3fb196 --- /dev/null +++ b/Experiment_hdfm.py @@ -0,0 +1,370 @@ +""" +量子哈密顿量模拟 - 1D横场伊辛模型(TFIM) +使用Qiskit实现,支持分布式量子计算(DQC) +""" +# ========================= +# Python path setup +# ========================= +import sys +from pathlib import Path +sys.path.insert(0, str(Path(__file__).parent / "src")) + +import numpy as np +import time +import collections +from typing import List, Tuple +import matplotlib.pyplot as plt + +from qiskit import QuantumCircuit, ClassicalRegister, transpile +from qiskit.circuit import Instruction, CircuitInstruction +from qiskit.converters import circuit_to_dag +from qiskit.quantum_info import Statevector +from qiskit_aer import AerSimulator +from qiskit.visualization import plot_histogram +from qiskit_aer.noise import NoiseModel + +# ========================= +# Fake backends (heterogeneous QPUs) +# ========================= +from qiskit_ibm_runtime.fake_provider import ( + FakeVigoV2, + FakeLagosV2, + FakeCasablancaV2, + FakeYorktownV2, + FakeManilaV2, + FakeNairobiV2, + FakeMumbaiV2, + FakeKolkataV2, + FakeGuadalupeV2, + FakeAlmadenV2, + FakeAthensV2, + FakeCambridgeV2 +) + +# ========================= +# Distributed Quantum Computing Simulator +# ========================= +from dqc_simulator import DQCCircuit, DQCQPU, QPUManager +from dqc_simulator.backend import IonQ + +class HamiltonianSimulationQiskit: + """ + 1D横场伊辛模型(TFIM)哈密顿量模拟的Qiskit实现 + + Parameters: + ----------- + num_qubits : int + 量子比特数量 + time_step : int + 时间步长 + total_time : int + 总演化时间 + """ + + def __init__(self, num_qubits: int, time_step: int, total_time: int) -> None: + self.num_qubits = num_qubits + self.time_step = time_step + self.total_time = total_time + + def circuit(self) -> QuantumCircuit: + """ + 生成量子电路 + + Returns: + -------- + QuantumCircuit : 构建的量子电路 + """ + # 物理常数 + hbar = 0.658212 # eV*fs (约化普朗克常数) + jz = hbar * np.pi / 4 # eV, 耦合系数 + freq = 0.0048 # 1/fs, MoSe2声子频率 + + # 声子参数 + w_ph = 2 * np.pi * freq + e_ph = 3 * np.pi * hbar / (8 * np.cos(np.pi * freq)) + + # 初始化量子电路 + qc = QuantumCircuit(self.num_qubits, self.num_qubits) + + # 构建时间演化电路 + num_steps = int(self.total_time / self.time_step) + for step in range(num_steps): + t = (step + 0.5) * self.time_step + + # 单量子比特项 (横场项) + psi = -2.0 * e_ph * np.cos(w_ph * t) * self.time_step / hbar + for q in range(self.num_qubits): + qc.h(q) + qc.rz(psi, q) + qc.h(q) + + # 双量子比特耦合项 (伊辛相互作用) + psi2 = -2.0 * jz * self.time_step / hbar + for i in range(self.num_qubits - 1): + qc.cx(i, i + 1) + qc.rz(psi2, i + 1) + qc.cx(i, i + 1) + + # 添加测量 + qc.measure(range(self.num_qubits), range(self.num_qubits)) + return qc + + def _get_ideal_counts(self, circuit: QuantumCircuit) -> collections.Counter: + """ + 计算理想(无噪声)概率分布 + + Parameters: + ----------- + circuit : QuantumCircuit + 量子电路 + + Returns: + -------- + Counter : 理想概率分布 + """ + qc_no_measure = circuit.remove_final_measurements(inplace=False) + sv = Statevector.from_instruction(qc_no_measure) + probs = sv.probabilities_dict() + return collections.Counter(probs) + + def _average_magnetization(self, result: dict, shots: int) -> float: + """ + 计算平均磁化强度 + + Parameters: + ----------- + result : dict + 测量结果计数 + shots : int + 总测量次数 + + Returns: + -------- + float : 平均磁化强度 + """ + mag = 0 + for spin_str, count in result.items(): + bitstring = spin_str[::-1] + # 将比特串转换为自旋: 0→+1, 1→-1 + spin_int = [1 - 2 * int(s) for s in bitstring] + mag += (sum(spin_int) / len(spin_int)) * count + average_mag = mag / shots + return average_mag + + def score(self, counts: collections.Counter) -> float: + """ + 基于磁化强度计算保真度评分 + + Parameters: + ----------- + counts : Counter + 实验测量结果 + + Returns: + -------- + float : [0,1]范围的评分,1表示完美匹配 + """ + ideal_counts = self._get_ideal_counts(self.circuit()) + total_shots = sum(counts.values()) + + mag_ideal = self._average_magnetization(ideal_counts, 1) + mag_experimental = self._average_magnetization(counts, total_shots) + + return 1 - abs(mag_ideal - mag_experimental) / 2 + + def score2(self, counts: collections.Counter) -> float: + """ + 使用Hellinger保真度评估模拟质量 + + Parameters: + ----------- + counts : Counter + 实验测量结果 + + Returns: + -------- + float : Hellinger保真度 [0,1] + """ + # 获取电路并移除测量 + qc = self.circuit() + qc_no_measure = qc.copy() + qc_no_measure.data = [ + inst for inst in qc_no_measure.data + if inst[0].name != 'measure' + ] + + # 生成理想状态向量 + sv = Statevector.from_instruction(qc_no_measure) + ideal_probs = sv.probabilities_dict() + + # 归一化实验概率 + total_shots = sum(counts.values()) + exp_probs = {k: v / total_shots for k, v in counts.items()} + + # 计算Hellinger保真度 + fidelity = sum( + np.sqrt(ideal_probs.get(k, 0)) * np.sqrt(p) + for k, p in exp_probs.items() + ) + + return fidelity + + +def count_two_qubit_gates(circuit: QuantumCircuit) -> int: + """ + 统计电路中两比特门的数量 + + Parameters: + ----------- + circuit : QuantumCircuit + 量子电路 + + Returns: + -------- + int : 两比特门数量 + """ + dag = circuit_to_dag(circuit) + dag.remove_all_ops_named("barrier") + return len(list(dag.two_qubit_ops())) + + +def setup_qpugroup(qpu_configs: List[Tuple[int, str]], + connections: List[Tuple[int, int]], + dist: float = 0.5) -> QPUManager: + """ + 设置QPU组 + + Parameters: + ----------- + qpu_configs : List[Tuple[int, str]] + QPU配置列表,每个元素为(qpu_id, backend_name) + connections : List[Tuple[int, int]] + QPU连接列表,每个元素为(qpu1_id, qpu2_id) + dist : float + QPU间距离,默认0.5 + + Returns: + -------- + QPUManager : 配置好的QPU管理器 + """ + qpugroup = QPUManager() + + # 添加QPU + for qpu_id, backend_name in qpu_configs: + qpugroup.add_qpu(DQCQPU(qpu_id, backend_name)) + + # 添加连接 + for qpu1, qpu2 in connections: + qpugroup.add_coonnection(qpu1, qpu2, distance=dist) + + return qpugroup + + +def run_hamiltonian_dqc(ham: HamiltonianSimulationQiskit, + partition: List[int], + qpugroup: QPUManager, + numbits: int = 12) -> Tuple[float, int, int]: + """ + 运行哈密顿量DQC模拟并测量性能指标 + + Parameters: + ----------- + ham : HamiltonianSimulationQiskit + 哈密顿量模拟对象 + partition : List[int] + 量子比特分区方案 + qpugroup : QPUManager + QPU管理器 + numbits : int + 保留的比特数,默认12 + + Returns: + -------- + Tuple[float, int, int] : (保真度, 远程门数量, 两比特门总数) + """ + qc = ham.circuit() + + print(f" 电路信息: {qc.num_qubits} qubits, {qc.num_clbits} classical bits") + + try: + # 执行DQC + qc_dqc = DQCCircuit(qc) + result_qc = qc_dqc.Execution(partition, qpugroup, comm_noise=True, measure_time = True) + + # 获取噪声模型并运行仿真 + noise_model = qc_dqc.get_noise_model() + sim = AerSimulator(noise_model=noise_model) + compiled = transpile(result_qc, sim) + start_time = time.time() + job = sim.run(compiled, shots=1000) + result = job.result() + end_time = time.time() + elapsed_time = end_time - start_time + print(f"Running time: {elapsed_time:.4f} seconds") + + # 处理测量结果 + counts = result.get_counts() + + # 保留前numbits位 + new_counts = {} + for bitstring, cnt in counts.items(): + bits = bitstring[:numbits] + new_counts[bits] = new_counts.get(bits, 0) + cnt + + # 计算保真度 (Hellinger fidelity) + fidelity = ham.score2(new_counts) + + # 获取性能指标 + num_remote_gates = qc_dqc.Num_RemoteGate + num_two_qubit_gates = count_two_qubit_gates(result_qc) + + return fidelity, num_remote_gates, num_two_qubit_gates + + except Exception as e: + print(f" Error with partition {partition}: {e}") + import traceback + traceback.print_exc() + return None, None, None + + +def main(): + """ + 主函数 - 演示如何使用 + """ + # 创建12量子比特的TFIM电路 + ham = HamiltonianSimulationQiskit(num_qubits=20, time_step=1, total_time=3) + qc = ham.circuit() + qc0 = qc.copy() + + # 设置分布式量子计算 + qc_dqc = DQCCircuit(qc0) + partition = [4, 4, 4, 4, 4] # 4个QPU,每个3个量子比特 + + # 配置QPU组 + qpugroup = QPUManager() + for i in range(5): + qpugroup.add_qpu(DQCQPU(i, "FakeVigoV2")) + + # 添加QPU连接(线性拓扑) + distance = 5 + qpugroup.add_coonnection(0, 1, distance=distance) + # 可以添加更多连接以形成不同的拓扑结构 + # qpugroup.add_coonnection(0, 2, distance=distance) + qpugroup.add_coonnection(1, 2, distance=distance) + # qpugroup.add_coonnection(1, 3, distance=distance) + qpugroup.add_coonnection(2, 3, distance=distance) + qpugroup.add_coonnection(3, 4, distance=distance) + + # 运行DQC模拟 + fidelity, num_remote, num_two_qubit = run_hamiltonian_dqc( + ham, partition, qpugroup, numbits=20 + ) + + print(f"\n结果:") + print(f" Hellinger保真度: {fidelity:.4f}") + # print(f" 远程门数量: {num_remote}") + # print(f" 两比特门总数: {num_two_qubit}") + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/LICENSE b/LICENSE index 261eeb9..29f81d8 100644 --- a/LICENSE +++ b/LICENSE @@ -1,201 +1,201 @@ - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright [yyyy] [name of copyright owner] - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/README.md b/README.md index c50f63c..61e8271 100644 --- a/README.md +++ b/README.md @@ -1,110 +1,110 @@ -# Distributed Quantum Circuit - -This repository demonstrates the creation, partitioning, execution, and visualization of a distributed quantum circuit using **DQCCircuit** and QPU simulation. It also shows how to save measurement results and circuit diagrams as images. - ---- - -## Example Environment - -- **Python**: 3.13.X -- **Qiskit** -- **Qiskit Aer** -- **Qiskit IBM Runtime** -- **Matplotlib** -- **pylatexenc** - -Install dependencies with pip: - -```bash -# Make sure you are using Python 3.13.5 -pip install qiskit qiskit-aer qiskit-ibm-runtime matplotlib pylatexenc -``` - -## Quick Start Guide - -### 1. Define Circuit Size -Set the number of qubits for your quantum circuit: -```python -numbits = 12 # Total logical qubits -``` - -### 2. Create Global Quantum Circuit -Build your quantum algorithm (e.g., GHZ state preparation): -```python -qc0 = QuantumCircuit(numbits, numbits) -qc0.h(0) -for i in range(numbits - 1): - qc0.cx(i, i + 1) -# Add measurements -for i in range(numbits): - qc0.measure(i, i) -``` - -### 3. Convert to Distributed Circuit -Transform the standard circuit into a distributed format: -```python -qc = DQCCircuit(qc0) -``` - -### 4. Define Circuit Partition -Specify how to distribute qubits across QPUs: -```python -# Option 1: Equal distribution by count -Partition = [3, 3, 3, 3] # 4 QPUs, 3 qubits each - -# Option 2: Explicit qubit assignment -Partition = [[0,1,2], [3,4,5], [6,7,8], [9,10,11]] -``` - -### 5. Configure QPU Manager -Set up heterogeneous QPU group with network topology: -```python -QPUGROUP = QPUManager() - -# Add QPUs with different backends -QPUGROUP.add_qpu(DQCQPU(0, "FakeVigoV2")) -QPUGROUP.add_qpu(DQCQPU(1, "FakeLagosV2")) -QPUGROUP.add_qpu(DQCQPU(2, "FakeAthensV2")) -QPUGROUP.add_qpu(DQCQPU(3, "FakeManilaV2")) - -# Define inter-QPU connections -dis = 5 # Communication distance/cost -QPUGROUP.add_coonnection(0, 1, distance=dis) -QPUGROUP.add_coonnection(1, 2, distance=dis) -QPUGROUP.add_coonnection(2, 3, distance=dis) -``` - -### 6. Execute Distributed Circuit -Run with communication noise modeling: -```python -result_qc = qc.Execution( - Partition, - QPUGROUP, - comm_noise=True # Enable teleportation noise -) -``` - -### 7. Simulate and Analyze Results -Execute on AerSimulator with combined noise model: -```python -# Get combined noise model (local QPU + communication) -noise_model = qc.get_noise_model() -sim = AerSimulator(noise_model=noise_model) - -# Transpile and run -compiled = transpile(result_qc, sim) -job = sim.run(compiled, shots=10_000) -result = job.result() -``` - -### 8. Visualize and Export -Generate publication-ready figures: -```python -# Measurement histogram -fig1 = plot_histogram(counts_res) -fig1.savefig("GHZ_12qubit_DQC_histogram.pdf") - -# Circuit diagram -fig2 = result_qc.draw("mpl", scale=0.7, fold=100) -fig2.savefig("GHZ_12qubit_DQC_circuit.pdf") -``` +# Distributed Quantum Circuit + +This repository demonstrates the creation, partitioning, execution, and visualization of a distributed quantum circuit using **DQCCircuit** and QPU simulation. It also shows how to save measurement results and circuit diagrams as images. + +--- + +## Example Environment + +- **Python**: 3.13.X +- **Qiskit** +- **Qiskit Aer** +- **Qiskit IBM Runtime** +- **Matplotlib** +- **pylatexenc** + +Install dependencies with pip: + +```bash +# Make sure you are using Python 3.13.5 +pip install qiskit qiskit-aer qiskit-ibm-runtime matplotlib pylatexenc +``` + +## Quick Start Guide + +### 1. Define Circuit Size +Set the number of qubits for your quantum circuit: +```python +numbits = 12 # Total logical qubits +``` + +### 2. Create Global Quantum Circuit +Build your quantum algorithm (e.g., GHZ state preparation): +```python +qc0 = QuantumCircuit(numbits, numbits) +qc0.h(0) +for i in range(numbits - 1): + qc0.cx(i, i + 1) +# Add measurements +for i in range(numbits): + qc0.measure(i, i) +``` + +### 3. Convert to Distributed Circuit +Transform the standard circuit into a distributed format: +```python +qc = DQCCircuit(qc0) +``` + +### 4. Define Circuit Partition +Specify how to distribute qubits across QPUs: +```python +# Option 1: Equal distribution by count +Partition = [3, 3, 3, 3] # 4 QPUs, 3 qubits each + +# Option 2: Explicit qubit assignment +Partition = [[0,1,2], [3,4,5], [6,7,8], [9,10,11]] +``` + +### 5. Configure QPU Manager +Set up heterogeneous QPU group with network topology: +```python +QPUGROUP = QPUManager() + +# Add QPUs with different backends +QPUGROUP.add_qpu(DQCQPU(0, "FakeVigoV2")) +QPUGROUP.add_qpu(DQCQPU(1, "FakeLagosV2")) +QPUGROUP.add_qpu(DQCQPU(2, "FakeAthensV2")) +QPUGROUP.add_qpu(DQCQPU(3, "FakeManilaV2")) + +# Define inter-QPU connections +dis = 5 # Communication distance/cost +QPUGROUP.add_coonnection(0, 1, distance=dis) +QPUGROUP.add_coonnection(1, 2, distance=dis) +QPUGROUP.add_coonnection(2, 3, distance=dis) +``` + +### 6. Execute Distributed Circuit +Run with communication noise modeling: +```python +result_qc = qc.Execution( + Partition, + QPUGROUP, + comm_noise=True # Enable teleportation noise +) +``` + +### 7. Simulate and Analyze Results +Execute on AerSimulator with combined noise model: +```python +# Get combined noise model (local QPU + communication) +noise_model = qc.get_noise_model() +sim = AerSimulator(noise_model=noise_model) + +# Transpile and run +compiled = transpile(result_qc, sim) +job = sim.run(compiled, shots=10_000) +result = job.result() +``` + +### 8. Visualize and Export +Generate publication-ready figures: +```python +# Measurement histogram +fig1 = plot_histogram(counts_res) +fig1.savefig("GHZ_12qubit_DQC_histogram.pdf") + +# Circuit diagram +fig2 = result_qc.draw("mpl", scale=0.7, fold=100) +fig2.savefig("GHZ_12qubit_DQC_circuit.pdf") +``` diff --git a/__pycache__/feature.cpython-313.pyc b/__pycache__/feature.cpython-313.pyc new file mode 100644 index 0000000..42355db Binary files /dev/null and b/__pycache__/feature.cpython-313.pyc differ diff --git a/feature.py b/feature.py new file mode 100644 index 0000000..e923e42 --- /dev/null +++ b/feature.py @@ -0,0 +1,439 @@ +from collections import Counter, defaultdict +from statistics import mean, pstdev +from math import pi + +from qiskit import QuantumCircuit, transpile +from qiskit.converters import circuit_to_dag +from qiskit.dagcircuit import DAGOpNode +from qiskit_ibm_runtime.fake_provider import FakeKolkataV2 + + +def build_common_ansatz(num_qubits: int = 12, layers: int = 3) -> QuantumCircuit: + """Create a common hardware-efficient ansatz circuit.""" + qc = QuantumCircuit(num_qubits, num_qubits, name="HEA_12Q") + + for q in range(num_qubits): + qc.h(q) + + for layer in range(layers): + for q in range(num_qubits): + theta = (layer + 1) * (q + 1) * pi / (num_qubits + 1) + phi = (layer + 1) * (q + 2) * pi / (num_qubits + 3) + qc.ry(theta, q) + qc.rz(phi, q) + + # Ring entanglement + for q in range(num_qubits - 1): + qc.cx(q, q + 1) + qc.cx(num_qubits - 1, 0) + + qc.barrier() + qc.measure(range(num_qubits), range(num_qubits)) + return qc + + +def safe_stats(values): + """Return simple statistics for a numeric list.""" + if not values: + return { + "count": 0, + "sum": 0.0, + "mean": 0.0, + "max": 0.0, + "min": 0.0, + "std": 0.0, + } + + return { + "count": len(values), + "sum": float(sum(values)), + "mean": float(mean(values)), + "max": float(max(values)), + "min": float(min(values)), + "std": float(pstdev(values)) if len(values) > 1 else 0.0, + } + + +def get_inst_props(target, op_name, qargs): + """ + Safely read instruction properties from backend.target. + Returns (error, duration), both may be None. + """ + try: + if op_name not in target: + return None, None + props_map = target[op_name] + props = props_map.get(tuple(qargs), None) + except Exception: + props = None + + if props is None: + return None, None + + err = getattr(props, "error", None) + dur = getattr(props, "duration", None) + return err, dur + + +def kraus_comm_noise_strength(op) -> float | None: + """ + Convert a Kraus instruction into a scalar communication-noise strength. + Uses: error = 1 - entanglement_fidelity_to_identity. + """ + try: + kraus_ops = list(getattr(op, "params", [])) + if not kraus_ops: + return None + + dim = kraus_ops[0].shape[0] + if dim <= 0: + return None + + fe = sum(abs(k.trace()) ** 2 for k in kraus_ops) / (dim * dim) + err = 1.0 - float(fe.real) + if err < 0: + return 0.0 + if err > 1: + return 1.0 + return err + except Exception: + return None + + +def circuit_metrics(circ: QuantumCircuit) -> dict: + """Basic circuit-level metrics.""" + two_qubit_gates = sum( + 1 + for inst in circ.data + if len(inst.qubits) == 2 and inst.operation.name != "barrier" + ) + + measure_count = sum(1 for inst in circ.data if inst.operation.name == "measure") + comm_count = sum(1 for inst in circ.data if inst.operation.name == "kraus") + one_qubit_gates = sum( + 1 + for inst in circ.data + if len(inst.qubits) == 1 and inst.operation.name not in ("measure", "barrier") + ) + + return { + "num_qubits": circ.num_qubits, + "depth": circ.depth(), + "one_qubit_gate_count": one_qubit_gates, + "two_qubit_gate_count": two_qubit_gates, + "measure_count": measure_count, + "comm_count": comm_count, + } + + +def dag_metrics(dag) -> dict: + """Extract only the selected DAG-related features.""" + op_nodes = list(dag.op_nodes()) + + qubit_activity = Counter() + for node in op_nodes: + for q in node.qargs: + qubit_activity[str(q)] += 1 + + longest_path_nodes = [node for node in dag.longest_path() if isinstance(node, DAGOpNode)] + + critical_path_2q_count = sum(1 for node in longest_path_nodes if len(node.qargs) == 2) + critical_path_1q_count = sum( + 1 + for node in longest_path_nodes + if len(node.qargs) == 1 and node.op.name != "measure" + ) + + busy_values = list(qubit_activity.values()) + busy_stats = safe_stats(busy_values) + + return { + "critical_path_1q_count": critical_path_1q_count, + "critical_path_2q_count": critical_path_2q_count, + "critical_path_ratio": len(longest_path_nodes) / dag.size() if dag.size() else 0.0, + "qubit_activity_mean": busy_stats["mean"], + "qubit_activity_max": busy_stats["max"], + } + + +def noise_time_metrics(circ: QuantumCircuit, backend) -> dict: + """ + Extract selected noise- and time-related features from a transpiled circuit. + Assumes `circ` is already transpiled for `backend`. + """ + target = backend.target + + one_q_errors = [] + two_q_errors = [] + meas_errors = [] + comm_noise_errors = [] + + two_q_durations = [] + + op_error_records = [] + + qubit_gate_time = defaultdict(float) + qubit_gate_error_sum = defaultdict(float) + + used_physical_qubits = set() + + for inst in circ.data: + op = inst.operation + name = op.name + + if name == "barrier": + continue + + qargs = [circ.find_bit(q).index for q in inst.qubits] + + for q in qargs: + used_physical_qubits.add(q) + + err, dur = get_inst_props(target, name, qargs) + + if err is not None: + op_error_records.append(err) + for q in qargs: + qubit_gate_error_sum[q] += err + + if dur is not None: + for q in qargs: + qubit_gate_time[q] += dur + + if name == "measure": + if err is not None: + meas_errors.append(err) + + if name == "kraus": + comm_err = kraus_comm_noise_strength(op) + if comm_err is not None: + comm_noise_errors.append(comm_err) + + elif len(qargs) == 1: + if err is not None: + one_q_errors.append(err) + + elif len(qargs) == 2: + if err is not None: + two_q_errors.append(err) + if dur is not None: + two_q_durations.append(dur) + + # Estimated circuit duration + try: + estimated_duration = circ.estimate_duration(target=target) + except Exception: + estimated_duration = None + + # T1 / T2 based normalized time + t1_ratios = [] + t2_ratios = [] + + for q in sorted(used_physical_qubits): + try: + qp = backend.qubit_properties(q) + except Exception: + qp = None + + t1 = getattr(qp, "t1", None) if qp is not None else None + t2 = getattr(qp, "t2", None) if qp is not None else None + acc_time = qubit_gate_time.get(q, 0.0) + + if t1 is not None and t1 > 0: + t1_ratios.append(acc_time / t1) + if t2 is not None and t2 > 0: + t2_ratios.append(acc_time / t2) + + per_qubit_gate_err_values = list(qubit_gate_error_sum.values()) + per_qubit_gate_time_values = list(qubit_gate_time.values()) + + one_q_stats = safe_stats(one_q_errors) + two_q_stats = safe_stats(two_q_errors) + meas_stats = safe_stats(meas_errors) + comm_noise_stats = safe_stats(comm_noise_errors) + all_op_error_stats = safe_stats(op_error_records) + two_q_duration_stats = safe_stats(two_q_durations) + t1_ratio_stats = safe_stats(t1_ratios) + t2_ratio_stats = safe_stats(t2_ratios) + per_qubit_gate_err_stats = safe_stats(per_qubit_gate_err_values) + per_qubit_gate_time_stats = safe_stats(per_qubit_gate_time_values) + + return { + # Noise strength: 1Q + "one_q_error_sum": one_q_stats["sum"], + "one_q_error_mean": one_q_stats["mean"], + "one_q_error_max": one_q_stats["max"], + "one_q_error_std": one_q_stats["std"], + + # Noise strength: 2Q + "two_q_error_sum": two_q_stats["sum"], + "two_q_error_mean": two_q_stats["mean"], + "two_q_error_max": two_q_stats["max"], + "two_q_error_std": two_q_stats["std"], + + # Noise strength: Measurement + "meas_error_sum": meas_stats["sum"], + "meas_error_mean": meas_stats["mean"], + "meas_error_max": meas_stats["max"], + "meas_error_std": meas_stats["std"], + + # Noise strength: Communication (Kraus instruction) + "comm_noise_mean": comm_noise_stats["mean"], + "comm_noise_max": comm_noise_stats["max"], + "comm_noise_min": comm_noise_stats["min"], + "comm_noise_std": comm_noise_stats["std"], + + # All ops + "all_op_error_sum": all_op_error_stats["sum"], + + # Time / decoherence + "estimated_duration_sec": ( + estimated_duration if estimated_duration is not None else -1.0 + ), + "two_q_duration_sum": two_q_duration_stats["sum"], + "time_over_t1_mean": t1_ratio_stats["mean"], + "time_over_t1_max": t1_ratio_stats["max"], + "time_over_t2_mean": t2_ratio_stats["mean"], + "time_over_t2_max": t2_ratio_stats["max"], + + # Load distribution + "per_qubit_gate_error_max": per_qubit_gate_err_stats["max"], + "per_qubit_gate_time_max": per_qubit_gate_time_stats["max"], + } + + +def build_feature_groups( + circuit_info: dict, + dag_info: dict, + noise_time_info: dict, +) -> dict: + # A. 基础结构 + group_a_basic_structure = { + "num_qubits": circuit_info["num_qubits"], + "depth": circuit_info["depth"], + "one_qubit_gate_count": circuit_info["one_qubit_gate_count"], + "two_qubit_gate_count": circuit_info["two_qubit_gate_count"], + "measure_count": circuit_info["measure_count"], + "comm_count": circuit_info["comm_count"], + } + + # B. 关键路径 + group_b_critical_path = { + "critical_path_1q_count": dag_info["critical_path_1q_count"], + "critical_path_2q_count": dag_info["critical_path_2q_count"], + "critical_path_ratio": dag_info["critical_path_ratio"], + } + + # C. 噪声强度 + group_c_noise_strength = { + "one_q_error_sum": noise_time_info["one_q_error_sum"], + "one_q_error_mean": noise_time_info["one_q_error_mean"], + "one_q_error_max": noise_time_info["one_q_error_max"], + "one_q_error_std": noise_time_info["one_q_error_std"], + + "two_q_error_sum": noise_time_info["two_q_error_sum"], + "two_q_error_mean": noise_time_info["two_q_error_mean"], + "two_q_error_max": noise_time_info["two_q_error_max"], + "two_q_error_std": noise_time_info["two_q_error_std"], + + "meas_error_sum": noise_time_info["meas_error_sum"], + "meas_error_mean": noise_time_info["meas_error_mean"], + "meas_error_max": noise_time_info["meas_error_max"], + "meas_error_std": noise_time_info["meas_error_std"], + + "comm_noise_mean": noise_time_info["comm_noise_mean"], + "comm_noise_max": noise_time_info["comm_noise_max"], + "comm_noise_min": noise_time_info["comm_noise_min"], + "comm_noise_std": noise_time_info["comm_noise_std"], + + "all_op_error_sum": noise_time_info["all_op_error_sum"], + } + + # D. 时间 / 退相干 + group_d_time_decoherence = { + "estimated_duration_sec": noise_time_info["estimated_duration_sec"], + "two_q_duration_sum": noise_time_info["two_q_duration_sum"], + "time_over_t1_mean": noise_time_info["time_over_t1_mean"], + "time_over_t1_max": noise_time_info["time_over_t1_max"], + "time_over_t2_mean": noise_time_info["time_over_t2_mean"], + "time_over_t2_max": noise_time_info["time_over_t2_max"], + } + + # E. 负载分布 + group_e_load_distribution = { + "qubit_activity_mean": dag_info["qubit_activity_mean"], + "qubit_activity_max": dag_info["qubit_activity_max"], + "per_qubit_gate_error_max": noise_time_info["per_qubit_gate_error_max"], + "per_qubit_gate_time_max": noise_time_info["per_qubit_gate_time_max"], + } + + return { + "group_a_basic_structure": group_a_basic_structure, + "group_b_critical_path": group_b_critical_path, + "group_c_noise_strength": group_c_noise_strength, + "group_d_time_decoherence": group_d_time_decoherence, + "group_e_load_distribution": group_e_load_distribution, + } + + +def build_flattened_feature_vector(feature_groups: dict) -> dict: + flat = {} + for group_name in [ + "group_a_basic_structure", + "group_b_critical_path", + "group_c_noise_strength", + "group_d_time_decoherence", + "group_e_load_distribution", + ]: + flat.update(feature_groups[group_name]) + return flat + + +def pretty_print_dict(title: str, d: dict) -> None: + print(f"\n=== {title} ===") + for k, v in d.items(): + print(f"{k}: {v}") + + +def main() -> None: + # 1) Build original circuit + qc = build_common_ansatz(num_qubits=12, layers=3) + + # 2) Backend + backend = FakeKolkataV2() + + # 3) Transpile + tqc = transpile( + qc, + backend=backend, + optimization_level=3, + seed_transpiler=7, + ) + + # 4) Convert to DAG + dag = circuit_to_dag(tqc) + + # 5) Extract metrics + circuit_info = circuit_metrics(tqc) + dag_info = dag_metrics(dag) + noise_time_info = noise_time_metrics(tqc, backend) + + # 6) Build selected feature groups + feature_groups = build_feature_groups( + circuit_info, + dag_info, + noise_time_info, + ) + + feature_vector = build_flattened_feature_vector(feature_groups) + + pretty_print_dict("A. Basic Structure", feature_groups["group_a_basic_structure"]) + pretty_print_dict("B. Critical Path", feature_groups["group_b_critical_path"]) + pretty_print_dict("C. Noise Strength", feature_groups["group_c_noise_strength"]) + pretty_print_dict("D. Time / Decoherence", feature_groups["group_d_time_decoherence"]) + pretty_print_dict("E. Load Distribution", feature_groups["group_e_load_distribution"]) + pretty_print_dict("Flattened Feature Vector (for ML)", feature_vector) + + +if __name__ == "__main__": + main() diff --git a/src/dqc_simulator.egg-info/PKG-INFO b/src/dqc_simulator.egg-info/PKG-INFO index c9e6199..8e8e421 100644 --- a/src/dqc_simulator.egg-info/PKG-INFO +++ b/src/dqc_simulator.egg-info/PKG-INFO @@ -1,5 +1,5 @@ -Metadata-Version: 2.4 -Name: dqc_simulator -Version: 0.0.0 -License-File: LICENSE -Dynamic: license-file +Metadata-Version: 2.4 +Name: dqc_simulator +Version: 0.0.0 +License-File: LICENSE +Dynamic: license-file diff --git a/src/dqc_simulator.egg-info/SOURCES.txt b/src/dqc_simulator.egg-info/SOURCES.txt index 5a9b0ef..fa8a56c 100644 --- a/src/dqc_simulator.egg-info/SOURCES.txt +++ b/src/dqc_simulator.egg-info/SOURCES.txt @@ -1,10 +1,10 @@ -LICENSE -README.md -pyproject.toml -src/dqc_simulator/__init__.py -src/dqc_simulator/backend.py -src/dqc_simulator/dqc_simulator.py -src/dqc_simulator.egg-info/PKG-INFO -src/dqc_simulator.egg-info/SOURCES.txt -src/dqc_simulator.egg-info/dependency_links.txt +LICENSE +README.md +pyproject.toml +src/dqc_simulator/__init__.py +src/dqc_simulator/backend.py +src/dqc_simulator/dqc_simulator.py +src/dqc_simulator.egg-info/PKG-INFO +src/dqc_simulator.egg-info/SOURCES.txt +src/dqc_simulator.egg-info/dependency_links.txt src/dqc_simulator.egg-info/top_level.txt \ No newline at end of file diff --git a/src/dqc_simulator.egg-info/dependency_links.txt b/src/dqc_simulator.egg-info/dependency_links.txt index 8b13789..d3f5a12 100644 --- a/src/dqc_simulator.egg-info/dependency_links.txt +++ b/src/dqc_simulator.egg-info/dependency_links.txt @@ -1 +1 @@ - + diff --git a/src/dqc_simulator.egg-info/top_level.txt b/src/dqc_simulator.egg-info/top_level.txt index e4b05f0..4c2313e 100644 --- a/src/dqc_simulator.egg-info/top_level.txt +++ b/src/dqc_simulator.egg-info/top_level.txt @@ -1 +1 @@ -dqc_simulator +dqc_simulator diff --git a/src/dqc_simulator/__init__.py b/src/dqc_simulator/__init__.py index e7eec99..a12f1c8 100644 --- a/src/dqc_simulator/__init__.py +++ b/src/dqc_simulator/__init__.py @@ -1,5 +1,18 @@ # __init__.py from .dqc_simulator import DQCCircuit, DQCQPU, QPUManager from .backend import IonQ +from .features import ( + extract_feature_bundle, + extract_feature_groups, + extract_feature_vector, +) -__all__ = ["DQCCircuit", "DQCQPU", "QPUManager", "IonQ"] +__all__ = [ + "DQCCircuit", + "DQCQPU", + "QPUManager", + "IonQ", + "extract_feature_bundle", + "extract_feature_groups", + "extract_feature_vector", +] diff --git a/src/dqc_simulator/__pycache__/__init__.cpython-313.pyc b/src/dqc_simulator/__pycache__/__init__.cpython-313.pyc index 30a1cf5..1ba505f 100644 Binary files a/src/dqc_simulator/__pycache__/__init__.cpython-313.pyc and b/src/dqc_simulator/__pycache__/__init__.cpython-313.pyc differ diff --git a/src/dqc_simulator/__pycache__/backend.cpython-313.pyc b/src/dqc_simulator/__pycache__/backend.cpython-313.pyc index bfb26f9..29dd233 100644 Binary files a/src/dqc_simulator/__pycache__/backend.cpython-313.pyc and b/src/dqc_simulator/__pycache__/backend.cpython-313.pyc differ diff --git a/src/dqc_simulator/__pycache__/dqc_simulator.cpython-313.pyc b/src/dqc_simulator/__pycache__/dqc_simulator.cpython-313.pyc index 74b3ab2..1e5dab4 100644 Binary files a/src/dqc_simulator/__pycache__/dqc_simulator.cpython-313.pyc and b/src/dqc_simulator/__pycache__/dqc_simulator.cpython-313.pyc differ diff --git a/src/dqc_simulator/__pycache__/features.cpython-313.pyc b/src/dqc_simulator/__pycache__/features.cpython-313.pyc new file mode 100644 index 0000000..c06d228 Binary files /dev/null and b/src/dqc_simulator/__pycache__/features.cpython-313.pyc differ diff --git a/src/dqc_simulator/backend.py b/src/dqc_simulator/backend.py index c390b3c..023b13b 100644 --- a/src/dqc_simulator/backend.py +++ b/src/dqc_simulator/backend.py @@ -1,161 +1,161 @@ -import numpy as np -from qiskit.providers import BackendV2, Options -from qiskit.transpiler import Target, InstructionProperties -from qiskit.circuit.library import XGate, SXGate, RZGate, CZGate -from qiskit.circuit import Measure, Delay, Parameter, Reset -from qiskit import QuantumCircuit, transpile -import matplotlib.pyplot as plt -from qiskit_aer.noise import NoiseModel -from qiskit_aer import AerSimulator -from qiskit.providers.backend import QubitProperties - -class IonQ(BackendV2): - """Fake Aria-2 backend with full connectivity.""" - - def __init__(self): - """Instantiate a new fake Aria-2 backend. - - This backend simulates a 25-qubit fully connected quantum processor - based on the qpu.aria-2 specifications. - """ - super().__init__(name="IonQ backend") - - # Backend properties from the provided spec - self._num_qubits = 25 - self.backend_name = "qpu.IonQ" - - # Fidelity parameters - self.fidelity_1q_mean = 0.9997 - self.fidelity_2q_mean = 0.9699 - self.fidelity_spam_mean = 0.9974 - - # Timing parameters (in seconds) - self.t_readout = 0.00005 # 50 μs - self.t_reset = 0.000015 # 15 μs - self.t_1q = 0.000135 # 135 ns - self.t_2q = 0.0006 # 600 ns - self.t1_time = 10 # 10 s - self.t2_time = 1.5 # 1.5 s - - # Set up random number generator with seed for reproducibility - rng = np.random.default_rng(seed=12345678942) - - # Create qubit properties for thermal relaxation errors - qubit_properties = [] - for i in range(self._num_qubits): - # Add some variation to T1 and T2 times - t1_var = rng.uniform(-0.5, 0.5) # ±0.5s variation - t2_var = rng.uniform(-0.1, 0.1) # ±0.1s variation - - qubit_properties.append( - QubitProperties( - t1=self.t1_time + t1_var, - t2=self.t2_time + t2_var, - frequency=rng.uniform(4.5e9, 5.5e9) # 4.5-5.5 GHz - ) - ) - - # Create target with qubit properties - self._target = Target( - "IonQ backend", - num_qubits=self._num_qubits, - qubit_properties=qubit_properties - ) - - # Single qubit gate properties - rz_props = {} - x_props = {} - sx_props = {} - measure_props = {} - delay_props = {} - reset_props = {} - - # Add 1q gates for all qubits - for i in range(self._num_qubits): - qarg = (i,) - - # RZ is virtual (no error, no duration) - rz_props[qarg] = InstructionProperties(error=0.0, duration=0.0) - - # X gate - error derived from 1q fidelity - error_1q = 1 - self.fidelity_1q_mean - x_props[qarg] = InstructionProperties( - error=error_1q + rng.uniform(-1e-5, 1e-5), - duration=self.t_1q, - ) - - # SX gate - similar error to X - sx_props[qarg] = InstructionProperties( - error=error_1q + rng.uniform(-1e-5, 1e-5), - duration=self.t_1q, - ) - - # Measurement - error from SPAM fidelity - error_spam = 1 - self.fidelity_spam_mean - measure_props[qarg] = InstructionProperties( - error=error_spam + rng.uniform(-1e-4, 1e-4), - duration=self.t_readout, - ) - - # Reset - reset_props[qarg] = InstructionProperties( - error=error_spam + rng.uniform(-1e-4, 1e-4), - duration=self.t_reset, - ) - - # Delay (no error) - delay_props[qarg] = None - - # Add single qubit instructions to target - self._target.add_instruction(XGate(), x_props) - self._target.add_instruction(SXGate(), sx_props) - self._target.add_instruction(RZGate(Parameter("theta")), rz_props) - self._target.add_instruction(Measure(), measure_props) - self._target.add_instruction(Reset(), reset_props) - self._target.add_instruction(Delay(Parameter("t")), delay_props) - - # Two qubit gate properties - Full connectivity with CZ - cz_props = {} - error_2q = 1 - self.fidelity_2q_mean - - # Create all possible qubit pairs (full connectivity) - for i in range(self._num_qubits): - for j in range(i + 1, self._num_qubits): - # Add both directions for bidirectional connectivity - edge_forward = (i, j) - edge_backward = (j, i) - - # Add some variation to 2q gate errors - error_var = rng.uniform(-5e-3, 5e-3) - - cz_props[edge_forward] = InstructionProperties( - error=error_2q + error_var, - duration=self.t_2q, - ) - cz_props[edge_backward] = InstructionProperties( - error=error_2q + error_var, - duration=self.t_2q, - ) - - self._target.add_instruction(CZGate(), cz_props) - - @property - def target(self): - return self._target - - @property - def max_circuits(self): - return None - - @property - def num_qubits(self): - return self._num_qubits - - @classmethod - def _default_options(cls): - return Options(shots=1024) - - def run(self, circuit, **kwargs): - raise NotImplementedError( - "This backend does not contain a run method" - ) +import numpy as np +from qiskit.providers import BackendV2, Options +from qiskit.transpiler import Target, InstructionProperties +from qiskit.circuit.library import XGate, SXGate, RZGate, CZGate +from qiskit.circuit import Measure, Delay, Parameter, Reset +from qiskit import QuantumCircuit, transpile +import matplotlib.pyplot as plt +from qiskit_aer.noise import NoiseModel +from qiskit_aer import AerSimulator +from qiskit.providers.backend import QubitProperties + +class IonQ(BackendV2): + """Fake Aria-2 backend with full connectivity.""" + + def __init__(self): + """Instantiate a new fake Aria-2 backend. + + This backend simulates a 25-qubit fully connected quantum processor + based on the qpu.aria-2 specifications. + """ + super().__init__(name="IonQ backend") + + # Backend properties from the provided spec + self._num_qubits = 25 + self.backend_name = "qpu.IonQ" + + # Fidelity parameters + self.fidelity_1q_mean = 0.9997 + self.fidelity_2q_mean = 0.9699 + self.fidelity_spam_mean = 0.9974 + + # Timing parameters (in seconds) + self.t_readout = 0.00005 # 50 μs + self.t_reset = 0.000015 # 15 μs + self.t_1q = 0.000135 # 135 ns + self.t_2q = 0.0006 # 600 ns + self.t1_time = 10 # 10 s + self.t2_time = 1.5 # 1.5 s + + # Set up random number generator with seed for reproducibility + rng = np.random.default_rng(seed=12345678942) + + # Create qubit properties for thermal relaxation errors + qubit_properties = [] + for i in range(self._num_qubits): + # Add some variation to T1 and T2 times + t1_var = rng.uniform(-0.5, 0.5) # ±0.5s variation + t2_var = rng.uniform(-0.1, 0.1) # ±0.1s variation + + qubit_properties.append( + QubitProperties( + t1=self.t1_time + t1_var, + t2=self.t2_time + t2_var, + frequency=rng.uniform(4.5e9, 5.5e9) # 4.5-5.5 GHz + ) + ) + + # Create target with qubit properties + self._target = Target( + "IonQ backend", + num_qubits=self._num_qubits, + qubit_properties=qubit_properties + ) + + # Single qubit gate properties + rz_props = {} + x_props = {} + sx_props = {} + measure_props = {} + delay_props = {} + reset_props = {} + + # Add 1q gates for all qubits + for i in range(self._num_qubits): + qarg = (i,) + + # RZ is virtual (no error, no duration) + rz_props[qarg] = InstructionProperties(error=0.0, duration=0.0) + + # X gate - error derived from 1q fidelity + error_1q = 1 - self.fidelity_1q_mean + x_props[qarg] = InstructionProperties( + error=error_1q + rng.uniform(-1e-5, 1e-5), + duration=self.t_1q, + ) + + # SX gate - similar error to X + sx_props[qarg] = InstructionProperties( + error=error_1q + rng.uniform(-1e-5, 1e-5), + duration=self.t_1q, + ) + + # Measurement - error from SPAM fidelity + error_spam = 1 - self.fidelity_spam_mean + measure_props[qarg] = InstructionProperties( + error=error_spam + rng.uniform(-1e-4, 1e-4), + duration=self.t_readout, + ) + + # Reset + reset_props[qarg] = InstructionProperties( + error=error_spam + rng.uniform(-1e-4, 1e-4), + duration=self.t_reset, + ) + + # Delay (no error) + delay_props[qarg] = None + + # Add single qubit instructions to target + self._target.add_instruction(XGate(), x_props) + self._target.add_instruction(SXGate(), sx_props) + self._target.add_instruction(RZGate(Parameter("theta")), rz_props) + self._target.add_instruction(Measure(), measure_props) + self._target.add_instruction(Reset(), reset_props) + self._target.add_instruction(Delay(Parameter("t")), delay_props) + + # Two qubit gate properties - Full connectivity with CZ + cz_props = {} + error_2q = 1 - self.fidelity_2q_mean + + # Create all possible qubit pairs (full connectivity) + for i in range(self._num_qubits): + for j in range(i + 1, self._num_qubits): + # Add both directions for bidirectional connectivity + edge_forward = (i, j) + edge_backward = (j, i) + + # Add some variation to 2q gate errors + error_var = rng.uniform(-5e-3, 5e-3) + + cz_props[edge_forward] = InstructionProperties( + error=error_2q + error_var, + duration=self.t_2q, + ) + cz_props[edge_backward] = InstructionProperties( + error=error_2q + error_var, + duration=self.t_2q, + ) + + self._target.add_instruction(CZGate(), cz_props) + + @property + def target(self): + return self._target + + @property + def max_circuits(self): + return None + + @property + def num_qubits(self): + return self._num_qubits + + @classmethod + def _default_options(cls): + return Options(shots=1024) + + def run(self, circuit, **kwargs): + raise NotImplementedError( + "This backend does not contain a run method" + ) diff --git a/src/dqc_simulator/dqc_simulator.py b/src/dqc_simulator/dqc_simulator.py index f14c17a..95ad3ab 100644 --- a/src/dqc_simulator/dqc_simulator.py +++ b/src/dqc_simulator/dqc_simulator.py @@ -1,1505 +1,1577 @@ -from qiskit import QuantumCircuit, ClassicalRegister, transpile -from qiskit.circuit import Instruction, CircuitInstruction -from qiskit_aer import AerSimulator -from qiskit.circuit.library.standard_gates import HGate, XGate, ZGate, CXGate -from qiskit.circuit import Reset, Measure, ClassicalRegister -from qiskit.visualization import plot_histogram -from qiskit import QuantumRegister -from qiskit.quantum_info import Kraus -from qiskit.converters import circuit_to_dag -from qiskit_ibm_runtime.fake_provider import ( - FakeVigoV2, # 5 - FakeLagosV2, # 7 - FakeCasablancaV2, - FakeYorktownV2, - FakeManilaV2, - FakeNairobiV2, - FakeMumbaiV2, - FakeKolkataV2, - FakeGuadalupeV2, - FakeAlmadenV2, - FakeAthensV2, # 5 - FakeCambridgeV2 -) - -# Backend IonQ -from .backend import IonQ - -from qiskit_aer.noise import ( - NoiseModel, - depolarizing_error, pauli_error, - amplitude_damping_error, phase_amplitude_damping_error, - phase_damping_error -) - -import numpy as np -import copy -import matplotlib.pyplot as plt - -class RemoteGate(Instruction): - """ Remote Gate""" - def __init__(self, index: int, target: int): - super().__init__("R", 1, 0, []) - self.index = index - self.target = target - -class MX(Instruction): - """MX for Control Side""" - def __init__(self, index: int, target: int): - super().__init__("MX", 2, 0, []) - self.index = index - self.target = target - -class MZ(Instruction): - """MZ for Target Side""" - def __init__(self, index: int, target: int): - super().__init__("MZ", 2, 0, []) - self.index = index - self.target = target - -class AnsM(Instruction): - """AnsM Measurement""" - def __init__(self, mea: int): - super().__init__("ANS_M", 1, 0, []) - self.mea = mea - -class S_CX(Instruction): - """Custom CNOT Gate""" - def __init__(self, control: int, target: int, path): - super().__init__("S_CX", 2, 0, []) - self.control = control - self.target = target - self.path = path - -class MS(Instruction): - """Multi-qubit Swap Gate""" - def __init__(self, index: int, target: int): - super().__init__("MS", 2, 0, []) - self.index = index - self.target = target - -class IF_Z(Instruction): - def __init__(self, index: int, target: int): - super().__init__("IF_Z", 1, 0, []) - self.index = index - self.target = target - -class IF_X(Instruction): - """Conditional X Gate""" - def __init__(self, index: int, target: int): - super().__init__("IF_X", 1, 0, []) - self.index = index - self.target = target - -# Mapping backend names to their classes -FAKE_BACKENDS = { - "FakeVigoV2": FakeVigoV2, - "FakeLagosV2": FakeLagosV2, - "FakeCasablancaV2": FakeCasablancaV2, - "FakeYorktownV2": FakeYorktownV2, - "FakeManilaV2": FakeManilaV2, - "FakeNairobiV2": FakeNairobiV2, - "FakeMumbaiV2": FakeMumbaiV2, - "FakeKolkataV2": FakeKolkataV2, - "FakeGuadalupeV2": FakeGuadalupeV2, - "FakeAlmadenV2": FakeAlmadenV2, - "FakeAthensV2": FakeAthensV2, - "FakeCambridgeV2": FakeCambridgeV2, - "IonQ": IonQ -} - -class QPUManager: - def __init__(self): - """ - Initialize QPU manager to handle multiple QPUs and their connections. - - Attributes: - qpus: List of QPU instances - noise_instructions: Dict mapping QPU pairs to noise instructions - map: Adjacency list representing QPU network topology - size: Total number of QPUs in the manager - """ - # Store QPUs and noise instructions - self.qpus = [] - self.noise_instructions = {} - # Adjacency list: {qpu_id: [(neighbor_id, distance), ...]} - self.map = {} - self.size = 0 - - def add_qpu(self, qpu): - """Add a QPU to the manager""" - self.qpus.append(qpu) - qpu_id = qpu.qpu_id - self.size += 1 - # Initialize adjacency list entry if not exists - if qpu_id not in self.map: - self.map[qpu_id] = [] - - def get_qpu(self, qpu_id): - """Retrieve QPU instance by ID""" - for qpu in self.qpus: - if qpu.qpu_id == qpu_id: - return qpu - return None - - def add_coonnection(self, qpu_id1, qpu_id2, distance: float = 0): - """Add bidirectional connection between two QPUs""" - qpu1 = self.get_qpu(qpu_id1) - qpu2 = self.get_qpu(qpu_id2) - if qpu1 is None or qpu2 is None: - raise ValueError(f"QPU {qpu_id1} or {qpu_id2} not found.") - - qpu1.add_connection(qpu_id2, distance) - qpu2.add_connection(qpu_id1, distance) - - # Add to adjacency list if not already present - if not any(n == qpu_id2 for n, _ in self.map[qpu_id1]): - self.map[qpu_id1].append((qpu_id2, distance)) - if not any(n == qpu_id1 for n, _ in self.map[qpu_id2]): - self.map[qpu_id2].append((qpu_id1, distance)) - - # Store noise instructions for both directions - noise_instr = qpu1.get_noise_by_distance(distance) - self.noise_instructions[(qpu_id1, qpu_id2)] = noise_instr - self.noise_instructions[(qpu_id2, qpu_id1)] = noise_instr - - def get_noise_instruction(self, qpu_id1, qpu_id2): - """Get noise instruction for connection between two QPUs""" - return self.noise_instructions.get((qpu_id1, qpu_id2), None) - - def check_connection(self, qpu_id1, qpu_id2) -> int: - """Check if two QPUs are connected (returns 1 if connected, 0 otherwise)""" - if qpu_id1 not in self.map: - return 0 - return 1 if any(n == qpu_id2 for n, _ in self.map[qpu_id1]) else 0 - - -class DQCQPU: - def __init__(self, qpu_id: int, backend_name: str, connections=None, noise_type: str = None, **noise_kwargs): - """ - Initialize a QPU instance with automatic custom instruction registration. - - :param qpu_id: QPU identifier - :param backend_name: Name of the backend (e.g., "FakeLagosV2") - :param connections: Optional list of connections to other QPUs - :param noise_type: Type of noise model to apply - :param noise_kwargs: Additional noise parameters - """ - self.qpu_id = qpu_id - - # Validate backend name - if backend_name not in FAKE_BACKENDS: - raise ValueError( - f"Unknown backend name '{backend_name}'. " - f"Available options are: {list(FAKE_BACKENDS.keys())}" - ) - - # Initialize backend - backend_cls = FAKE_BACKENDS[backend_name] - backend = backend_cls() - - # Register custom instructions to target - target = backend.target - target.add_instruction(RemoteGate, name="R") - target.add_instruction(MX, name="MX") - target.add_instruction(MZ, name="MZ") - target.add_instruction(AnsM, name="ANS_M") - target.add_instruction(AnsM, name="IF_Z") - target.add_instruction(AnsM, name="IF_X") - target.add_instruction(S_CX, name="MS") - - self.backend = backend - self.target = backend.target - - # Initialize connections and noise configuration - self.connections = connections if connections else [] - - if noise_type is not None: - self.noise_config = {"type": noise_type, "params": noise_kwargs} - else: - self.noise_config = None - - def add_connection(self, other_qpu_id: int, distance: float = 1.0): - """Add connection to another QPU with distance-based noise""" - kraus = self.get_noise_by_distance(distance) - conn = { - "id": other_qpu_id, - "distance": distance, - "noise": kraus - } - self.connections.append(conn) - - def compile_x_gate(self): - """Compile X gate for this QPU's backend""" - qc = QuantumCircuit(1) - qc.x(0) - compiled = transpile(qc, self.backend) - return compiled.data - - def compile_z_gate(self): - """Compile Z gate for this QPU's backend""" - qc = QuantumCircuit(1) - qc.z(0) - compiled = transpile(qc, self.backend) - return compiled.data - - def exponential(self, L, alpha): - """Calculate exponential decay based on distance""" - return np.exp(-alpha * L) - - def get_noise_by_distance(self, distance=1): - """ - Generate composite noise (amplitude damping + depolarizing) Kraus instruction - based on transmission distance. - :param distance: Transmission distance (e.g., km or m) - :return: Qiskit Instruction containing Kraus operators - """ - - # Calculate amplitude damping parameter - if distance == 0: - gamma = 0 - else: - gamma = self.exponential(distance, alpha=0.02) - - # Calculate depolarizing parameter - p_depol = gamma - - gamma = 1 - gamma - p_depol = 1 - p_depol - - # Amplitude damping Kraus operators - K0 = np.array([[1, 0], [0, np.sqrt(1 - gamma)]], dtype=complex) - K1 = np.array([[0, np.sqrt(gamma)], [0, 0]], dtype=complex) - K_amp = [K0, K1] - - # Depolarizing Kraus operators - sqrt1mp = np.sqrt(1 - p_depol) - sqrt_p3 = np.sqrt(p_depol / 3) - I = np.eye(2, dtype=complex) - X = np.array([[0, 1], [1, 0]], dtype=complex) - Y = np.array([[0, -1j], [1j, 0]], dtype=complex) - Z = np.array([[1, 0], [0, -1]], dtype=complex) - K_depol = [sqrt1mp * I, sqrt_p3 * X, sqrt_p3 * Y, sqrt_p3 * Z] - - # Combine channels (amplitude → depolarizing) - K_combined = [D @ A for D in K_depol for A in K_amp] - - # Create Qiskit Kraus instruction - kraus_instr = Kraus(K_combined).to_instruction() - - return kraus_instr - - def __repr__(self): - return ( - f"" - ) - -# ------------------------------------------------------------- -class DQCCircuit(QuantumCircuit): - def __init__(self, *args, **kwargs): - # 用已有 QuantumCircuit 初始化 - if len(args) == 1 and isinstance(args[0], QuantumCircuit): - qc = args[0] - # 保留原寄存器结构 - super().__init__(*qc.qregs, *qc.cregs, - name=qc.name, - global_phase=qc.global_phase, - metadata=copy.deepcopy(qc.metadata) if qc.metadata else None) - - # 复制电路数据 - self.data = copy.deepcopy(qc.data) - else: - super().__init__(*args, **kwargs) - - self.step = [] # 各阶段存储的电路图 - self.step.append(copy.deepcopy(self)) - - self.sub_circuit = [] # 子电路 - self.sub_circuit_trans = [] # 编译后的子电路集合 - self.result_circuit = None # 最终结果电路 - - self.partition = [] # 划分 - self.qubit_group = [-1] * self.num_qubits # 比特分组 - self.Entanglement_swapping = [] # 纠缠信息 - self.swap_routes = [] # 交换路径 - - self.qubit_tele = [] # 比特对应的通信比特位置 - - self.qpus = [] - self.merged_qubits_map = {} # 合并后比特映射 - self.merged_qubits_map_reverse = {} # 反向映射 - - self.qpugroup = None - - self.Num_Entanglement_swapping = 0 # 纠缠交换次数统计 - self.Num_RemoteGate = 0 - - # 执行 - def Execution(self, config, qpugroup, comm_noise = False): - self.qpugroup = qpugroup - qpus = self.qpugroup.qpus - - self.split(config) - self.valid_trans() - self.check_swap_entanglement() - print("Entanglement Number:", self.Num_Entanglement_swapping) - self.rearrange_with_partition() - self.rewrite_cross_group_cnots() - self.physic_split() - - self.transpile_subcircuits(qpus) - # for sub_circ in self.sub_circuit_trans: - # sub_circ.draw("mpl", scale = 0.5, fold = 100) - # plt.show() - - # dag = circuit_to_dag(self.sub_circuit_trans[2]) - # dag.draw(output='mpl') - # plt.show() - - result_qc = self.merge_trans_circuits(comm_noise) - - return result_qc - - # Get the index of a qubit in the circuit - def get_index(self, q): - return int(self.find_bit(q).index) - - # Split the circuit based on the provided configuration - def split(self, config): - partition = [] - - if all(isinstance(x, int) for x in config): - # 按长度生成索引列表 - start = 0 - for s in config: - indices = list(range(start, start + s)) - partition.append(indices) - start += s - elif all(isinstance(x, (list, tuple)) for x in config): - # 按指定索引组合, 并排序 - for sub in config: - partition.append(sorted(sub)) - else: - raise ValueError("Input must be a list of ints or a list of lists of ints") - - self.partition = partition - - for gid, group in enumerate(partition): - self.Entanglement_swapping.append(0) - for q in group: - self.qubit_group[q] = gid - return partition - - # Preprocessing to validate and decompose cross-group multi-qubit gates to single-qubit gates and CX gates - # step[1] - def valid_trans(self): - """ - 遍历 old_circ 的指令,检查是否跨组多比特门。 - 如果跨组且不是 CX,则分解后 append 到 new_circ; - 如果同组或是 CX,则直接 append。 - - 参数: - new_circ: DQCCircuit,目标电路 - old_circ: DQCCircuit,源电路(step[0]) - """ - old_circ = self - - # === 1. 构建新电路(仅复制量子比特)=== - new_circ = DQCCircuit(old_circ.qubits) - new_circ.qubit_group = old_circ.qubit_group - new_circ.qubit_tele = old_circ.qubit_tele - print("Qubit Group:", new_circ.qubit_group) - - # === 2. 继承所有经典寄存器 === - # 复制通信寄存器(如果有) - comm_cregs = [creg for creg in getattr(old_circ, "cregs", []) if "Tele" in creg.name] - for comm_creg in comm_cregs: - new_circ.add_register(ClassicalRegister(len(comm_creg), comm_creg.name)) - - # 复制原始寄存器(如果有) - orig_cregs = [creg for creg in getattr(old_circ, "cregs", []) if "Tele" not in creg.name] - for orig_creg in orig_cregs: - new_circ.add_register(ClassicalRegister(len(orig_creg), orig_creg.name)) - - for instri in old_circ.data: - instr = instri.operation # 量子门或操作对象 - qargs = instri.qubits # 作用的量子比特列表 - cargs = instri.clbits # 作用的经典比特列表 - - # 先处理cx - if instr.name in ["cx"]: - new_circ.append(instr, qargs, cargs) - continue - - # 单比特门直接 append - if len(qargs) <= 1: - new_circ.append(instr, qargs, cargs) - continue - - # 多比特门,判断是否跨组 - groups = [new_circ.qubit_group[old_circ.get_index(q)] for q in qargs] - if len(set(groups)) > 1: - # 跨组且不是 CX,分解 - ci = CircuitInstruction(instr, qargs, cargs) - decomposed_instrs = self.decompose_and_get_data(ci) - for di in decomposed_instrs: - mapped_qubits = [new_circ.qubits[old_circ.get_index(q)] for q in di.qubits] - mapped_clbits = [new_circ.clbits[old_circ.get_index(c)] for c in di.clbits] - new_circ.append(di.operation, mapped_qubits, mapped_clbits) - else: - # 同组,多比特门直接 append - new_circ.append(instr, qargs, cargs) - - self.step.append(new_circ) - return new_circ - - # step[2] - def check_swap_entanglement(self): - """检查跨 QPU 纠缠,如果无直接连接则寻找可行的 SWAP 路径""" - swap_routes = [] - qpu_map = self.qpugroup.map - - old_circ = self.step[1] - - # === 1. 构建新电路(仅复制量子比特)=== - new_circ = DQCCircuit(old_circ.qubits) - - for creg in getattr(old_circ, "cregs", []): - new_circ.add_register(ClassicalRegister(len(creg), creg.name)) - - # 遍历电路中的每一个 CX 操作 - for instr in old_circ.data: - if instr.operation.name == "cx": - ctrl, tgt = instr.qubits - g_ctrl = self.qubit_group[self.get_index(ctrl)] - g_tgt = self.qubit_group[self.get_index(tgt)] - - # 如果控制和目标在不同 QPU 上 - if g_ctrl != g_tgt: - # 检查是否直接连接 - if not self.qpugroup.check_connection(g_ctrl, g_tgt): - # --- 没有直接连接:寻找最短路径 --- - path = self._find_shortest_path(qpu_map, g_ctrl, g_tgt) - if path: - swap_routes.append(path) - new_circ.append(S_CX(g_ctrl, g_tgt, path), instr.qubits, instr.clbits) - print(f"[Info] Found SWAP route {path} for CX({g_ctrl}, {g_tgt})") - self.Num_Entanglement_swapping += len(path) - 2 - else: - print(f"[Warning] No route found between QPU {g_ctrl} and {g_tgt}") - else: - # 直接连接,直接添加 CX 操作 - new_circ.append(instr.operation, instr.qubits, instr.clbits) - else: - # 同组,直接添加 CX 操作 - new_circ.append(instr.operation, instr.qubits, instr.clbits) - else: - new_circ.append(instr.operation, instr.qubits, instr.clbits) - - for path in swap_routes: - # 取除首尾外的中间节点 - for node in path[1:-1]: - self.Entanglement_swapping[node] = 1 - - self.swap_routes = swap_routes - self.step.append(new_circ) - - def _find_shortest_path(self, graph, start, end): - """使用 BFS 在邻接表图中寻找最短路径""" - from collections import deque - - visited = set() - queue = deque([[start]]) # 队列中每个元素是路径 list - - while queue: - path = queue.popleft() - node = path[-1] - if node == end: - return path # 找到路径 - - if node not in visited: - visited.add(node) - for neighbor, _ in graph.get(node, []): # 遍历邻居 - if neighbor not in visited: - queue.append(path + [neighbor]) - - return None # 无路径 - - # step[3] - def rearrange_with_partition(self): - """ - 根据 partition 重新排列 qubits, 每组加一个通信比特, - 并为 group 之间的通信分配 classical bits。 - """ - if not self.partition: - raise ValueError("请先设置 self.partition") - - partition = self.partition - num_groups = len(partition) - - # classical bit 数量 = g * (g-1) - comm_creg = ClassicalRegister(num_groups * (num_groups - 1), "Tele") - - # 每组 qubits 数量 = 原本 + 1(comm) - - # 先统计 partition 中普通 qubit 数量 - total_qubits = sum(len(group) for group in partition) - # 给每组添加一个通信 qubit - total_qubits += len(partition) - # 给每个需要交换纠缠的 QPU 添加一个通信 qubit - total_qubits += self.Entanglement_swapping.count(1) - - new_qreg = QuantumRegister(total_qubits, "q") - - new_circ = DQCCircuit(new_qreg, comm_creg) - - old_circ = self.step[2] - # === 4. 保留原始经典寄存器信息 === - # 如果原始电路 self 中有经典寄存器 - if hasattr( old_circ, "clbits") and old_circ.clbits: - # 尝试从 self.cregs 中找到原寄存器名 - if hasattr( old_circ, "cregs") and old_circ.cregs: - # 取第一个 ClassicalRegister 的名字 - orig_name = old_circ.cregs[0].name - else: - orig_name = "c" # 如果没有记录,则默认命名为 "c" - - # 使用原寄存器名创建新的 ClassicalRegister - orig_creg = ClassicalRegister(len(old_circ.clbits), orig_name) - new_circ.add_register(orig_creg) - - # 保存引用方便之后访问 - new_circ.orig_creg = orig_creg - else: - new_circ.orig_creg = None - - # ===== 建立旧 qubit -> 新 qubit 映射 ===== - old2new = {} - group_comm_qubits = [] - new_index = 0 - - for indices in partition: - # 字典映射量子寄存器 - # group 内 qubit - for qi in indices: - old2new[qi] = new_qreg[new_index] - new_index += 1 - # comm qubit - comm_q = new_qreg[new_index] - group_comm_qubits.append(comm_q) - new_index += 1 - if self.Entanglement_swapping[self.qubit_group[indices[0]]] == 1: - # 需要交换纠缠的 QPU 多加一个 comm qubit - extra_comm_q = new_qreg[new_index] - group_comm_qubits.append(extra_comm_q) - new_index += 1 - - # ===== 遍历原电路, 重映射到新电路 ===== - for instr in old_circ.data: - qubit_indices = [old_circ.get_index(q) for q in instr.qubits] - - if all(qi in old2new for qi in qubit_indices): - new_qargs = [old2new[qi] for qi in qubit_indices] - new_circ.append(instr.operation, new_qargs, instr.clbits) - - # ==== 给 new_circ.qubit_group 填值 ===== - new_circ.qubit_group = [] - for gid, group in enumerate(partition): - base_count = len(group) + 1 # 普通 + 通信 - if self.Entanglement_swapping[gid] == 1: - base_count += 1 # 额外加一个 SWAP qubit - new_circ.qubit_group.extend([gid] * base_count) - - # ===== 给 new_circ.qubit_tele 填值 ===== - qubit_tele = [-1] * len(new_qreg) - - idx = 0 - for gid, group in enumerate(partition): - # 每个分区的普通 + 通信 qubit 数量 - comm_q = group_comm_qubits[idx] - comm_idx = new_circ.get_index(comm_q) - - # 普通 qubit 指向该组通信 qubit - for qi in group: - qubit_tele[new_circ.get_index(old2new[qi])] = comm_idx - - # 通信 qubit 自己设为 -1 - qubit_tele[comm_idx] = -1 - idx += 1 - - # 若该组需要 entanglement swapping,则还要处理多出来的通信 qubit - if self.Entanglement_swapping[gid] == 1: - swap_comm_q = group_comm_qubits[idx] - swap_comm_idx = new_circ.get_index(swap_comm_q) - qubit_tele[swap_comm_idx] = -1 # 交换通信 qubit 也设为 -1 - idx += 1 - - # 赋值 - new_circ.qubit_tele = qubit_tele - - self.step.append(new_circ) - return new_circ - - # Rewrite cross-group CNOTs into RemoteGate, Measurement, If_X, If_Z - # step[4] - def rewrite_cross_group_cnots(self): - - old_circ = self.step[3] - - # === 1. 构建新电路(仅复制量子比特)=== - new_circ = DQCCircuit(old_circ.qubits) - new_circ.qubit_group = old_circ.qubit_group - new_circ.qubit_tele = old_circ.qubit_tele - group_tele = {gid: self.step[3].qubit_tele[self.step[3].qubit_group.index(gid)] for gid in set(self.step[3].qubit_group)} - - # === 2. 继承所有经典寄存器 === - # 复制通信寄存器(如果有) - comm_cregs = [creg for creg in getattr(old_circ, "cregs", []) if "Tele" in creg.name] - for comm_creg in comm_cregs: - new_circ.add_register(ClassicalRegister(len(comm_creg), comm_creg.name)) - - # 复制原始寄存器(如果有) - orig_cregs = [creg for creg in getattr(old_circ, "cregs", []) if "Tele" not in creg.name] - for orig_creg in orig_cregs: - new_circ.add_register(ClassicalRegister(len(orig_creg), orig_creg.name)) - - - idx_counter = 0 # 全局 index 计数器 - - # === 4. 改写电路 === - for inst_obj in old_circ.data: - instr = inst_obj.operation - qargs = inst_obj.qubits - cargs = inst_obj.clbits - if instr.name == "cx": - ctrl, tgt = qargs - g_ctrl = new_circ.qubit_group[old_circ.get_index(ctrl)] - g_tgt = new_circ.qubit_group[old_circ.get_index(tgt)] - - if g_ctrl != g_tgt: - # ====== 跨组 CNOT,改写 ====== - ctrl_index = old_circ.get_index(ctrl) - tgt_index = old_circ.get_index(tgt) - ctrl_comm_index = old_circ.qubit_tele[ctrl_index] - tgt_comm_index = old_circ.qubit_tele[tgt_index] - - ctrl_comm = new_circ.qubits[ctrl_comm_index] - tgt_comm = new_circ.qubits[tgt_comm_index] - ctrl_q = new_circ.qubits[ctrl_index] - tgt_q = new_circ.qubits[tgt_index] - - # Reset - new_circ.append(Reset(), [ctrl_comm]) - new_circ.append(Reset(), [tgt_comm]) - - # RemoteGate - new_circ.append(RemoteGate(idx_counter, g_tgt), [ctrl_comm]) - new_circ.append(RemoteGate(idx_counter, g_ctrl), [tgt_comm]) - idx_counter += 1 - - # CNOT: ctrl->ctrl_comm, tgt_comm->tgt - new_circ.append(CXGate(), [ctrl_q, ctrl_comm]) - new_circ.append(CXGate(), [tgt_comm, tgt_q]) - - # H on tgt_comm - new_circ.append(HGate(), [tgt_comm]) - - # Measurement + If_Z - # Measurement + If_X - mz_inst = MZ(index=idx_counter, target=g_tgt) - mx_inst = MX(index=idx_counter, target=g_ctrl) - new_circ.append(mz_inst, [ctrl_q, ctrl_comm]) - new_circ.append(mx_inst, [tgt_comm, tgt_q]) - idx_counter += 1 - - self.Num_RemoteGate += 1 - else: - # ====== 同组 CNOT,保持原样 ====== - new_circ.append(instr, qargs, cargs) - elif instr.name == "measure": - # ====== 测量指令替换为 AnsM ====== - qarg = qargs[0] - carg = cargs[0] if cargs else None - - # 获取经典比特在其寄存器中的索引 - if carg is not None: - mea_index = old_circ.find_bit(carg).index - else: - mea_index = -1 # 没有关联经典比特 - - # 创建 AnsM 占位指令 - ansm_gate = AnsM(mea_index) - - # 添加到新电路 - new_circ.append(ansm_gate, [qarg]) - elif instr.name == "S_CX": - path = instr.path - ctrl, tgt = qargs - g_ctrl = new_circ.qubit_group[old_circ.get_index(ctrl)] - g_tgt = new_circ.qubit_group[old_circ.get_index(tgt)] - - ctrl_index = old_circ.get_index(ctrl) - tgt_index = old_circ.get_index(tgt) - ctrl_comm_index = old_circ.qubit_tele[ctrl_index] - tgt_comm_index = old_circ.qubit_tele[tgt_index] - - ctrl_q = new_circ.qubits[ctrl_index] - tgt_q = new_circ.qubits[tgt_index] - ctrl_comm = new_circ.qubits[ctrl_comm_index] - tgt_comm = new_circ.qubits[tgt_comm_index] - - new_circ.append(Reset(), [ctrl_comm_index]) - - for i in range(1, len(path) - 1): - mid_g = path[i] - tgt_g = path[i + 1] - - mid_comm_index_1 = group_tele[mid_g] - mid_comm_index_2 = mid_comm_index_1 + 1 - tgt_comm_index = group_tele[tgt_g] - - mid_comm_1 = new_circ.qubits[mid_comm_index_1] - mid_comm_2 = new_circ.qubits[mid_comm_index_2] - tgt_comm = new_circ.qubits[tgt_comm_index] - - if i == 1: - # Reset - new_circ.append(Reset(), [mid_comm_1]) - new_circ.append(Reset(), [mid_comm_2]) - new_circ.append(Reset(), [tgt_comm]) - - # RemoteGate - new_circ.append(RemoteGate(idx_counter, mid_g), [ctrl_comm]) - new_circ.append(RemoteGate(idx_counter, g_ctrl), [mid_comm_1]) - idx_counter += 1 - - new_circ.append(RemoteGate(idx_counter, tgt_g), [mid_comm_2]) - new_circ.append(RemoteGate(idx_counter, mid_g), [tgt_comm]) - idx_counter += 1 - - new_circ.append(CXGate(), [mid_comm_1, mid_comm_2]) - new_circ.append(HGate(), [mid_comm_1]) - - new_circ.append(IF_Z(index=idx_counter, target=mid_g), [ctrl_comm_index]) - new_circ.append(MS(index=idx_counter, target=tgt_g), [mid_comm_1, mid_comm_2]) - new_circ.append(IF_X(index=idx_counter, target=g_ctrl), [tgt_comm_index]) - idx_counter += 1 - else: - new_circ.append(Reset(), [mid_comm_2]) - new_circ.append(Reset(), [tgt_comm]) - - # RemoteGate - new_circ.append(RemoteGate(idx_counter, tgt_g), [mid_comm_2]) - new_circ.append(RemoteGate(idx_counter, mid_g), [tgt_comm]) - idx_counter += 1 - - new_circ.append(CXGate(), [mid_comm_1, mid_comm_2]) - new_circ.append(HGate(), [mid_comm_1]) - - new_circ.append(IF_Z(index=idx_counter, target=mid_g), [ctrl_comm_index]) - new_circ.append(MS(index=idx_counter, target=tgt_g), [mid_comm_1, mid_comm_2]) - new_circ.append(IF_X(index=idx_counter, target=g_ctrl), [tgt_comm_index]) - idx_counter += 1 - - - # CNOT: ctrl->ctrl_comm, tgt_comm->tgt - new_circ.append(CXGate(), [ctrl_q, ctrl_comm]) - new_circ.append(CXGate(), [tgt_comm, tgt_q]) - - # H on tgt_comm - new_circ.append(HGate(), [tgt_comm]) - - # Measurement + If_Z - # Measurement + If_X - mz_inst = MZ(index=idx_counter, target=g_tgt) - mx_inst = MX(index=idx_counter, target=g_ctrl) - new_circ.append(mz_inst, [ctrl_q, ctrl_comm]) - new_circ.append(mx_inst, [tgt_comm, tgt_q]) - idx_counter += 1 - else: - # 不是 CNOT 和 Mesurement,保持原样 - new_circ.append(instr, qargs, cargs) - - self.step.append(new_circ) - return new_circ - - # Physically split the circuit into sub-circuits based on partition - def physic_split(self): - config = [len(group) + 1 for group in self.partition] - for gid, flag in enumerate(self.Entanglement_swapping): - if flag == 1: - config[gid] += 1 - - temp_circuit = self.step[4] - sub_circuits = [] - - if all(isinstance(x, int) for x in config): - # 按数量顺序分割, 例如 [2,2,3] - start = 0 - for s in config: - sub = DQCCircuit(s) - sub.qubit_group = temp_circuit.qubit_group[start:start+s] - - # 给子电路加 classical bits - num_clbits = (len(config) - 1) * 2 - if num_clbits > 0: - creg = ClassicalRegister(num_clbits, "c") - sub.add_register(creg) - - # 复制原电路对应 qubit 的操作 - for instr in temp_circuit.data: - qubit_indices = [temp_circuit.get_index(q) for q in instr.qubits] - # 只保留属于子电路的 qubit - indices_in_sub = [i for i, qi in enumerate(qubit_indices) if start <= qi < start+s] - if indices_in_sub: - new_qargs = [sub.qubits[qi - start] for i, qi in enumerate(qubit_indices) if i in indices_in_sub] - new_cargs = [] - # print(f"Appending instruction {instr.operation.name} to sub-circuit with qubits {new_qargs}") - sub.append(instr.operation, new_qargs, new_cargs) - - sub_circuits.append(sub) - start += s - - elif all(isinstance(x, (list, tuple)) for x in config): - # 按指定索引组合, 例如 [[1,3],[0,2,4]] - for indices in config: - sub = DQCCircuit(len(indices)) - sub.qubit_type = [self.qubit_type[i] for i in indices] - - for instr in self.data: - qubit_indices = [self.get_index(q) for q in instr.qubits] - # 只保留在 indices 中的 qubit - indices_in_sub = [i for i, qi in enumerate(qubit_indices) if qi in indices] - if indices_in_sub: - new_qargs = [sub.qubits[indices.index(qi)] for i, qi in enumerate(qubit_indices) if i in indices_in_sub] - new_cargs = [] - sub.append(instr.operation, new_qargs, new_cargs) - - sub_circuits.append(sub) - - else: - raise ValueError("Config must be a list of ints or a list of lists of ints") - - self.sub_circuit = sub_circuits - return sub_circuits - - # Protect custom instructions with barriers - def protect_custom_instructions(self, subcirc): - """ - 为 RemoteGate、Measurement、If_X、If_Z、AnsM 添加 barrier 保护。 - """ - new_circ = DQCCircuit(*subcirc.qregs, *subcirc.cregs) - - for inst_obj in subcirc.data: - inst = inst_obj.operation - qargs = inst_obj.qubits - cargs = inst_obj.clbits - - if isinstance(inst, (MX, MZ, AnsM, IF_Z, IF_X, MS, S_CX)): # 在同样的 qubits 上加 barrier - new_circ.barrier(*qargs) - new_circ.append(inst, qargs, cargs) - new_circ.barrier(*qargs) - # print(f"[Info] Protecting instruction: {inst.name} on qubits {[q for q in qargs]}") - elif isinstance(inst, (RemoteGate)): - new_circ.barrier() - new_circ.append(inst, qargs, cargs) - new_circ.barrier() - else: - new_circ.append(inst, qargs, cargs) - - return new_circ - - # Restore custom instructions by removing barriers - def restore_custom_instructions(self, subcirc): - """ - 还原 RemoteGate、Measurement、If_X、If_Z、AnsM 的 barrier 保护。 - """ - new_circ = DQCCircuit(*subcirc.qregs, *subcirc.cregs) - - for inst_obj in subcirc.data: - inst = inst_obj.operation - qargs = inst_obj.qubits - cargs = inst_obj.clbits - - if inst.name != "barrier": - new_circ.append(inst, qargs, cargs) - # if isinstance(inst, (RemoteGate, MX, MZ, AnsM)): - # print(f"[Info] Restoring instruction: {inst.name} on qubits {[q for q in qargs]}") - return new_circ - - def transpile_subcircuits(self, qpus, layout_out=None): - """ - Transpile each sub-circuit using the corresponding QPU backend target, - and optional layout mapping per subcircuit. - - :param qpus: list of QPU instances (each having .backend and .target) - :param layout_out: list of layouts; each layout is a list of physical qubit indices - corresponding to the sub-circuit's logical qubits - e.g. [[0,1,2,3], [5,6,7,8]] - """ - # ---- 按 qpu_id 排序 ---- - self.qpus = sorted(qpus, key=lambda x: x.qpu_id) - - if not self.sub_circuit: - raise ValueError("No sub-circuits found. Please populate self.sub_circuit first.") - if len(qpus) != len(self.sub_circuit): - raise ValueError( - f"Number of QPUs ({len(qpus)}) does not match number of sub-circuits ({len(self.sub_circuit)})." - ) - - # ---- 检查 layout_out ---- - if layout_out is not None: - if len(layout_out) != len(self.sub_circuit): - raise ValueError("layout_out length must match number of sub-circuits.") - - # ---- 检查子电路规模是否符合后端限制 ---- - for idx, (sub_circ, qpu) in enumerate(zip(self.sub_circuit, qpus)): - backend = qpu.backend - size = sub_circ.num_qubits - qpu_id = getattr(qpu, "qpu_id", idx) - backend_name = getattr(backend, "name", "unknown") - - if size > backend.num_qubits: - raise ValueError( - f"QPU {qpu_id}: requested size={size} exceeds " - f"the maximum number of qubits ({backend.num_qubits}) " - f"supported by backend {backend_name}." - ) - - self.sub_circuit_trans = [] - - # ---- 对每个子电路执行 transpile ---- - for idx, (sub, qpu) in enumerate(zip(self.sub_circuit, qpus)): - try: - # Step 1: 保护自定义指令 - sub = self.protect_custom_instructions(sub) - - layout = None - if layout_out is not None and idx < len(layout_out): - layout = layout_out[idx] - - # Step 3: 调用 transpile - if layout is not None: - sub = transpile( - sub, - backend=qpu.backend, - initial_layout=layout, - optimization_level=3, - ) - else: - sub = transpile( - sub, - backend=qpu.backend, - optimization_level=3, - ) - - # Step 4: 还原自定义指令 - sub = self.restore_custom_instructions(sub) - - self.sub_circuit_trans.append(sub) - print(f"[Info] Sub-circuit {idx} transpiled successfully on {qpu.backend.name}.") - - except Exception as e: - print(f"[Error] Failed to transpile sub-circuit {idx} on {qpu.backend.name}: {e}") - self.sub_circuit_trans.append(None) - - return self.sub_circuit_trans - - # Merge the transpiled sub-circuits into a complete circuit - def merge_trans_circuits(self, comm_noise = False): - """ - 将 self.sub_circuit_trans 中的子电路组合成完整电路。 - 支持跨电路配对操作 (R, M+IF_X/IF_Z), 通过栈实现返回上层逻辑。 - 双向配对:无论先遇到 M 还是 IF_X/IF_Z 都能触发配对。 - """ - # === 构建 qubit / cbit 映射 === - qubits_map = {} - merged_qubits_map = {} - cbits_map = {} - global_q_index = 0 - global_c_index = 0 - for i, sub in enumerate(self.sub_circuit_trans): - if sub is None: - continue - local_indices = {sub.find_bit(q).index for instr in sub.data for q in instr.qubits} - for local_index in sorted(local_indices): - qubits_map[(i, local_index)] = global_q_index - merged_qubits_map[global_q_index] = (i, local_index) - global_q_index += 1 - - # Record index for noise model - self.merged_qubits_map = merged_qubits_map - self.merged_qubits_map_reverse = qubits_map - - num_sub = len(self.sub_circuit_trans) - for i in range(num_sub): - for j in range(num_sub): - if i != j: - cbits_map[(i, j)] = global_c_index - global_c_index += 1 - - # === 初始化变量 === - # 创建新电路(只初始化量子比特) - new_circ = DQCCircuit(len(qubits_map)) - new_circ.qubit_group = self.step[1].qubit_group - - # === 1. 添加通信寄存器 === - tele_creg = ClassicalRegister(len(cbits_map), "Tele") - new_circ.add_register(tele_creg) - - # === 2. 保留原始经典寄存器 === - if hasattr(self, "clbits") and self.clbits: - if hasattr(self, "cregs") and self.cregs: - orig_name = self.cregs[0].name - else: - orig_name = "c" - orig_creg = ClassicalRegister(len(self.clbits), orig_name) - new_circ.add_register(orig_creg) - - indices = [0] * len(self.sub_circuit_trans) - paired_op = {} # {index: (sub_index, instr)} - paired_op2 = {} - paired_done = set() # 已完成配对 - call_stack = [] - now = 0 - step = 0 - - # 条件操作函数 - def apply_conditional_gate(gate_list, qubit, clbit): - with new_circ.if_test((clbit, 1)): - for instr in gate_list: - new_circ.append(instr.operation, [qubit], instr.clbits) - - while True: - done = all(sub is None or indices[i] >= len(sub.data) for i, sub in enumerate(self.sub_circuit_trans)) - if done: - break - - sub = self.sub_circuit_trans[now] - if sub is None or indices[now] >= len(sub.data): - if call_stack: - now = call_stack.pop() - continue - else: - now = (now + 1) % len(self.sub_circuit_trans) - continue - - instr = sub.data[indices[now]] - inst = instr.operation - op_name = inst.name.upper() - local_indices = [sub.find_bit(q).index for q in instr.qubits] - global_qs = [qubits_map[(now, idx)] for idx in local_indices] - target = getattr(inst, "target", None) - idx = getattr(inst, "index", None) - mea = getattr(inst,"mea", None) - - # print(f"\n[STEP] now={now}, target={target},idx={idx}, inst={inst.name}, indices={indices}") - - # === R 门配对生成 Bell 态 === - if op_name == "R" and idx not in paired_done: - target_sub = self.sub_circuit_trans[target] if target is not None else None - - if idx not in paired_op: - paired_op[idx] = now - if target_sub: - call_stack.append(now) - now = target - continue - else: - first_now = paired_op.pop(idx) - first_sub = self.sub_circuit_trans[first_now] - first_instr = first_sub.data[indices[first_now]] - first_qs = [qubits_map[(first_now, first_sub.find_bit(q).index)] for q in first_instr.qubits] - target_qs = global_qs - # print(first_now,now,first_qs,target_qs) - new_circ.initialize([1/np.sqrt(2),0,0,1/np.sqrt(2)], first_qs + target_qs) - if comm_noise: - noise_instr = self.qpugroup.get_noise_instruction(first_now, now) - # 插入噪声,映射到新电路 qubit - if isinstance(target_qs, list): - # 对列表中所有量子比特加噪声 - noise_qargs = [new_circ.qubits[i] for i in target_qs] - else: - # 对单个 qubit 加噪声 - noise_qargs = [new_circ.qubits[target_qs]] - - if noise_instr is not None: - new_circ.append(noise_instr, noise_qargs) - - paired_done.add(idx) - indices[now] += 1 - indices[first_now] += 1 - now = call_stack.pop() if call_stack else now - continue - - # === M / IF_X / IF_Z 双向配对 === - elif op_name in ("MX", "MZ") and idx not in paired_done: - if idx not in paired_op: - paired_op[idx] = now - if target_sub: - call_stack.append(now) - now = target - continue - - # 已经配对 - first_now = paired_op.pop(idx) - first_sub = self.sub_circuit_trans[first_now] - first_instr = first_sub.data[indices[first_now]] - - first_qs = [qubits_map[(first_now, first_sub.find_bit(q).index)] - for q in first_instr.qubits] - target_qs = global_qs - - # classical bit 对应 - cbit_idx1 = cbits_map[(now, first_now)] - cbit_idx2 = cbits_map[(first_now, now)] - clbit_obj1 = new_circ.clbits[cbit_idx1] - clbit_obj2 = new_circ.clbits[cbit_idx2] - - - if first_instr.name == "MX": - new_circ.measure(first_qs[0], clbit_obj1) - apply_conditional_gate(self.qpus[now].compile_z_gate(), target_qs[0], clbit_obj1) - new_circ.measure(target_qs[1], clbit_obj2) - apply_conditional_gate(self.qpus[first_now].compile_x_gate(), first_qs[1], clbit_obj2) - - else: - new_circ.measure(first_qs[1], clbit_obj1) - apply_conditional_gate(self.qpus[now].compile_x_gate(), target_qs[1], clbit_obj1) - new_circ.measure(target_qs[0], clbit_obj2) - apply_conditional_gate(self.qpus[first_now].compile_z_gate(), first_qs[0], clbit_obj2) - - - # 更新状态 - paired_done.add(idx) - indices[now] += 1 - indices[first_now] += 1 - now = call_stack.pop() if call_stack else now - continue - elif op_name == "ANS_M": - # global_qs[0] 是量子比特索引,mea 是存储的经典比特索引 - q_idx = global_qs[0] - c_idx = mea # mea 已经是全局经典比特索引 - - # 在 new_circ 中找到对应的 Clbit 对象 - clbit_obj = None - temp_idx = c_idx - for creg in new_circ.cregs: - if temp_idx < len(creg): - clbit_obj = creg[temp_idx] - break - else: - temp_idx -= len(creg) - - if clbit_obj is None: - raise ValueError(f"Cannot find the classical bit index {c_idx} corresponding to a Clbit") - - # 添加测量操作 - new_circ.measure(new_circ.qubits[q_idx], clbit_obj) - indices[now] += 1 - continue - elif op_name in ("IF_X", "IF_Z", "MS") and idx not in paired_done: - target_sub = self.sub_circuit_trans[target] if target is not None else None - if idx not in paired_op: - paired_op[idx] = now - if target_sub: - call_stack.append(now) - now = target - continue - if idx not in paired_op2: - paired_op2[idx] = now - if target_sub: - call_stack.append(now) - now = target - continue - - # 已经配对 - first_now = paired_op.pop(idx) - first_sub = self.sub_circuit_trans[first_now] - first_instr = first_sub.data[indices[first_now]] - - second_now = paired_op2.pop(idx) - second_sub = self.sub_circuit_trans[second_now] - second_instr = second_sub.data[indices[second_now]] - - first_qs = [qubits_map[(first_now, first_sub.find_bit(q).index)] - for q in first_instr.qubits] - - second_qs = [qubits_map[(second_now, second_sub.find_bit(q).index)] - for q in second_instr.qubits] - - # instr global_qs - third_qs = [qubits_map[(now, self.sub_circuit_trans[now].find_bit(q).index)] - for q in instr.qubits] - - # classical bit 对应 - cbit_idx1 = cbits_map[(first_now, second_now)] - cbit_idx2 = cbits_map[(second_now, first_now)] - cbit_idx3 = cbits_map[(second_now, now)] - cbit_idx4 = cbits_map[(now, second_now)] - cbit_idx5 = cbits_map[(now, first_now)] - cbit_idx6 = cbits_map[(first_now, now)] - - clbit_obj1 = new_circ.clbits[cbit_idx1] - clbit_obj2 = new_circ.clbits[cbit_idx2] - clbit_obj3 = new_circ.clbits[cbit_idx3] - clbit_obj4 = new_circ.clbits[cbit_idx4] - clbit_obj5 = new_circ.clbits[cbit_idx5] - clbit_obj6 = new_circ.clbits[cbit_idx6] - # print("Entanglement Swapping", first_instr.name,second_instr.name, instr.name) - if first_instr.name == "IF_Z": - new_circ.measure(second_qs[0], clbit_obj2) - apply_conditional_gate(self.qpus[first_now].compile_z_gate(), first_qs, clbit_obj2) - new_circ.measure(second_qs[1], clbit_obj3) - apply_conditional_gate(self.qpus[now].compile_x_gate(), third_qs, clbit_obj3) - elif first_instr.name == "MS": - new_circ.measure(first_qs[0], clbit_obj6) - apply_conditional_gate(self.qpus[now].compile_z_gate(), third_qs, clbit_obj6) - new_circ.measure(first_qs[1], clbit_obj1) - apply_conditional_gate(self.qpus[second_now].compile_x_gate(), second_qs, clbit_obj1) - elif first_instr.name == "IF_X": - new_circ.measure(third_qs[0], clbit_obj4) - apply_conditional_gate(self.qpus[second_now].compile_z_gate(), second_qs, clbit_obj4) - new_circ.measure(third_qs[1], clbit_obj5) - apply_conditional_gate(self.qpus[first_now].compile_x_gate(), first_qs, clbit_obj5) - # 更新状态 - paired_done.add(idx) - indices[now] += 1 - indices[first_now] += 1 - indices[second_now] += 1 - now = call_stack.pop() if call_stack else now - now = call_stack.pop() if call_stack else now - continue - # === 普通门 === - else: - new_circ.append(inst, global_qs, []) - indices[now] += 1 - continue - - step += 1 - if step > 50000: - raise RuntimeError(f"[Error] Possible deadlock. indices={indices}, now={now}, stack={call_stack}") - - self.result_circuit = new_circ - self.result_circuit.qubit_group = self.step[4].qubit_group - return new_circ - - def decompose_and_get_data(self, instr: CircuitInstruction, basis_gates=None): - """ - 递归分解单个指令到基础门集。 - - 参数: - instr: 待分解的 CircuitInstruction - basis_gates: 允许的基础门名称集合(默认: {'cx'} + 单比特门) - - 返回: - 保留原始 qubit 引用的 CircuitInstruction 列表(可直接用于 circuit.data) - """ - if basis_gates is None: - # 定义基础门集合 - basis_gates = {'cx', 'u3', 'u2', 'u1', 'id', 'x', 'y', 'z', - 'h', 's', 't', 'rx', 'ry', 'rz', 'sx', 'p'} - - # 如果是基础门或没有定义,直接返回 - if (not hasattr(instr.operation, "definition") or - instr.operation.name in basis_gates): - return [instr] - - # 创建临时电路进行分解 - n_qubits = len(instr.qubits) - qc_temp = QuantumCircuit(n_qubits) - qc_temp.append(instr.operation, list(range(n_qubits))) - - # 建立临时 qubit 到原始 qubit 的映射 - # 临时电路的 qc_temp.qubits[i] 对应原始的 instr.qubits[i] - temp_to_orig = {qc_temp.qubits[i]: instr.qubits[i] for i in range(n_qubits)} - - # 分解一次 - decomposed = qc_temp.decompose() - - result = [] - for sub_instr in decomposed.data: - # 使用映射字典将临时 qubit 对象映射回原始 qubit 对象 - mapped_qubits = [temp_to_orig[q] for q in sub_instr.qubits] - - # 创建新的指令 - new_instr = CircuitInstruction( - sub_instr.operation, - mapped_qubits, - sub_instr.clbits - ) - - # 递归分解(如果还需要) - result.extend(self.decompose_and_get_data(new_instr, basis_gates)) - - return result - - def reduce_noise_model(self, subset_qubits, noise_model=None, coupling_map=None): - """ - 裁剪 NoiseModel,使其只保留 subset_qubits 上的噪声,同时保留 basis_gates、description 和可选耦合图。 - - 参数: - subset_qubits: list[int] - 需要保留噪声的量子比特 - noise_model: NoiseModel, 可选 - 如果不提供,则使用 self.backend_noise_model - coupling_map: list[tuple], 可选 - 如果提供,则裁剪耦合图 - - 返回: - NoiseModel - """ - - if noise_model is None: - if getattr(self, "backend_noise_model", None) is None: - raise ValueError("No noise model provided or set in self.backend_noise_model.") - noise_model = self.backend_noise_model - - # 如果 Qiskit 版本支持 reduce(),优先使用 - if hasattr(noise_model, "reduce"): - return noise_model.reduce(subset_qubits) - - sub_model = NoiseModel() - - # --- 1️⃣ 保留量子门噪声 --- - for instr_name, qerrors in noise_model._local_quantum_errors.items(): - for qubits, error in qerrors.items(): - if all(q in subset_qubits for q in qubits): - sub_model.add_quantum_error(error, instr_name, qubits) - - # --- 2️⃣ 保留读出噪声 (measure) --- - for qubit, error in noise_model._local_readout_errors.items(): - if qubit[0] in subset_qubits: # qubit 是 tuple,如 (0,) - sub_model.add_readout_error(error, [qubit[0]]) - - # --- 3️⃣ 保留 basis_gates 和 description --- - if hasattr(noise_model, "basis_gates"): - sub_model._basis_gates = list(noise_model.basis_gates) - if hasattr(noise_model, "description"): - sub_model._description = noise_model.description - - # --- 4️⃣ 可选裁剪耦合图 --- - target_coupling_map = coupling_map if coupling_map is not None else getattr(self, "coupling_map", None) - if target_coupling_map is not None: - sub_model._coupling_map = [edge for edge in target_coupling_map if all(q in subset_qubits for q in edge)] - else: - sub_model._coupling_map = None - - return sub_model - - # Get a combined noise model for the distributed circuit based on QPU backends - def get_noise_model(self): - """ - 构建一个综合噪声模型 (combined_noise_model), - 将多个 QPU 的噪声模型裁剪后合并,并映射到全局连续 qubit。 - """ - # === Step 1: 参数检查 === - qpus = self.qpugroup.qpus - qpus = sorted(qpus, key=lambda x: x.qpu_id) - if not self.sub_circuit: - raise ValueError("No sub-circuits found.") - if len(qpus) != len(self.sub_circuit): - raise ValueError( - f"Number of QPUs ({len(qpus)}) does not match sub-circuits ({len(self.sub_circuit)})." - ) - - # === Step 2: 提取每个子电路对应的局部 qubit === - sub_qubit_maps = [] - for idx, _ in enumerate(self.sub_circuit): - local_qubits = [ - local_index - for global_q, (sub_index, local_index) in self.merged_qubits_map.items() - if sub_index == idx - ] - sub_qubit_maps.append(local_qubits) - - # === Step 3: 构建全局 qubit 映射 (sub_idx, local_qubit) -> global_qubit === - qubit_mapping = self.merged_qubits_map_reverse - - # === Step 4: 初始化全局 NoiseModel === - combined_noise_model = NoiseModel() - combined_basis_gates = set() - - # === Step 5: 对每个 QPU 裁剪并合并噪声 === - for idx, qpu in enumerate(qpus): - backend_noise = NoiseModel.from_backend(qpu.backend) - local_qubits = sub_qubit_maps[idx] - - # --- 5a. 裁剪局部噪声 --- - reduced_noise = self.reduce_noise_model( - subset_qubits=local_qubits, - noise_model=backend_noise, - coupling_map=qpu.backend.coupling_map - ) - - # --- 5b. 合并量子门噪声 --- - for instr_name, qerrors in reduced_noise._local_quantum_errors.items(): - for qubits_tuple, error in qerrors.items(): - # 解包 qubit - qubit_indices = [q if isinstance(q, int) else q[0] for q in qubits_tuple] - global_qubits = tuple(qubit_mapping[(idx, q)] for q in qubit_indices) - combined_noise_model.add_quantum_error(error, instr_name, global_qubits) - # print(f"Added quantum error: {instr_name}, local {qubit_indices} -> global {global_qubits}") - - # --- 5c. 合并读出噪声 --- - for qubit, error in reduced_noise._local_readout_errors.items(): - q_local = qubit[0] if isinstance(qubit, tuple) else qubit - if (idx, q_local) not in qubit_mapping: - print(f"⚠️ Warning: qubit mapping missing for {(idx, q_local)}") - continue - global_qubit = qubit_mapping[(idx, q_local)] - combined_noise_model.add_readout_error(error, [global_qubit]) - # print(f"Added readout error: local {q_local} -> global {global_qubit}") - - # --- 5d. 汇总 basis_gates --- - if hasattr(reduced_noise, "basis_gates"): - combined_basis_gates.update(reduced_noise.basis_gates) - - # === Step 6: 写入全局基础门、描述信息 === - combined_noise_model._basis_gates = list(combined_basis_gates) - combined_noise_model._description = "Combined noise model from multiple QPUs" - - return combined_noise_model - -def detect_swap_pattern(circuit): - swap_count = 0 - instructions = list(circuit.data) - - i = 0 - while i < len(instructions) - 2: - inst1 = instructions[i] - inst2 = instructions[i + 1] - inst3 = instructions[i + 2] - - # 检查是否都是 CX 门 - if (inst1.operation.name == 'cx' and - inst2.operation.name == 'cx' and - inst3.operation.name == 'cx'): - - # 获取量子比特索引 - q1_0 = circuit.find_bit(inst1.qubits[0]).index - q1_1 = circuit.find_bit(inst1.qubits[1]).index - - q2_0 = circuit.find_bit(inst2.qubits[0]).index - q2_1 = circuit.find_bit(inst2.qubits[1]).index - - q3_0 = circuit.find_bit(inst3.qubits[0]).index - q3_1 = circuit.find_bit(inst3.qubits[1]).index - - # 检查 SWAP 模式: CX(a,b), CX(b,a), CX(a,b) - if (q1_0 == q3_0 and q1_1 == q3_1 and # 第1和第3个相同 - q2_0 == q1_1 and q2_1 == q1_0): # 第2个是反向的 - swap_count += 1 - print(f" 检测到 SWAP 模式在位置 {i}: q{q1_0} ↔ q{q1_1}") - i += 3 # 跳过这3个门 - continue - - i += 1 - - print("\n" + "=" * 70) - print("检测 CNOT 模式") - print("=" * 70) - print(f"检测到的 SWAP 数量: {swap_count}") - - return swap_count \ No newline at end of file +from qiskit import QuantumCircuit, ClassicalRegister, transpile +from qiskit.circuit import Instruction, CircuitInstruction +from qiskit_aer import AerSimulator +from qiskit.circuit.library.standard_gates import HGate, XGate, ZGate, CXGate +from qiskit.circuit import Reset, Measure, ClassicalRegister +from qiskit.visualization import plot_histogram +from qiskit import QuantumRegister +from qiskit.quantum_info import Kraus +from qiskit.converters import circuit_to_dag +from qiskit_ibm_runtime.fake_provider import ( + FakeVigoV2, # 5 + FakeLagosV2, # 7 + FakeCasablancaV2, + FakeYorktownV2, + FakeManilaV2, + FakeNairobiV2, + FakeMumbaiV2, + FakeKolkataV2, + FakeGuadalupeV2, + FakeAlmadenV2, + FakeAthensV2, # 5 + FakeCambridgeV2 +) + +# Backend IonQ +from .backend import IonQ +from .features import extract_feature_bundle + +from qiskit_aer.noise import ( + NoiseModel, + depolarizing_error, pauli_error, + amplitude_damping_error, phase_amplitude_damping_error, + phase_damping_error +) + +import numpy as np +import copy +import time +import matplotlib.pyplot as plt + +class RemoteGate(Instruction): + """ Remote Gate""" + def __init__(self, index: int, target: int): + super().__init__("R", 1, 0, []) + self.index = index + self.target = target + +class MX(Instruction): + """MX for Control Side""" + def __init__(self, index: int, target: int): + super().__init__("MX", 2, 0, []) + self.index = index + self.target = target + +class MZ(Instruction): + """MZ for Target Side""" + def __init__(self, index: int, target: int): + super().__init__("MZ", 2, 0, []) + self.index = index + self.target = target + +class AnsM(Instruction): + """AnsM Measurement""" + def __init__(self, mea: int): + super().__init__("ANS_M", 1, 0, []) + self.mea = mea + +class S_CX(Instruction): + """Custom CNOT Gate""" + def __init__(self, control: int, target: int, path): + super().__init__("S_CX", 2, 0, []) + self.control = control + self.target = target + self.path = path + +class MS(Instruction): + """Multi-qubit Swap Gate""" + def __init__(self, index: int, target: int): + super().__init__("MS", 2, 0, []) + self.index = index + self.target = target + +class IF_Z(Instruction): + def __init__(self, index: int, target: int): + super().__init__("IF_Z", 1, 0, []) + self.index = index + self.target = target + +class IF_X(Instruction): + """Conditional X Gate""" + def __init__(self, index: int, target: int): + super().__init__("IF_X", 1, 0, []) + self.index = index + self.target = target + +# Mapping backend names to their classes +FAKE_BACKENDS = { + "FakeVigoV2": FakeVigoV2, + "FakeLagosV2": FakeLagosV2, + "FakeCasablancaV2": FakeCasablancaV2, + "FakeYorktownV2": FakeYorktownV2, + "FakeManilaV2": FakeManilaV2, + "FakeNairobiV2": FakeNairobiV2, + "FakeMumbaiV2": FakeMumbaiV2, + "FakeKolkataV2": FakeKolkataV2, + "FakeGuadalupeV2": FakeGuadalupeV2, + "FakeAlmadenV2": FakeAlmadenV2, + "FakeAthensV2": FakeAthensV2, + "FakeCambridgeV2": FakeCambridgeV2, + "IonQ": IonQ +} + +class QPUManager: + def __init__(self): + """ + Initialize QPU manager to handle multiple QPUs and their connections. + + Attributes: + qpus: List of QPU instances + noise_instructions: Dict mapping QPU pairs to noise instructions + map: Adjacency list representing QPU network topology + size: Total number of QPUs in the manager + """ + # Store QPUs and noise instructions + self.qpus = [] + self.noise_instructions = {} + # Adjacency list: {qpu_id: [(neighbor_id, distance), ...]} + self.map = {} + self.size = 0 + + def add_qpu(self, qpu): + """Add a QPU to the manager""" + self.qpus.append(qpu) + qpu_id = qpu.qpu_id + self.size += 1 + # Initialize adjacency list entry if not exists + if qpu_id not in self.map: + self.map[qpu_id] = [] + + def get_qpu(self, qpu_id): + """Retrieve QPU instance by ID""" + for qpu in self.qpus: + if qpu.qpu_id == qpu_id: + return qpu + return None + + def add_coonnection(self, qpu_id1, qpu_id2, distance: float = 0): + """Add bidirectional connection between two QPUs""" + qpu1 = self.get_qpu(qpu_id1) + qpu2 = self.get_qpu(qpu_id2) + if qpu1 is None or qpu2 is None: + raise ValueError(f"QPU {qpu_id1} or {qpu_id2} not found.") + + qpu1.add_connection(qpu_id2, distance) + qpu2.add_connection(qpu_id1, distance) + + # Add to adjacency list if not already present + if not any(n == qpu_id2 for n, _ in self.map[qpu_id1]): + self.map[qpu_id1].append((qpu_id2, distance)) + if not any(n == qpu_id1 for n, _ in self.map[qpu_id2]): + self.map[qpu_id2].append((qpu_id1, distance)) + + # Store noise instructions for both directions + noise_instr = qpu1.get_noise_by_distance(distance) + self.noise_instructions[(qpu_id1, qpu_id2)] = noise_instr + self.noise_instructions[(qpu_id2, qpu_id1)] = noise_instr + + def get_noise_instruction(self, qpu_id1, qpu_id2): + """Get noise instruction for connection between two QPUs""" + return self.noise_instructions.get((qpu_id1, qpu_id2), None) + + def check_connection(self, qpu_id1, qpu_id2) -> int: + """Check if two QPUs are connected (returns 1 if connected, 0 otherwise)""" + if qpu_id1 not in self.map: + return 0 + return 1 if any(n == qpu_id2 for n, _ in self.map[qpu_id1]) else 0 + + +class DQCQPU: + def __init__(self, qpu_id: int, backend_name: str, connections=None, noise_type: str = None, **noise_kwargs): + """ + Initialize a QPU instance with automatic custom instruction registration. + + :param qpu_id: QPU identifier + :param backend_name: Name of the backend (e.g., "FakeLagosV2") + :param connections: Optional list of connections to other QPUs + :param noise_type: Type of noise model to apply + :param noise_kwargs: Additional noise parameters + """ + self.qpu_id = qpu_id + + # Validate backend name + if backend_name not in FAKE_BACKENDS: + raise ValueError( + f"Unknown backend name '{backend_name}'. " + f"Available options are: {list(FAKE_BACKENDS.keys())}" + ) + + # Initialize backend + backend_cls = FAKE_BACKENDS[backend_name] + backend = backend_cls() + + # Register custom instructions to target + target = backend.target + target.add_instruction(RemoteGate, name="R") + target.add_instruction(MX, name="MX") + target.add_instruction(MZ, name="MZ") + target.add_instruction(AnsM, name="ANS_M") + target.add_instruction(AnsM, name="IF_Z") + target.add_instruction(AnsM, name="IF_X") + target.add_instruction(S_CX, name="MS") + + self.backend = backend + self.target = backend.target + + # Initialize connections and noise configuration + self.connections = connections if connections else [] + + if noise_type is not None: + self.noise_config = {"type": noise_type, "params": noise_kwargs} + else: + self.noise_config = None + + def add_connection(self, other_qpu_id: int, distance: float = 1.0): + """Add connection to another QPU with distance-based noise""" + kraus = self.get_noise_by_distance(distance) + conn = { + "id": other_qpu_id, + "distance": distance, + "noise": kraus + } + self.connections.append(conn) + + def compile_x_gate(self): + """Compile X gate for this QPU's backend""" + qc = QuantumCircuit(1) + qc.x(0) + compiled = transpile(qc, self.backend) + return compiled.data + + def compile_z_gate(self): + """Compile Z gate for this QPU's backend""" + qc = QuantumCircuit(1) + qc.z(0) + compiled = transpile(qc, self.backend) + return compiled.data + + def exponential(self, L, alpha): + """Calculate exponential decay based on distance""" + return np.exp(-alpha * L) + + def get_noise_by_distance(self, distance=1): + """ + Generate composite noise (amplitude damping + depolarizing) Kraus instruction + based on transmission distance. + :param distance: Transmission distance (e.g., km or m) + :return: Qiskit Instruction containing Kraus operators + """ + + # Calculate amplitude damping parameter + if distance == 0: + gamma = 0 + else: + gamma = self.exponential(distance, alpha=0.02) + + # Calculate depolarizing parameter + p_depol = gamma + + gamma = 1 - gamma + p_depol = 1 - p_depol + + # Amplitude damping Kraus operators + K0 = np.array([[1, 0], [0, np.sqrt(1 - gamma)]], dtype=complex) + K1 = np.array([[0, np.sqrt(gamma)], [0, 0]], dtype=complex) + K_amp = [K0, K1] + + # Depolarizing Kraus operators + sqrt1mp = np.sqrt(1 - p_depol) + sqrt_p3 = np.sqrt(p_depol / 3) + I = np.eye(2, dtype=complex) + X = np.array([[0, 1], [1, 0]], dtype=complex) + Y = np.array([[0, -1j], [1j, 0]], dtype=complex) + Z = np.array([[1, 0], [0, -1]], dtype=complex) + K_depol = [sqrt1mp * I, sqrt_p3 * X, sqrt_p3 * Y, sqrt_p3 * Z] + + # Combine channels (amplitude → depolarizing) + K_combined = [D @ A for D in K_depol for A in K_amp] + + # Create Qiskit Kraus instruction + kraus_instr = Kraus(K_combined).to_instruction() + + return kraus_instr + + def __repr__(self): + return ( + f"" + ) + +# ------------------------------------------------------------- +class DQCCircuit(QuantumCircuit): + def __init__(self, *args, **kwargs): + # 用已有 QuantumCircuit 初始化 + if len(args) == 1 and isinstance(args[0], QuantumCircuit): + qc = args[0] + # 保留原寄存器结构 + super().__init__(*qc.qregs, *qc.cregs, + name=qc.name, + global_phase=qc.global_phase, + metadata=copy.deepcopy(qc.metadata) if qc.metadata else None) + + # 复制电路数据 + self.data = copy.deepcopy(qc.data) + else: + super().__init__(*args, **kwargs) + + self.step = [] # 各阶段存储的电路图 + self.step.append(copy.deepcopy(self)) + + self.sub_circuit = [] # 子电路 + self.sub_circuit_trans = [] # 编译后的子电路集合 + self.result_circuit = None # 最终结果电路 + + self.partition = [] # 划分 + self.qubit_group = [-1] * self.num_qubits # 比特分组 + self.Entanglement_swapping = [] # 纠缠信息 + self.swap_routes = [] # 交换路径 + + self.qubit_tele = [] # 比特对应的通信比特位置 + + self.qpus = [] + self.merged_qubits_map = {} # 合并后比特映射 + self.merged_qubits_map_reverse = {} # 反向映射 + + self.qpugroup = None + + self.Num_Entanglement_swapping = 0 # 纠缠交换次数统计 + self.Num_RemoteGate = 0 + + def get_feature_bundle( + self, + circuit=None, + backend=None, + transpile_first=False, + optimization_level=3, + seed_transpiler=7, + ): + """ + 提取完整特征信息。 + backend 为 None 时,默认使用 self.qpugroup 的第一个 QPU backend。 + """ + target_circuit = self if circuit is None else circuit + + if backend is None: + if self.qpugroup is not None and getattr(self.qpugroup, "qpus", None): + backend = self.qpugroup.qpus[0].backend + + return extract_feature_bundle( + target_circuit, + backend=backend, + transpile_first=transpile_first, + optimization_level=optimization_level, + seed_transpiler=seed_transpiler, + ) + + def get_feature_groups( + self, + circuit=None, + backend=None, + transpile_first=False, + optimization_level=3, + seed_transpiler=7, + ): + """提取分组特征。""" + return self.get_feature_bundle( + circuit=circuit, + backend=backend, + transpile_first=transpile_first, + optimization_level=optimization_level, + seed_transpiler=seed_transpiler, + )["feature_groups"] + + def get_feature_vector( + self, + circuit=None, + backend=None, + transpile_first=False, + optimization_level=3, + seed_transpiler=7, + ): + """提取扁平特征向量。""" + return self.get_feature_bundle( + circuit=circuit, + backend=backend, + transpile_first=transpile_first, + optimization_level=optimization_level, + seed_transpiler=seed_transpiler, + )["feature_vector"] + + # 执行 + def Execution(self, config, qpugroup, comm_noise=False, measure_time=False): + # 开始计时 + if measure_time: + start_time = time.time() + + self.qpugroup = qpugroup + qpus = self.qpugroup.qpus + + self.split(config) + self.valid_trans() + self.check_swap_entanglement() + print("Entanglement Number:", self.Num_Entanglement_swapping) + self.rearrange_with_partition() + self.rewrite_cross_group_cnots() + self.physic_split() + + self.transpile_subcircuits(qpus) + # for sub_circ in self.sub_circuit_trans: + # sub_circ.draw("mpl", scale = 0.5, fold = 100) + # plt.show() + + # dag = circuit_to_dag(self.sub_circuit_trans[2]) + # dag.draw(output='mpl') + # plt.show() + + result_qc = self.merge_trans_circuits(comm_noise) + + # 结束计时并输出 + if measure_time: + end_time = time.time() + elapsed_time = end_time - start_time + print(f"SimDisQ Execution time: {elapsed_time:.4f} seconds") + + return result_qc + + # Get the index of a qubit in the circuit + def get_index(self, q): + return int(self.find_bit(q).index) + + # Split the circuit based on the provided configuration + def split(self, config): + partition = [] + + if all(isinstance(x, int) for x in config): + # 按长度生成索引列表 + start = 0 + for s in config: + indices = list(range(start, start + s)) + partition.append(indices) + start += s + elif all(isinstance(x, (list, tuple)) for x in config): + # 按指定索引组合, 并排序 + for sub in config: + partition.append(sorted(sub)) + else: + raise ValueError("Input must be a list of ints or a list of lists of ints") + + self.partition = partition + + for gid, group in enumerate(partition): + self.Entanglement_swapping.append(0) + for q in group: + self.qubit_group[q] = gid + return partition + + # Preprocessing to validate and decompose cross-group multi-qubit gates to single-qubit gates and CX gates + # step[1] + def valid_trans(self): + """ + 遍历 old_circ 的指令,检查是否跨组多比特门。 + 如果跨组且不是 CX,则分解后 append 到 new_circ; + 如果同组或是 CX,则直接 append。 + + 参数: + new_circ: DQCCircuit,目标电路 + old_circ: DQCCircuit,源电路(step[0]) + """ + old_circ = self + + # === 1. 构建新电路(仅复制量子比特)=== + new_circ = DQCCircuit(old_circ.qubits) + new_circ.qubit_group = old_circ.qubit_group + new_circ.qubit_tele = old_circ.qubit_tele + print("Qubit Group:", new_circ.qubit_group) + + # === 2. 继承所有经典寄存器 === + # 复制通信寄存器(如果有) + comm_cregs = [creg for creg in getattr(old_circ, "cregs", []) if "Tele" in creg.name] + for comm_creg in comm_cregs: + new_circ.add_register(ClassicalRegister(len(comm_creg), comm_creg.name)) + + # 复制原始寄存器(如果有) + orig_cregs = [creg for creg in getattr(old_circ, "cregs", []) if "Tele" not in creg.name] + for orig_creg in orig_cregs: + new_circ.add_register(ClassicalRegister(len(orig_creg), orig_creg.name)) + + for instri in old_circ.data: + instr = instri.operation # 量子门或操作对象 + qargs = instri.qubits # 作用的量子比特列表 + cargs = instri.clbits # 作用的经典比特列表 + + # 先处理cx + if instr.name in ["cx"]: + new_circ.append(instr, qargs, cargs) + continue + + # 单比特门直接 append + if len(qargs) <= 1: + new_circ.append(instr, qargs, cargs) + continue + + # 多比特门,判断是否跨组 + groups = [new_circ.qubit_group[old_circ.get_index(q)] for q in qargs] + if len(set(groups)) > 1: + # 跨组且不是 CX,分解 + ci = CircuitInstruction(instr, qargs, cargs) + decomposed_instrs = self.decompose_and_get_data(ci) + for di in decomposed_instrs: + mapped_qubits = [new_circ.qubits[old_circ.get_index(q)] for q in di.qubits] + mapped_clbits = [new_circ.clbits[old_circ.get_index(c)] for c in di.clbits] + new_circ.append(di.operation, mapped_qubits, mapped_clbits) + else: + # 同组,多比特门直接 append + new_circ.append(instr, qargs, cargs) + + self.step.append(new_circ) + return new_circ + + # step[2] + def check_swap_entanglement(self): + """检查跨 QPU 纠缠,如果无直接连接则寻找可行的 SWAP 路径""" + swap_routes = [] + qpu_map = self.qpugroup.map + + old_circ = self.step[1] + + # === 1. 构建新电路(仅复制量子比特)=== + new_circ = DQCCircuit(old_circ.qubits) + + for creg in getattr(old_circ, "cregs", []): + new_circ.add_register(ClassicalRegister(len(creg), creg.name)) + + # 遍历电路中的每一个 CX 操作 + for instr in old_circ.data: + if instr.operation.name == "cx": + ctrl, tgt = instr.qubits + g_ctrl = self.qubit_group[self.get_index(ctrl)] + g_tgt = self.qubit_group[self.get_index(tgt)] + + # 如果控制和目标在不同 QPU 上 + if g_ctrl != g_tgt: + # 检查是否直接连接 + if not self.qpugroup.check_connection(g_ctrl, g_tgt): + # --- 没有直接连接:寻找最短路径 --- + path = self._find_shortest_path(qpu_map, g_ctrl, g_tgt) + if path: + swap_routes.append(path) + new_circ.append(S_CX(g_ctrl, g_tgt, path), instr.qubits, instr.clbits) + print(f"[Info] Found SWAP route {path} for CX({g_ctrl}, {g_tgt})") + self.Num_Entanglement_swapping += len(path) - 2 + else: + print(f"[Warning] No route found between QPU {g_ctrl} and {g_tgt}") + else: + # 直接连接,直接添加 CX 操作 + new_circ.append(instr.operation, instr.qubits, instr.clbits) + else: + # 同组,直接添加 CX 操作 + new_circ.append(instr.operation, instr.qubits, instr.clbits) + else: + new_circ.append(instr.operation, instr.qubits, instr.clbits) + + for path in swap_routes: + # 取除首尾外的中间节点 + for node in path[1:-1]: + self.Entanglement_swapping[node] = 1 + + self.swap_routes = swap_routes + self.step.append(new_circ) + + def _find_shortest_path(self, graph, start, end): + """使用 BFS 在邻接表图中寻找最短路径""" + from collections import deque + + visited = set() + queue = deque([[start]]) # 队列中每个元素是路径 list + + while queue: + path = queue.popleft() + node = path[-1] + if node == end: + return path # 找到路径 + + if node not in visited: + visited.add(node) + for neighbor, _ in graph.get(node, []): # 遍历邻居 + if neighbor not in visited: + queue.append(path + [neighbor]) + + return None # 无路径 + + # step[3] + def rearrange_with_partition(self): + """ + 根据 partition 重新排列 qubits, 每组加一个通信比特, + 并为 group 之间的通信分配 classical bits。 + """ + if not self.partition: + raise ValueError("请先设置 self.partition") + + partition = self.partition + num_groups = len(partition) + + # classical bit 数量 = g * (g-1) + comm_creg = ClassicalRegister(num_groups * (num_groups - 1), "Tele") + + # 每组 qubits 数量 = 原本 + 1(comm) + + # 先统计 partition 中普通 qubit 数量 + total_qubits = sum(len(group) for group in partition) + # 给每组添加一个通信 qubit + total_qubits += len(partition) + # 给每个需要交换纠缠的 QPU 添加一个通信 qubit + total_qubits += self.Entanglement_swapping.count(1) + + new_qreg = QuantumRegister(total_qubits, "q") + + new_circ = DQCCircuit(new_qreg, comm_creg) + + old_circ = self.step[2] + # === 4. 保留原始经典寄存器信息 === + # 如果原始电路 self 中有经典寄存器 + if hasattr( old_circ, "clbits") and old_circ.clbits: + # 尝试从 self.cregs 中找到原寄存器名 + if hasattr( old_circ, "cregs") and old_circ.cregs: + # 取第一个 ClassicalRegister 的名字 + orig_name = old_circ.cregs[0].name + else: + orig_name = "c" # 如果没有记录,则默认命名为 "c" + + # 使用原寄存器名创建新的 ClassicalRegister + orig_creg = ClassicalRegister(len(old_circ.clbits), orig_name) + new_circ.add_register(orig_creg) + + # 保存引用方便之后访问 + new_circ.orig_creg = orig_creg + else: + new_circ.orig_creg = None + + # ===== 建立旧 qubit -> 新 qubit 映射 ===== + old2new = {} + group_comm_qubits = [] + new_index = 0 + + for indices in partition: + # 字典映射量子寄存器 + # group 内 qubit + for qi in indices: + old2new[qi] = new_qreg[new_index] + new_index += 1 + # comm qubit + comm_q = new_qreg[new_index] + group_comm_qubits.append(comm_q) + new_index += 1 + if self.Entanglement_swapping[self.qubit_group[indices[0]]] == 1: + # 需要交换纠缠的 QPU 多加一个 comm qubit + extra_comm_q = new_qreg[new_index] + group_comm_qubits.append(extra_comm_q) + new_index += 1 + + # ===== 遍历原电路, 重映射到新电路 ===== + for instr in old_circ.data: + qubit_indices = [old_circ.get_index(q) for q in instr.qubits] + + if all(qi in old2new for qi in qubit_indices): + new_qargs = [old2new[qi] for qi in qubit_indices] + new_circ.append(instr.operation, new_qargs, instr.clbits) + + # ==== 给 new_circ.qubit_group 填值 ===== + new_circ.qubit_group = [] + for gid, group in enumerate(partition): + base_count = len(group) + 1 # 普通 + 通信 + if self.Entanglement_swapping[gid] == 1: + base_count += 1 # 额外加一个 SWAP qubit + new_circ.qubit_group.extend([gid] * base_count) + + # ===== 给 new_circ.qubit_tele 填值 ===== + qubit_tele = [-1] * len(new_qreg) + + idx = 0 + for gid, group in enumerate(partition): + # 每个分区的普通 + 通信 qubit 数量 + comm_q = group_comm_qubits[idx] + comm_idx = new_circ.get_index(comm_q) + + # 普通 qubit 指向该组通信 qubit + for qi in group: + qubit_tele[new_circ.get_index(old2new[qi])] = comm_idx + + # 通信 qubit 自己设为 -1 + qubit_tele[comm_idx] = -1 + idx += 1 + + # 若该组需要 entanglement swapping,则还要处理多出来的通信 qubit + if self.Entanglement_swapping[gid] == 1: + swap_comm_q = group_comm_qubits[idx] + swap_comm_idx = new_circ.get_index(swap_comm_q) + qubit_tele[swap_comm_idx] = -1 # 交换通信 qubit 也设为 -1 + idx += 1 + + # 赋值 + new_circ.qubit_tele = qubit_tele + + self.step.append(new_circ) + return new_circ + + # Rewrite cross-group CNOTs into RemoteGate, Measurement, If_X, If_Z + # step[4] + def rewrite_cross_group_cnots(self): + + old_circ = self.step[3] + + # === 1. 构建新电路(仅复制量子比特)=== + new_circ = DQCCircuit(old_circ.qubits) + new_circ.qubit_group = old_circ.qubit_group + new_circ.qubit_tele = old_circ.qubit_tele + group_tele = {gid: self.step[3].qubit_tele[self.step[3].qubit_group.index(gid)] for gid in set(self.step[3].qubit_group)} + + # === 2. 继承所有经典寄存器 === + # 复制通信寄存器(如果有) + comm_cregs = [creg for creg in getattr(old_circ, "cregs", []) if "Tele" in creg.name] + for comm_creg in comm_cregs: + new_circ.add_register(ClassicalRegister(len(comm_creg), comm_creg.name)) + + # 复制原始寄存器(如果有) + orig_cregs = [creg for creg in getattr(old_circ, "cregs", []) if "Tele" not in creg.name] + for orig_creg in orig_cregs: + new_circ.add_register(ClassicalRegister(len(orig_creg), orig_creg.name)) + + + idx_counter = 0 # 全局 index 计数器 + + # === 4. 改写电路 === + for inst_obj in old_circ.data: + instr = inst_obj.operation + qargs = inst_obj.qubits + cargs = inst_obj.clbits + if instr.name == "cx": + ctrl, tgt = qargs + g_ctrl = new_circ.qubit_group[old_circ.get_index(ctrl)] + g_tgt = new_circ.qubit_group[old_circ.get_index(tgt)] + + if g_ctrl != g_tgt: + # ====== 跨组 CNOT,改写 ====== + ctrl_index = old_circ.get_index(ctrl) + tgt_index = old_circ.get_index(tgt) + ctrl_comm_index = old_circ.qubit_tele[ctrl_index] + tgt_comm_index = old_circ.qubit_tele[tgt_index] + + ctrl_comm = new_circ.qubits[ctrl_comm_index] + tgt_comm = new_circ.qubits[tgt_comm_index] + ctrl_q = new_circ.qubits[ctrl_index] + tgt_q = new_circ.qubits[tgt_index] + + # Reset + new_circ.append(Reset(), [ctrl_comm]) + new_circ.append(Reset(), [tgt_comm]) + + # RemoteGate + new_circ.append(RemoteGate(idx_counter, g_tgt), [ctrl_comm]) + new_circ.append(RemoteGate(idx_counter, g_ctrl), [tgt_comm]) + idx_counter += 1 + + # CNOT: ctrl->ctrl_comm, tgt_comm->tgt + new_circ.append(CXGate(), [ctrl_q, ctrl_comm]) + new_circ.append(CXGate(), [tgt_comm, tgt_q]) + + # H on tgt_comm + new_circ.append(HGate(), [tgt_comm]) + + # Measurement + If_Z + # Measurement + If_X + mz_inst = MZ(index=idx_counter, target=g_tgt) + mx_inst = MX(index=idx_counter, target=g_ctrl) + new_circ.append(mz_inst, [ctrl_q, ctrl_comm]) + new_circ.append(mx_inst, [tgt_comm, tgt_q]) + idx_counter += 1 + + self.Num_RemoteGate += 1 + else: + # ====== 同组 CNOT,保持原样 ====== + new_circ.append(instr, qargs, cargs) + elif instr.name == "measure": + # ====== 测量指令替换为 AnsM ====== + qarg = qargs[0] + carg = cargs[0] if cargs else None + + # 获取经典比特在其寄存器中的索引 + if carg is not None: + mea_index = old_circ.find_bit(carg).index + else: + mea_index = -1 # 没有关联经典比特 + + # 创建 AnsM 占位指令 + ansm_gate = AnsM(mea_index) + + # 添加到新电路 + new_circ.append(ansm_gate, [qarg]) + elif instr.name == "S_CX": + path = instr.path + ctrl, tgt = qargs + g_ctrl = new_circ.qubit_group[old_circ.get_index(ctrl)] + g_tgt = new_circ.qubit_group[old_circ.get_index(tgt)] + + ctrl_index = old_circ.get_index(ctrl) + tgt_index = old_circ.get_index(tgt) + ctrl_comm_index = old_circ.qubit_tele[ctrl_index] + tgt_comm_index = old_circ.qubit_tele[tgt_index] + + ctrl_q = new_circ.qubits[ctrl_index] + tgt_q = new_circ.qubits[tgt_index] + ctrl_comm = new_circ.qubits[ctrl_comm_index] + tgt_comm = new_circ.qubits[tgt_comm_index] + + new_circ.append(Reset(), [ctrl_comm_index]) + + for i in range(1, len(path) - 1): + mid_g = path[i] + tgt_g = path[i + 1] + + mid_comm_index_1 = group_tele[mid_g] + mid_comm_index_2 = mid_comm_index_1 + 1 + tgt_comm_index = group_tele[tgt_g] + + mid_comm_1 = new_circ.qubits[mid_comm_index_1] + mid_comm_2 = new_circ.qubits[mid_comm_index_2] + tgt_comm = new_circ.qubits[tgt_comm_index] + + if i == 1: + # Reset + new_circ.append(Reset(), [mid_comm_1]) + new_circ.append(Reset(), [mid_comm_2]) + new_circ.append(Reset(), [tgt_comm]) + + # RemoteGate + new_circ.append(RemoteGate(idx_counter, mid_g), [ctrl_comm]) + new_circ.append(RemoteGate(idx_counter, g_ctrl), [mid_comm_1]) + idx_counter += 1 + + new_circ.append(RemoteGate(idx_counter, tgt_g), [mid_comm_2]) + new_circ.append(RemoteGate(idx_counter, mid_g), [tgt_comm]) + idx_counter += 1 + + new_circ.append(CXGate(), [mid_comm_1, mid_comm_2]) + new_circ.append(HGate(), [mid_comm_1]) + + new_circ.append(IF_Z(index=idx_counter, target=mid_g), [ctrl_comm_index]) + new_circ.append(MS(index=idx_counter, target=tgt_g), [mid_comm_1, mid_comm_2]) + new_circ.append(IF_X(index=idx_counter, target=g_ctrl), [tgt_comm_index]) + idx_counter += 1 + else: + new_circ.append(Reset(), [mid_comm_2]) + new_circ.append(Reset(), [tgt_comm]) + + # RemoteGate + new_circ.append(RemoteGate(idx_counter, tgt_g), [mid_comm_2]) + new_circ.append(RemoteGate(idx_counter, mid_g), [tgt_comm]) + idx_counter += 1 + + new_circ.append(CXGate(), [mid_comm_1, mid_comm_2]) + new_circ.append(HGate(), [mid_comm_1]) + + new_circ.append(IF_Z(index=idx_counter, target=mid_g), [ctrl_comm_index]) + new_circ.append(MS(index=idx_counter, target=tgt_g), [mid_comm_1, mid_comm_2]) + new_circ.append(IF_X(index=idx_counter, target=g_ctrl), [tgt_comm_index]) + idx_counter += 1 + + + # CNOT: ctrl->ctrl_comm, tgt_comm->tgt + new_circ.append(CXGate(), [ctrl_q, ctrl_comm]) + new_circ.append(CXGate(), [tgt_comm, tgt_q]) + + # H on tgt_comm + new_circ.append(HGate(), [tgt_comm]) + + # Measurement + If_Z + # Measurement + If_X + mz_inst = MZ(index=idx_counter, target=g_tgt) + mx_inst = MX(index=idx_counter, target=g_ctrl) + new_circ.append(mz_inst, [ctrl_q, ctrl_comm]) + new_circ.append(mx_inst, [tgt_comm, tgt_q]) + idx_counter += 1 + else: + # 不是 CNOT 和 Mesurement,保持原样 + new_circ.append(instr, qargs, cargs) + + self.step.append(new_circ) + return new_circ + + # Physically split the circuit into sub-circuits based on partition + def physic_split(self): + config = [len(group) + 1 for group in self.partition] + for gid, flag in enumerate(self.Entanglement_swapping): + if flag == 1: + config[gid] += 1 + + temp_circuit = self.step[4] + sub_circuits = [] + + if all(isinstance(x, int) for x in config): + # 按数量顺序分割, 例如 [2,2,3] + start = 0 + for s in config: + sub = DQCCircuit(s) + sub.qubit_group = temp_circuit.qubit_group[start:start+s] + + # 给子电路加 classical bits + num_clbits = (len(config) - 1) * 2 + if num_clbits > 0: + creg = ClassicalRegister(num_clbits, "c") + sub.add_register(creg) + + # 复制原电路对应 qubit 的操作 + for instr in temp_circuit.data: + qubit_indices = [temp_circuit.get_index(q) for q in instr.qubits] + # 只保留属于子电路的 qubit + indices_in_sub = [i for i, qi in enumerate(qubit_indices) if start <= qi < start+s] + if indices_in_sub: + new_qargs = [sub.qubits[qi - start] for i, qi in enumerate(qubit_indices) if i in indices_in_sub] + new_cargs = [] + # print(f"Appending instruction {instr.operation.name} to sub-circuit with qubits {new_qargs}") + sub.append(instr.operation, new_qargs, new_cargs) + + sub_circuits.append(sub) + start += s + + elif all(isinstance(x, (list, tuple)) for x in config): + # 按指定索引组合, 例如 [[1,3],[0,2,4]] + for indices in config: + sub = DQCCircuit(len(indices)) + sub.qubit_type = [self.qubit_type[i] for i in indices] + + for instr in self.data: + qubit_indices = [self.get_index(q) for q in instr.qubits] + # 只保留在 indices 中的 qubit + indices_in_sub = [i for i, qi in enumerate(qubit_indices) if qi in indices] + if indices_in_sub: + new_qargs = [sub.qubits[indices.index(qi)] for i, qi in enumerate(qubit_indices) if i in indices_in_sub] + new_cargs = [] + sub.append(instr.operation, new_qargs, new_cargs) + + sub_circuits.append(sub) + + else: + raise ValueError("Config must be a list of ints or a list of lists of ints") + + self.sub_circuit = sub_circuits + return sub_circuits + + # Protect custom instructions with barriers + def protect_custom_instructions(self, subcirc): + """ + 为 RemoteGate、Measurement、If_X、If_Z、AnsM 添加 barrier 保护。 + """ + new_circ = DQCCircuit(*subcirc.qregs, *subcirc.cregs) + + for inst_obj in subcirc.data: + inst = inst_obj.operation + qargs = inst_obj.qubits + cargs = inst_obj.clbits + + if isinstance(inst, (MX, MZ, AnsM, IF_Z, IF_X, MS, S_CX)): # 在同样的 qubits 上加 barrier + new_circ.barrier(*qargs) + new_circ.append(inst, qargs, cargs) + new_circ.barrier(*qargs) + # print(f"[Info] Protecting instruction: {inst.name} on qubits {[q for q in qargs]}") + elif isinstance(inst, (RemoteGate)): + new_circ.barrier() + new_circ.append(inst, qargs, cargs) + new_circ.barrier() + else: + new_circ.append(inst, qargs, cargs) + + return new_circ + + # Restore custom instructions by removing barriers + def restore_custom_instructions(self, subcirc): + """ + 还原 RemoteGate、Measurement、If_X、If_Z、AnsM 的 barrier 保护。 + """ + new_circ = DQCCircuit(*subcirc.qregs, *subcirc.cregs) + + for inst_obj in subcirc.data: + inst = inst_obj.operation + qargs = inst_obj.qubits + cargs = inst_obj.clbits + + if inst.name != "barrier": + new_circ.append(inst, qargs, cargs) + # if isinstance(inst, (RemoteGate, MX, MZ, AnsM)): + # print(f"[Info] Restoring instruction: {inst.name} on qubits {[q for q in qargs]}") + return new_circ + + def transpile_subcircuits(self, qpus, layout_out=None): + """ + Transpile each sub-circuit using the corresponding QPU backend target, + and optional layout mapping per subcircuit. + + :param qpus: list of QPU instances (each having .backend and .target) + :param layout_out: list of layouts; each layout is a list of physical qubit indices + corresponding to the sub-circuit's logical qubits + e.g. [[0,1,2,3], [5,6,7,8]] + """ + # ---- 按 qpu_id 排序 ---- + self.qpus = sorted(qpus, key=lambda x: x.qpu_id) + + if not self.sub_circuit: + raise ValueError("No sub-circuits found. Please populate self.sub_circuit first.") + if len(qpus) != len(self.sub_circuit): + raise ValueError( + f"Number of QPUs ({len(qpus)}) does not match number of sub-circuits ({len(self.sub_circuit)})." + ) + + # ---- 检查 layout_out ---- + if layout_out is not None: + if len(layout_out) != len(self.sub_circuit): + raise ValueError("layout_out length must match number of sub-circuits.") + + # ---- 检查子电路规模是否符合后端限制 ---- + for idx, (sub_circ, qpu) in enumerate(zip(self.sub_circuit, qpus)): + backend = qpu.backend + size = sub_circ.num_qubits + qpu_id = getattr(qpu, "qpu_id", idx) + backend_name = getattr(backend, "name", "unknown") + + if size > backend.num_qubits: + raise ValueError( + f"QPU {qpu_id}: requested size={size} exceeds " + f"the maximum number of qubits ({backend.num_qubits}) " + f"supported by backend {backend_name}." + ) + + self.sub_circuit_trans = [] + + # ---- 对每个子电路执行 transpile ---- + for idx, (sub, qpu) in enumerate(zip(self.sub_circuit, qpus)): + try: + # Step 1: 保护自定义指令 + sub = self.protect_custom_instructions(sub) + + layout = None + if layout_out is not None and idx < len(layout_out): + layout = layout_out[idx] + + # Step 3: 调用 transpile + if layout is not None: + sub = transpile( + sub, + backend=qpu.backend, + initial_layout=layout, + optimization_level=3, + ) + else: + sub = transpile( + sub, + backend=qpu.backend, + optimization_level=3, + ) + + # Step 4: 还原自定义指令 + sub = self.restore_custom_instructions(sub) + + self.sub_circuit_trans.append(sub) + print(f"[Info] Sub-circuit {idx} transpiled successfully on {qpu.backend.name}.") + + except Exception as e: + print(f"[Error] Failed to transpile sub-circuit {idx} on {qpu.backend.name}: {e}") + self.sub_circuit_trans.append(None) + + return self.sub_circuit_trans + + # Merge the transpiled sub-circuits into a complete circuit + def merge_trans_circuits(self, comm_noise = False): + """ + 将 self.sub_circuit_trans 中的子电路组合成完整电路。 + 支持跨电路配对操作 (R, M+IF_X/IF_Z), 通过栈实现返回上层逻辑。 + 双向配对:无论先遇到 M 还是 IF_X/IF_Z 都能触发配对。 + """ + # === 构建 qubit / cbit 映射 === + qubits_map = {} + merged_qubits_map = {} + cbits_map = {} + global_q_index = 0 + global_c_index = 0 + for i, sub in enumerate(self.sub_circuit_trans): + if sub is None: + continue + local_indices = {sub.find_bit(q).index for instr in sub.data for q in instr.qubits} + for local_index in sorted(local_indices): + qubits_map[(i, local_index)] = global_q_index + merged_qubits_map[global_q_index] = (i, local_index) + global_q_index += 1 + + # Record index for noise model + self.merged_qubits_map = merged_qubits_map + self.merged_qubits_map_reverse = qubits_map + + num_sub = len(self.sub_circuit_trans) + for i in range(num_sub): + for j in range(num_sub): + if i != j: + cbits_map[(i, j)] = global_c_index + global_c_index += 1 + + # === 初始化变量 === + # 创建新电路(只初始化量子比特) + new_circ = DQCCircuit(len(qubits_map)) + new_circ.qubit_group = self.step[1].qubit_group + + # === 1. 添加通信寄存器 === + tele_creg = ClassicalRegister(len(cbits_map), "Tele") + new_circ.add_register(tele_creg) + + # === 2. 保留原始经典寄存器 === + if hasattr(self, "clbits") and self.clbits: + if hasattr(self, "cregs") and self.cregs: + orig_name = self.cregs[0].name + else: + orig_name = "c" + orig_creg = ClassicalRegister(len(self.clbits), orig_name) + new_circ.add_register(orig_creg) + + indices = [0] * len(self.sub_circuit_trans) + paired_op = {} # {index: (sub_index, instr)} + paired_op2 = {} + paired_done = set() # 已完成配对 + call_stack = [] + now = 0 + step = 0 + + # 条件操作函数 + def apply_conditional_gate(gate_list, qubit, clbit): + with new_circ.if_test((clbit, 1)): + for instr in gate_list: + new_circ.append(instr.operation, [qubit], instr.clbits) + + while True: + done = all(sub is None or indices[i] >= len(sub.data) for i, sub in enumerate(self.sub_circuit_trans)) + if done: + break + + sub = self.sub_circuit_trans[now] + if sub is None or indices[now] >= len(sub.data): + if call_stack: + now = call_stack.pop() + continue + else: + now = (now + 1) % len(self.sub_circuit_trans) + continue + + instr = sub.data[indices[now]] + inst = instr.operation + op_name = inst.name.upper() + local_indices = [sub.find_bit(q).index for q in instr.qubits] + global_qs = [qubits_map[(now, idx)] for idx in local_indices] + target = getattr(inst, "target", None) + idx = getattr(inst, "index", None) + mea = getattr(inst,"mea", None) + + # print(f"\n[STEP] now={now}, target={target},idx={idx}, inst={inst.name}, indices={indices}") + + # === R 门配对生成 Bell 态 === + if op_name == "R" and idx not in paired_done: + target_sub = self.sub_circuit_trans[target] if target is not None else None + + if idx not in paired_op: + paired_op[idx] = now + if target_sub: + call_stack.append(now) + now = target + continue + else: + first_now = paired_op.pop(idx) + first_sub = self.sub_circuit_trans[first_now] + first_instr = first_sub.data[indices[first_now]] + first_qs = [qubits_map[(first_now, first_sub.find_bit(q).index)] for q in first_instr.qubits] + target_qs = global_qs + # print(first_now,now,first_qs,target_qs) + new_circ.initialize([1/np.sqrt(2),0,0,1/np.sqrt(2)], first_qs + target_qs) + if comm_noise: + noise_instr = self.qpugroup.get_noise_instruction(first_now, now) + # 插入噪声,映射到新电路 qubit + if isinstance(target_qs, list): + # 对列表中所有量子比特加噪声 + noise_qargs = [new_circ.qubits[i] for i in target_qs] + else: + # 对单个 qubit 加噪声 + noise_qargs = [new_circ.qubits[target_qs]] + + if noise_instr is not None: + new_circ.append(noise_instr, noise_qargs) + + paired_done.add(idx) + indices[now] += 1 + indices[first_now] += 1 + now = call_stack.pop() if call_stack else now + continue + + # === M / IF_X / IF_Z 双向配对 === + elif op_name in ("MX", "MZ") and idx not in paired_done: + if idx not in paired_op: + paired_op[idx] = now + if target_sub: + call_stack.append(now) + now = target + continue + + # 已经配对 + first_now = paired_op.pop(idx) + first_sub = self.sub_circuit_trans[first_now] + first_instr = first_sub.data[indices[first_now]] + + first_qs = [qubits_map[(first_now, first_sub.find_bit(q).index)] + for q in first_instr.qubits] + target_qs = global_qs + + # classical bit 对应 + cbit_idx1 = cbits_map[(now, first_now)] + cbit_idx2 = cbits_map[(first_now, now)] + clbit_obj1 = new_circ.clbits[cbit_idx1] + clbit_obj2 = new_circ.clbits[cbit_idx2] + + + if first_instr.name == "MX": + new_circ.measure(first_qs[0], clbit_obj1) + apply_conditional_gate(self.qpus[now].compile_z_gate(), target_qs[0], clbit_obj1) + new_circ.measure(target_qs[1], clbit_obj2) + apply_conditional_gate(self.qpus[first_now].compile_x_gate(), first_qs[1], clbit_obj2) + + else: + new_circ.measure(first_qs[1], clbit_obj1) + apply_conditional_gate(self.qpus[now].compile_x_gate(), target_qs[1], clbit_obj1) + new_circ.measure(target_qs[0], clbit_obj2) + apply_conditional_gate(self.qpus[first_now].compile_z_gate(), first_qs[0], clbit_obj2) + + + # 更新状态 + paired_done.add(idx) + indices[now] += 1 + indices[first_now] += 1 + now = call_stack.pop() if call_stack else now + continue + elif op_name == "ANS_M": + # global_qs[0] 是量子比特索引,mea 是存储的经典比特索引 + q_idx = global_qs[0] + c_idx = mea # mea 已经是全局经典比特索引 + + # 在 new_circ 中找到对应的 Clbit 对象 + clbit_obj = None + temp_idx = c_idx + for creg in new_circ.cregs: + if temp_idx < len(creg): + clbit_obj = creg[temp_idx] + break + else: + temp_idx -= len(creg) + + if clbit_obj is None: + raise ValueError(f"Cannot find the classical bit index {c_idx} corresponding to a Clbit") + + # 添加测量操作 + new_circ.measure(new_circ.qubits[q_idx], clbit_obj) + indices[now] += 1 + continue + elif op_name in ("IF_X", "IF_Z", "MS") and idx not in paired_done: + target_sub = self.sub_circuit_trans[target] if target is not None else None + if idx not in paired_op: + paired_op[idx] = now + if target_sub: + call_stack.append(now) + now = target + continue + if idx not in paired_op2: + paired_op2[idx] = now + if target_sub: + call_stack.append(now) + now = target + continue + + # 已经配对 + first_now = paired_op.pop(idx) + first_sub = self.sub_circuit_trans[first_now] + first_instr = first_sub.data[indices[first_now]] + + second_now = paired_op2.pop(idx) + second_sub = self.sub_circuit_trans[second_now] + second_instr = second_sub.data[indices[second_now]] + + first_qs = [qubits_map[(first_now, first_sub.find_bit(q).index)] + for q in first_instr.qubits] + + second_qs = [qubits_map[(second_now, second_sub.find_bit(q).index)] + for q in second_instr.qubits] + + # instr global_qs + third_qs = [qubits_map[(now, self.sub_circuit_trans[now].find_bit(q).index)] + for q in instr.qubits] + + # classical bit 对应 + cbit_idx1 = cbits_map[(first_now, second_now)] + cbit_idx2 = cbits_map[(second_now, first_now)] + cbit_idx3 = cbits_map[(second_now, now)] + cbit_idx4 = cbits_map[(now, second_now)] + cbit_idx5 = cbits_map[(now, first_now)] + cbit_idx6 = cbits_map[(first_now, now)] + + clbit_obj1 = new_circ.clbits[cbit_idx1] + clbit_obj2 = new_circ.clbits[cbit_idx2] + clbit_obj3 = new_circ.clbits[cbit_idx3] + clbit_obj4 = new_circ.clbits[cbit_idx4] + clbit_obj5 = new_circ.clbits[cbit_idx5] + clbit_obj6 = new_circ.clbits[cbit_idx6] + # print("Entanglement Swapping", first_instr.name,second_instr.name, instr.name) + if first_instr.name == "IF_Z": + new_circ.measure(second_qs[0], clbit_obj2) + apply_conditional_gate(self.qpus[first_now].compile_z_gate(), first_qs, clbit_obj2) + new_circ.measure(second_qs[1], clbit_obj3) + apply_conditional_gate(self.qpus[now].compile_x_gate(), third_qs, clbit_obj3) + elif first_instr.name == "MS": + new_circ.measure(first_qs[0], clbit_obj6) + apply_conditional_gate(self.qpus[now].compile_z_gate(), third_qs, clbit_obj6) + new_circ.measure(first_qs[1], clbit_obj1) + apply_conditional_gate(self.qpus[second_now].compile_x_gate(), second_qs, clbit_obj1) + elif first_instr.name == "IF_X": + new_circ.measure(third_qs[0], clbit_obj4) + apply_conditional_gate(self.qpus[second_now].compile_z_gate(), second_qs, clbit_obj4) + new_circ.measure(third_qs[1], clbit_obj5) + apply_conditional_gate(self.qpus[first_now].compile_x_gate(), first_qs, clbit_obj5) + # 更新状态 + paired_done.add(idx) + indices[now] += 1 + indices[first_now] += 1 + indices[second_now] += 1 + now = call_stack.pop() if call_stack else now + now = call_stack.pop() if call_stack else now + continue + # === 普通门 === + else: + new_circ.append(inst, global_qs, []) + indices[now] += 1 + continue + + step += 1 + if step > 50000: + raise RuntimeError(f"[Error] Possible deadlock. indices={indices}, now={now}, stack={call_stack}") + + self.result_circuit = new_circ + self.result_circuit.qubit_group = self.step[4].qubit_group + return new_circ + + def decompose_and_get_data(self, instr: CircuitInstruction, basis_gates=None): + """ + 递归分解单个指令到基础门集。 + + 参数: + instr: 待分解的 CircuitInstruction + basis_gates: 允许的基础门名称集合(默认: {'cx'} + 单比特门) + + 返回: + 保留原始 qubit 引用的 CircuitInstruction 列表(可直接用于 circuit.data) + """ + if basis_gates is None: + # 定义基础门集合 + basis_gates = {'cx', 'u3', 'u2', 'u1', 'id', 'x', 'y', 'z', + 'h', 's', 't', 'rx', 'ry', 'rz', 'sx', 'p'} + + # 如果是基础门或没有定义,直接返回 + if (not hasattr(instr.operation, "definition") or + instr.operation.name in basis_gates): + return [instr] + + # 创建临时电路进行分解 + n_qubits = len(instr.qubits) + qc_temp = QuantumCircuit(n_qubits) + qc_temp.append(instr.operation, list(range(n_qubits))) + + # 建立临时 qubit 到原始 qubit 的映射 + # 临时电路的 qc_temp.qubits[i] 对应原始的 instr.qubits[i] + temp_to_orig = {qc_temp.qubits[i]: instr.qubits[i] for i in range(n_qubits)} + + # 分解一次 + decomposed = qc_temp.decompose() + + result = [] + for sub_instr in decomposed.data: + # 使用映射字典将临时 qubit 对象映射回原始 qubit 对象 + mapped_qubits = [temp_to_orig[q] for q in sub_instr.qubits] + + # 创建新的指令 + new_instr = CircuitInstruction( + sub_instr.operation, + mapped_qubits, + sub_instr.clbits + ) + + # 递归分解(如果还需要) + result.extend(self.decompose_and_get_data(new_instr, basis_gates)) + + return result + + def reduce_noise_model(self, subset_qubits, noise_model=None, coupling_map=None): + """ + 裁剪 NoiseModel,使其只保留 subset_qubits 上的噪声,同时保留 basis_gates、description 和可选耦合图。 + + 参数: + subset_qubits: list[int] + 需要保留噪声的量子比特 + noise_model: NoiseModel, 可选 + 如果不提供,则使用 self.backend_noise_model + coupling_map: list[tuple], 可选 + 如果提供,则裁剪耦合图 + + 返回: + NoiseModel + """ + + if noise_model is None: + if getattr(self, "backend_noise_model", None) is None: + raise ValueError("No noise model provided or set in self.backend_noise_model.") + noise_model = self.backend_noise_model + + # 如果 Qiskit 版本支持 reduce(),优先使用 + if hasattr(noise_model, "reduce"): + return noise_model.reduce(subset_qubits) + + sub_model = NoiseModel() + + # --- 1️⃣ 保留量子门噪声 --- + for instr_name, qerrors in noise_model._local_quantum_errors.items(): + for qubits, error in qerrors.items(): + if all(q in subset_qubits for q in qubits): + sub_model.add_quantum_error(error, instr_name, qubits) + + # --- 2️⃣ 保留读出噪声 (measure) --- + for qubit, error in noise_model._local_readout_errors.items(): + if qubit[0] in subset_qubits: # qubit 是 tuple,如 (0,) + sub_model.add_readout_error(error, [qubit[0]]) + + # --- 3️⃣ 保留 basis_gates 和 description --- + if hasattr(noise_model, "basis_gates"): + sub_model._basis_gates = list(noise_model.basis_gates) + if hasattr(noise_model, "description"): + sub_model._description = noise_model.description + + # --- 4️⃣ 可选裁剪耦合图 --- + target_coupling_map = coupling_map if coupling_map is not None else getattr(self, "coupling_map", None) + if target_coupling_map is not None: + sub_model._coupling_map = [edge for edge in target_coupling_map if all(q in subset_qubits for q in edge)] + else: + sub_model._coupling_map = None + + return sub_model + + # Get a combined noise model for the distributed circuit based on QPU backends + def get_noise_model(self): + """ + 构建一个综合噪声模型 (combined_noise_model), + 将多个 QPU 的噪声模型裁剪后合并,并映射到全局连续 qubit。 + """ + # === Step 1: 参数检查 === + qpus = self.qpugroup.qpus + qpus = sorted(qpus, key=lambda x: x.qpu_id) + if not self.sub_circuit: + raise ValueError("No sub-circuits found.") + if len(qpus) != len(self.sub_circuit): + raise ValueError( + f"Number of QPUs ({len(qpus)}) does not match sub-circuits ({len(self.sub_circuit)})." + ) + + # === Step 2: 提取每个子电路对应的局部 qubit === + sub_qubit_maps = [] + for idx, _ in enumerate(self.sub_circuit): + local_qubits = [ + local_index + for global_q, (sub_index, local_index) in self.merged_qubits_map.items() + if sub_index == idx + ] + sub_qubit_maps.append(local_qubits) + + # === Step 3: 构建全局 qubit 映射 (sub_idx, local_qubit) -> global_qubit === + qubit_mapping = self.merged_qubits_map_reverse + + # === Step 4: 初始化全局 NoiseModel === + combined_noise_model = NoiseModel() + combined_basis_gates = set() + + # === Step 5: 对每个 QPU 裁剪并合并噪声 === + for idx, qpu in enumerate(qpus): + backend_noise = NoiseModel.from_backend(qpu.backend) + local_qubits = sub_qubit_maps[idx] + + # --- 5a. 裁剪局部噪声 --- + reduced_noise = self.reduce_noise_model( + subset_qubits=local_qubits, + noise_model=backend_noise, + coupling_map=qpu.backend.coupling_map + ) + + # --- 5b. 合并量子门噪声 --- + for instr_name, qerrors in reduced_noise._local_quantum_errors.items(): + for qubits_tuple, error in qerrors.items(): + # 解包 qubit + qubit_indices = [q if isinstance(q, int) else q[0] for q in qubits_tuple] + global_qubits = tuple(qubit_mapping[(idx, q)] for q in qubit_indices) + combined_noise_model.add_quantum_error(error, instr_name, global_qubits) + # print(f"Added quantum error: {instr_name}, local {qubit_indices} -> global {global_qubits}") + + # --- 5c. 合并读出噪声 --- + for qubit, error in reduced_noise._local_readout_errors.items(): + q_local = qubit[0] if isinstance(qubit, tuple) else qubit + if (idx, q_local) not in qubit_mapping: + print(f"⚠️ Warning: qubit mapping missing for {(idx, q_local)}") + continue + global_qubit = qubit_mapping[(idx, q_local)] + combined_noise_model.add_readout_error(error, [global_qubit]) + # print(f"Added readout error: local {q_local} -> global {global_qubit}") + + # --- 5d. 汇总 basis_gates --- + if hasattr(reduced_noise, "basis_gates"): + combined_basis_gates.update(reduced_noise.basis_gates) + + # === Step 6: 写入全局基础门、描述信息 === + combined_noise_model._basis_gates = list(combined_basis_gates) + combined_noise_model._description = "Combined noise model from multiple QPUs" + + return combined_noise_model + +def detect_swap_pattern(circuit): + swap_count = 0 + instructions = list(circuit.data) + + i = 0 + while i < len(instructions) - 2: + inst1 = instructions[i] + inst2 = instructions[i + 1] + inst3 = instructions[i + 2] + + # 检查是否都是 CX 门 + if (inst1.operation.name == 'cx' and + inst2.operation.name == 'cx' and + inst3.operation.name == 'cx'): + + # 获取量子比特索引 + q1_0 = circuit.find_bit(inst1.qubits[0]).index + q1_1 = circuit.find_bit(inst1.qubits[1]).index + + q2_0 = circuit.find_bit(inst2.qubits[0]).index + q2_1 = circuit.find_bit(inst2.qubits[1]).index + + q3_0 = circuit.find_bit(inst3.qubits[0]).index + q3_1 = circuit.find_bit(inst3.qubits[1]).index + + # 检查 SWAP 模式: CX(a,b), CX(b,a), CX(a,b) + if (q1_0 == q3_0 and q1_1 == q3_1 and # 第1和第3个相同 + q2_0 == q1_1 and q2_1 == q1_0): # 第2个是反向的 + swap_count += 1 + print(f" 检测到 SWAP 模式在位置 {i}: q{q1_0} ↔ q{q1_1}") + i += 3 # 跳过这3个门 + continue + + i += 1 + + print("\n" + "=" * 70) + print("检测 CNOT 模式") + print("=" * 70) + print(f"检测到的 SWAP 数量: {swap_count}") + + return swap_count diff --git a/src/dqc_simulator/features.py b/src/dqc_simulator/features.py new file mode 100644 index 0000000..a576fbe --- /dev/null +++ b/src/dqc_simulator/features.py @@ -0,0 +1,395 @@ +from collections import Counter, defaultdict +from statistics import mean, pstdev + +from qiskit import transpile +from qiskit.converters import circuit_to_dag +from qiskit.dagcircuit import DAGOpNode + + +def safe_stats(values): + """Return simple statistics for a numeric list.""" + if not values: + return { + "count": 0, + "sum": 0.0, + "mean": 0.0, + "max": 0.0, + "min": 0.0, + "std": 0.0, + } + + return { + "count": len(values), + "sum": float(sum(values)), + "mean": float(mean(values)), + "max": float(max(values)), + "min": float(min(values)), + "std": float(pstdev(values)) if len(values) > 1 else 0.0, + } + + +def get_inst_props(target, op_name, qargs): + """ + Safely read instruction properties from backend.target. + Returns (error, duration), both may be None. + """ + try: + if op_name not in target: + return None, None + props_map = target[op_name] + props = props_map.get(tuple(qargs), None) + except Exception: + props = None + + if props is None: + return None, None + + err = getattr(props, "error", None) + dur = getattr(props, "duration", None) + return err, dur + + +def kraus_comm_noise_strength(op): + """ + Convert a Kraus instruction into a scalar communication-noise strength. + Uses: error = 1 - entanglement_fidelity_to_identity. + """ + try: + kraus_ops = list(getattr(op, "params", [])) + if not kraus_ops: + return None + + dim = kraus_ops[0].shape[0] + if dim <= 0: + return None + + fe = sum(abs(k.trace()) ** 2 for k in kraus_ops) / (dim * dim) + err = 1.0 - float(fe.real) + if err < 0: + return 0.0 + if err > 1: + return 1.0 + return err + except Exception: + return None + + +def circuit_metrics(circ): + """Basic circuit-level metrics.""" + two_qubit_gates = sum( + 1 + for inst in circ.data + if len(inst.qubits) == 2 and inst.operation.name != "barrier" + ) + + measure_count = sum(1 for inst in circ.data if inst.operation.name == "measure") + comm_count = sum(1 for inst in circ.data if inst.operation.name == "kraus") + one_qubit_gates = sum( + 1 + for inst in circ.data + if len(inst.qubits) == 1 and inst.operation.name not in ("measure", "barrier") + ) + + return { + "num_qubits": circ.num_qubits, + "depth": circ.depth(), + "one_qubit_gate_count": one_qubit_gates, + "two_qubit_gate_count": two_qubit_gates, + "measure_count": measure_count, + "comm_count": comm_count, + } + + +def dag_metrics(dag): + """Extract selected DAG-related features.""" + op_nodes = list(dag.op_nodes()) + + qubit_activity = Counter() + for node in op_nodes: + for q in node.qargs: + qubit_activity[str(q)] += 1 + + longest_path_nodes = [node for node in dag.longest_path() if isinstance(node, DAGOpNode)] + + critical_path_2q_count = sum(1 for node in longest_path_nodes if len(node.qargs) == 2) + critical_path_1q_count = sum( + 1 + for node in longest_path_nodes + if len(node.qargs) == 1 and node.op.name != "measure" + ) + + busy_values = list(qubit_activity.values()) + busy_stats = safe_stats(busy_values) + + try: + dag_size = dag.size(recurse=True) + except Exception: + try: + dag_size = dag.size() + except Exception: + dag_size = len(op_nodes) + + return { + "critical_path_1q_count": critical_path_1q_count, + "critical_path_2q_count": critical_path_2q_count, + "critical_path_ratio": len(longest_path_nodes) / dag_size if dag_size else 0.0, + "qubit_activity_mean": busy_stats["mean"], + "qubit_activity_max": busy_stats["max"], + } + + +def noise_time_metrics(circ, backend=None): + """ + Extract selected noise- and time-related features from a transpiled circuit. + Assumes `circ` is already transpiled for `backend`. + """ + target = backend.target if backend is not None else None + + one_q_errors = [] + two_q_errors = [] + meas_errors = [] + comm_noise_errors = [] + + two_q_durations = [] + op_error_records = [] + qubit_gate_time = defaultdict(float) + qubit_gate_error_sum = defaultdict(float) + used_physical_qubits = set() + + for inst in circ.data: + op = inst.operation + name = op.name + + if name == "barrier": + continue + + qargs = [circ.find_bit(q).index for q in inst.qubits] + for q in qargs: + used_physical_qubits.add(q) + + if target is not None: + err, dur = get_inst_props(target, name, qargs) + else: + # Fallback for backend-free usage on transpiled circuits. + err = getattr(op, "error", None) + dur = getattr(op, "duration", None) + + if err is not None: + op_error_records.append(err) + for q in qargs: + qubit_gate_error_sum[q] += err + + if dur is not None: + for q in qargs: + qubit_gate_time[q] += dur + + if name == "measure": + if err is not None: + meas_errors.append(err) + + if name == "kraus": + comm_err = kraus_comm_noise_strength(op) + if comm_err is not None: + comm_noise_errors.append(comm_err) + + elif len(qargs) == 1: + if err is not None: + one_q_errors.append(err) + + elif len(qargs) == 2: + if err is not None: + two_q_errors.append(err) + if dur is not None: + two_q_durations.append(dur) + + try: + estimated_duration = ( + circ.estimate_duration(target=target) + if target is not None + else circ.estimate_duration() + ) + except Exception: + estimated_duration = None + + t1_ratios = [] + t2_ratios = [] + if backend is not None: + for q in sorted(used_physical_qubits): + try: + qp = backend.qubit_properties(q) + except Exception: + qp = None + + t1 = getattr(qp, "t1", None) if qp is not None else None + t2 = getattr(qp, "t2", None) if qp is not None else None + acc_time = qubit_gate_time.get(q, 0.0) + + if t1 is not None and t1 > 0: + t1_ratios.append(acc_time / t1) + if t2 is not None and t2 > 0: + t2_ratios.append(acc_time / t2) + + one_q_stats = safe_stats(one_q_errors) + two_q_stats = safe_stats(two_q_errors) + meas_stats = safe_stats(meas_errors) + comm_noise_stats = safe_stats(comm_noise_errors) + all_op_error_stats = safe_stats(op_error_records) + two_q_duration_stats = safe_stats(two_q_durations) + t1_ratio_stats = safe_stats(t1_ratios) + t2_ratio_stats = safe_stats(t2_ratios) + per_qubit_gate_err_stats = safe_stats(list(qubit_gate_error_sum.values())) + per_qubit_gate_time_stats = safe_stats(list(qubit_gate_time.values())) + + return { + "one_q_error_sum": one_q_stats["sum"], + "one_q_error_mean": one_q_stats["mean"], + "one_q_error_max": one_q_stats["max"], + "one_q_error_std": one_q_stats["std"], + "two_q_error_sum": two_q_stats["sum"], + "two_q_error_mean": two_q_stats["mean"], + "two_q_error_max": two_q_stats["max"], + "two_q_error_std": two_q_stats["std"], + "meas_error_sum": meas_stats["sum"], + "meas_error_mean": meas_stats["mean"], + "meas_error_max": meas_stats["max"], + "meas_error_std": meas_stats["std"], + "comm_noise_mean": comm_noise_stats["mean"], + "comm_noise_max": comm_noise_stats["max"], + "comm_noise_min": comm_noise_stats["min"], + "comm_noise_std": comm_noise_stats["std"], + "all_op_error_sum": all_op_error_stats["sum"], + "estimated_duration_sec": ( + estimated_duration if estimated_duration is not None else -1.0 + ), + "two_q_duration_sum": two_q_duration_stats["sum"], + "time_over_t1_mean": t1_ratio_stats["mean"], + "time_over_t1_max": t1_ratio_stats["max"], + "time_over_t2_mean": t2_ratio_stats["mean"], + "time_over_t2_max": t2_ratio_stats["max"], + "per_qubit_gate_error_max": per_qubit_gate_err_stats["max"], + "per_qubit_gate_time_max": per_qubit_gate_time_stats["max"], + } + + +def build_feature_groups(circuit_info, dag_info, noise_time_info): + group_a_basic_structure = { + "num_qubits": circuit_info["num_qubits"], + "depth": circuit_info["depth"], + "one_qubit_gate_count": circuit_info["one_qubit_gate_count"], + "two_qubit_gate_count": circuit_info["two_qubit_gate_count"], + "measure_count": circuit_info["measure_count"], + "comm_count": circuit_info["comm_count"], + } + + group_b_critical_path = { + "critical_path_1q_count": dag_info["critical_path_1q_count"], + "critical_path_2q_count": dag_info["critical_path_2q_count"], + "critical_path_ratio": dag_info["critical_path_ratio"], + } + + group_c_noise_strength = { + "one_q_error_sum": noise_time_info["one_q_error_sum"], + "one_q_error_mean": noise_time_info["one_q_error_mean"], + "one_q_error_max": noise_time_info["one_q_error_max"], + "one_q_error_std": noise_time_info["one_q_error_std"], + "two_q_error_sum": noise_time_info["two_q_error_sum"], + "two_q_error_mean": noise_time_info["two_q_error_mean"], + "two_q_error_max": noise_time_info["two_q_error_max"], + "two_q_error_std": noise_time_info["two_q_error_std"], + "meas_error_sum": noise_time_info["meas_error_sum"], + "meas_error_mean": noise_time_info["meas_error_mean"], + "meas_error_max": noise_time_info["meas_error_max"], + "meas_error_std": noise_time_info["meas_error_std"], + "comm_noise_mean": noise_time_info["comm_noise_mean"], + "comm_noise_max": noise_time_info["comm_noise_max"], + "comm_noise_min": noise_time_info["comm_noise_min"], + "comm_noise_std": noise_time_info["comm_noise_std"], + "all_op_error_sum": noise_time_info["all_op_error_sum"], + } + + group_d_time_decoherence = { + "estimated_duration_sec": noise_time_info["estimated_duration_sec"], + "two_q_duration_sum": noise_time_info["two_q_duration_sum"], + "time_over_t1_mean": noise_time_info["time_over_t1_mean"], + "time_over_t1_max": noise_time_info["time_over_t1_max"], + "time_over_t2_mean": noise_time_info["time_over_t2_mean"], + "time_over_t2_max": noise_time_info["time_over_t2_max"], + } + + group_e_load_distribution = { + "qubit_activity_mean": dag_info["qubit_activity_mean"], + "qubit_activity_max": dag_info["qubit_activity_max"], + "per_qubit_gate_error_max": noise_time_info["per_qubit_gate_error_max"], + "per_qubit_gate_time_max": noise_time_info["per_qubit_gate_time_max"], + } + + return { + "group_a_basic_structure": group_a_basic_structure, + "group_b_critical_path": group_b_critical_path, + "group_c_noise_strength": group_c_noise_strength, + "group_d_time_decoherence": group_d_time_decoherence, + "group_e_load_distribution": group_e_load_distribution, + } + + +def build_flattened_feature_vector(feature_groups): + flat = {} + for group_name in [ + "group_a_basic_structure", + "group_b_critical_path", + "group_c_noise_strength", + "group_d_time_decoherence", + "group_e_load_distribution", + ]: + flat.update(feature_groups[group_name]) + return flat + + +def extract_feature_bundle( + circuit, + backend=None, + transpile_first=False, + optimization_level=3, + seed_transpiler=7, +): + """ + Run the full feature extraction pipeline on a circuit. + Returns a dict with circuit_info/dag_info/noise_time_info/feature_groups/feature_vector. + """ + if transpile_first: + if backend is None: + raise ValueError("backend is required when transpile_first=True.") + target_circuit = transpile( + circuit, + backend=backend, + optimization_level=optimization_level, + seed_transpiler=seed_transpiler, + ) + else: + target_circuit = circuit + + dag = circuit_to_dag(target_circuit) + circuit_info = circuit_metrics(target_circuit) + dag_info = dag_metrics(dag) + noise_time_info = noise_time_metrics(target_circuit, backend) + feature_groups = build_feature_groups(circuit_info, dag_info, noise_time_info) + feature_vector = build_flattened_feature_vector(feature_groups) + + return { + "circuit": target_circuit, + "circuit_info": circuit_info, + "dag_info": dag_info, + "noise_time_info": noise_time_info, + "feature_groups": feature_groups, + "feature_vector": feature_vector, + } + + +def extract_feature_groups(circuit, backend=None): + """Convenience wrapper: return grouped features only.""" + return extract_feature_bundle(circuit, backend=backend)["feature_groups"] + + +def extract_feature_vector(circuit, backend=None): + """Convenience wrapper: return flattened feature vector only.""" + return extract_feature_bundle(circuit, backend=backend)["feature_vector"]