From f3d185187f41140a28b0b1a3761fd5826415ca19 Mon Sep 17 00:00:00 2001 From: tobi-forster Date: Thu, 18 Jul 2024 16:35:58 +0200 Subject: [PATCH 01/27] Changed to one file, but still problems with multiprocessing --- pyproject.toml | 4 +- res_satellite_solver.csv | 2 + .../equivalence_checker/executer.py | 143 ++++++++++++++++++ .../equivalence_checker/sampler.py | 88 +++++++++++ tests/test_equivalence_checking_grover.py | 90 +++++++++++ 5 files changed, 325 insertions(+), 2 deletions(-) create mode 100644 res_satellite_solver.csv create mode 100644 src/mqt/problemsolver/equivalence_checker/executer.py create mode 100644 src/mqt/problemsolver/equivalence_checker/sampler.py create mode 100644 tests/test_equivalence_checking_grover.py diff --git a/pyproject.toml b/pyproject.toml index 2c738dd..bf71e5d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -27,7 +27,8 @@ dependencies = [ "networkx", "python_tsp", "docplex", - "qiskit_optimization" + "qiskit_optimization", + "tweedledum==1.0.0" ] classifiers = [ @@ -41,7 +42,6 @@ classifiers = [ "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", - "Programming Language :: Python :: 3.11", "Intended Audience :: Science/Research", "Natural Language :: English", "Topic :: Scientific/Engineering :: Electronic Design Automation (EDA)", diff --git a/res_satellite_solver.csv b/res_satellite_solver.csv new file mode 100644 index 0000000..9695412 --- /dev/null +++ b/res_satellite_solver.csv @@ -0,0 +1,2 @@ +num_qubits,calculation_time_qaoa,calculation_time_wqaoa,calculation_time_vqe,success_rate_qaoa,success_rate_wqaoa,success_rate_vqe +3,0.3417479991912842,0.2661118507385254,0.12465500831604004,1.0,1.0,1.0 diff --git a/src/mqt/problemsolver/equivalence_checker/executer.py b/src/mqt/problemsolver/equivalence_checker/executer.py new file mode 100644 index 0000000..87b5c49 --- /dev/null +++ b/src/mqt/problemsolver/equivalence_checker/executer.py @@ -0,0 +1,143 @@ +from __future__ import annotations + +import multiprocessing +import string + +import numpy as np + +# from mqt.problemsolver.equivalence_checker import sampler + +from qiskit import QuantumCircuit +from qiskit.circuit.library import GroverOperator, PhaseOracle +from qiskit.compiler import transpile +from qiskit_aer import AerSimulator + +sim_counts = AerSimulator(method="statevector") + + +def sampler( + process_number: int, + return_dict: dict[int, str | int], + miter: str, + start_iterations: int, + shots: int, + delta: float, +) -> None: + """ + Runs the algorithm utilizing Grover's algorithm to look for elements satisfying the conditions. + + Parameters + ---------- + process_number : int + Index of the process + return_dict : dict[int, str | int] + Dictionary to share results with parent process + miter: str + String that contains the conditions to satisfy + start_iterations: int + Defines the number of Grover iterations to start with + shots: int + Number of shots the algorithm should run for + delta: float + Threshold parameter between 0 and 1 + """ + total_iterations = 0 + for iterations in reversed(range(1, start_iterations + 1)): + total_iterations += iterations + oracle = PhaseOracle(miter) + + operator = GroverOperator(oracle).decompose() + num_qubits = operator.num_qubits + total_num_combinations = 2**num_qubits + + qc = QuantumCircuit(num_qubits) + qc.h(list(range(num_qubits))) + qc.compose(operator.power(iterations).decompose(), inplace=True) + qc.measure_all() + + qc = transpile(qc, sim_counts) + + job = sim_counts.run(qc, shots=shots) + result = job.result() + counts_dict = dict(result.get_counts()) + counts_list = list(counts_dict.values()) + counts_list.sort(reverse=True) + + counts_dict = dict(sorted(counts_dict.items(), key=lambda item: item[1])[::-1]) # Sort state dictionary with respect to values (counts) + + stopping_condition = False + for i in range(round(total_num_combinations * 0.5)): + if (i + 1) == len(counts_list): + stopping_condition = True + targets_list = counts_list + targets_dict = { + list(counts_dict.keys())[t]: list(counts_dict.values())[t] for t in range(len(targets_list)) + } + target_states = list(targets_dict.keys()) + break + + diff = counts_list[i] - counts_list[i + 1] + if diff > counts_list[i] * delta: + stopping_condition = True + targets_list = counts_list[: i + 1] + targets_dict = { + list(counts_dict.keys())[t]: list(counts_dict.values())[t] for t in range(len(targets_list)) + } + target_states = list(targets_dict.keys()) + break + + if stopping_condition: + break + + for i, state in enumerate(target_states): + target_states[i] = state[::-1] # Compensate Qiskit's qubit ordering + + return_dict[process_number] = target_states + + +def run(miter: str, num_qubits:int, shots: int, delta: float, number_of_processes: int) -> list[str | int]: + """ + Runs the grover verification application in multiple processes. + + Parameters + ---------- + miter: str + String that contains the conditions to satisfy + num_qubits : int + Number of input bits + shots: int + Number of shots + delta: float + Threshold parameter between 0 and 1 + number_of_processes: int + Number of processes the algorithm should run in simultaneously + + Returns + ------- + list[str | int] + A list of values representing the targets found by the Grover algorithm + """ + try: + assert 0 <= delta <= 1 + except AssertionError: + print(f'Invalid delta of {delta}. It must be between 0 and 1.') + + total_num_combinations = 2**num_qubits + start_iterations = np.floor(np.pi / (4 * np.arcsin((1 / total_num_combinations) ** 0.5)) - 0.5).astype(int) + manager = multiprocessing.Manager() + return_dict = manager.dict() + jobs = [] + for i in range(number_of_processes): + process = multiprocessing.Process( + target=sampler, + args=(i, return_dict, miter, start_iterations, shots, delta), + ) + jobs.append(process) + process.start() + + for job in jobs: + job.join() + return return_dict.values() + +miter = "a & b" +targets = run(miter, 2, 32, 0.7, 1) \ No newline at end of file diff --git a/src/mqt/problemsolver/equivalence_checker/sampler.py b/src/mqt/problemsolver/equivalence_checker/sampler.py new file mode 100644 index 0000000..305711f --- /dev/null +++ b/src/mqt/problemsolver/equivalence_checker/sampler.py @@ -0,0 +1,88 @@ +from __future__ import annotations + +from qiskit import QuantumCircuit +from qiskit.circuit.library import GroverOperator, PhaseOracle +from qiskit.compiler import transpile +from qiskit_aer import AerSimulator + +sim_counts = AerSimulator(method="statevector") + + +def sampler( + process_number: int, + return_dict: dict[int, str | int], + miter: str, + start_iterations: int, + shots: int, + delta: float, +) -> None: + """ + Runs the algorithm utilizing Grover's algorithm to look for elements satisfying the conditions. + + Parameters + ---------- + process_number : int + Index of the process + return_dict : dict[int, str | int] + Dictionary to share results with parent process + miter: str + String that contains the conditions to satisfy + start_iterations: int + Defines the number of Grover iterations to start with + shots: int + Number of shots the algorithm should run for + delta: float + Threshold parameter between 0 and 1 + """ + total_iterations = 0 + for iterations in reversed(range(1, start_iterations + 1)): + total_iterations += iterations + oracle = PhaseOracle(miter) + + operator = GroverOperator(oracle).decompose() + num_qubits = operator.num_qubits + total_num_combinations = 2**num_qubits + + qc = QuantumCircuit(num_qubits) + qc.h(list(range(num_qubits))) + qc.compose(operator.power(iterations).decompose(), inplace=True) + qc.measure_all() + + qc = transpile(qc, sim_counts) + + job = sim_counts.run(qc, shots=shots) + result = job.result() + counts_dict = dict(result.get_counts()) + counts_list = list(counts_dict.values()) + counts_list.sort(reverse=True) + + counts_dict = dict(sorted(counts_dict.items(), key=lambda item: item[1])[::-1]) # Sort state dictionary with respect to values (counts) + + stopping_condition = False + for i in range(round(total_num_combinations * 0.5)): + if (i + 1) == len(counts_list): + stopping_condition = True + targets_list = counts_list + targets_dict = { + list(counts_dict.keys())[t]: list(counts_dict.values())[t] for t in range(len(targets_list)) + } + target_states = list(targets_dict.keys()) + break + + diff = counts_list[i] - counts_list[i + 1] + if diff > counts_list[i] * delta: + stopping_condition = True + targets_list = counts_list[: i + 1] + targets_dict = { + list(counts_dict.keys())[t]: list(counts_dict.values())[t] for t in range(len(targets_list)) + } + target_states = list(targets_dict.keys()) + break + + if stopping_condition: + break + + for i, state in enumerate(target_states): + target_states[i] = state[::-1] # Compensate Qiskit's qubit ordering + + return_dict[process_number] = target_states \ No newline at end of file diff --git a/tests/test_equivalence_checking_grover.py b/tests/test_equivalence_checking_grover.py new file mode 100644 index 0000000..7a29015 --- /dev/null +++ b/tests/test_equivalence_checking_grover.py @@ -0,0 +1,90 @@ +from __future__ import annotations + +import string +from mqt.problemsolver.equivalence_checker import executer + +alphabet = list(string.ascii_lowercase) + +def create_condition_string(num_qubits: int, num_targets: int) -> tuple[str, list[str]]: + """ + Creates a string to simulate a miter out of bitstring combinations (e.g. '0000' -> 'a & b & c & d') + + Parameters + ---------- + num_qubits : int + Number of input bits + num_targets : int + Number of counter examples + + Returns + ------- + res_string : str + Resulting condition string + list_of_bitstrings : list[str] + The corresponding bitstrings to res_string (e.g. list_of_bitstrings is ['0000'] for res_string 'a & b & c & d') + """ + + if num_qubits < 0 or num_targets < 0: + raise TypeError + + list_of_bitstrings: list[str] = [] + if num_targets == 0: + res, _ = create_condition_string(num_qubits, 1) + res += " & a" + return res, list_of_bitstrings + res_string: str = "" + list_of_bitstrings = [] + for num in range(num_targets): + bitstring = list(str(format(num, f"0{num_qubits}b")))[::-1] + list_of_bitstrings.append(str(format(num, f"0{num_qubits}b"))) + for i, char in enumerate(bitstring): + if char == "0" and i == 0: + bitstring[i] = "~" + alphabet[i] + elif char == "1" and i == 0: + bitstring[i] = alphabet[i] + elif char == "0": + bitstring[i] = " & " + "~" + alphabet[i] + elif char == "1": + bitstring[i] = " & " + alphabet[i] + combined_bitstring = "".join(bitstring) + if num < num_targets - 1: + res_string += combined_bitstring + " | " + else: + res_string += combined_bitstring + return res_string, list_of_bitstrings + +if __name__ == "__main__": + + def test_create_condition_string() -> None: + num_qubits = 3 + num_targets = 2 + res_string, list_of_bitstrings = create_condition_string( + num_qubits=num_qubits, num_targets=num_targets + ) + assert isinstance(res_string, str) + assert isinstance(list_of_bitstrings, list) + assert len(res_string) == 26 + assert len(list_of_bitstrings) == num_targets + assert res_string == "~a & ~b & ~c | a & ~b & ~c" + + + def test_run() -> None: + num_qubits = 4 + num_targets = 2 + shots = 128 + delta = 0.7 + number_of_processes = 4 + res_states = executer.run(num_qubits, num_targets, shots, delta, number_of_processes) + for process in res_states: + for state in ['1010','0000']: + assert state in process + + num_qubits = 6 + num_targets = 10 + shots = 512 + delta = 0.8 + number_of_processes = 4 + res_states = executer.run(num_qubits, num_targets, shots, delta, number_of_processes) + for process in res_states: + for state in ['010000', '100000', '110000', '000000', '100100', '101000', '011000', '001000', '000100', '111000']: + assert state in process \ No newline at end of file From a9bfca427a7704b42e4462927dbf7866f50de927 Mon Sep 17 00:00:00 2001 From: tobi-forster Date: Fri, 19 Jul 2024 08:56:03 +0200 Subject: [PATCH 02/27] bug fixes --- .../equivalence_checker/executer.py | 39 +++++------ tests/test_equivalence_checking_grover.py | 65 ++++++++++--------- 2 files changed, 49 insertions(+), 55 deletions(-) diff --git a/src/mqt/problemsolver/equivalence_checker/executer.py b/src/mqt/problemsolver/equivalence_checker/executer.py index 87b5c49..b5d8c65 100644 --- a/src/mqt/problemsolver/equivalence_checker/executer.py +++ b/src/mqt/problemsolver/equivalence_checker/executer.py @@ -1,17 +1,14 @@ from __future__ import annotations - -import multiprocessing +import threading import string - import numpy as np - -# from mqt.problemsolver.equivalence_checker import sampler - from qiskit import QuantumCircuit from qiskit.circuit.library import GroverOperator, PhaseOracle from qiskit.compiler import transpile from qiskit_aer import AerSimulator +# from mqt.problemsolver.equivalence_checker import sampler + sim_counts = AerSimulator(method="statevector") @@ -95,9 +92,9 @@ def sampler( return_dict[process_number] = target_states -def run(miter: str, num_qubits:int, shots: int, delta: float, number_of_processes: int) -> list[str | int]: +def find_counter_examples(miter: str, num_qubits:int, shots: int, delta: float, number_of_threads: int) -> list[str | int]: """ - Runs the grover verification application in multiple processes. + Runs the grover verification application in multiple threads. Parameters ---------- @@ -109,8 +106,8 @@ def run(miter: str, num_qubits:int, shots: int, delta: float, number_of_processe Number of shots delta: float Threshold parameter between 0 and 1 - number_of_processes: int - Number of processes the algorithm should run in simultaneously + number_of_threads: int + Number of threads the algorithm should run in simultaneously Returns ------- @@ -124,20 +121,16 @@ def run(miter: str, num_qubits:int, shots: int, delta: float, number_of_processe total_num_combinations = 2**num_qubits start_iterations = np.floor(np.pi / (4 * np.arcsin((1 / total_num_combinations) ** 0.5)) - 0.5).astype(int) - manager = multiprocessing.Manager() - return_dict = manager.dict() - jobs = [] - for i in range(number_of_processes): - process = multiprocessing.Process( + return_dict = {} + threads = [] + for i in range(number_of_threads): + thread = threading.Thread( target=sampler, args=(i, return_dict, miter, start_iterations, shots, delta), ) - jobs.append(process) - process.start() - - for job in jobs: - job.join() - return return_dict.values() + threads.append(thread) + thread.start() -miter = "a & b" -targets = run(miter, 2, 32, 0.7, 1) \ No newline at end of file + for thread in threads: + thread.join() + return list(return_dict.values()) diff --git a/tests/test_equivalence_checking_grover.py b/tests/test_equivalence_checking_grover.py index 7a29015..aea83b4 100644 --- a/tests/test_equivalence_checking_grover.py +++ b/tests/test_equivalence_checking_grover.py @@ -35,7 +35,7 @@ def create_condition_string(num_qubits: int, num_targets: int) -> tuple[str, lis res_string: str = "" list_of_bitstrings = [] for num in range(num_targets): - bitstring = list(str(format(num, f"0{num_qubits}b")))[::-1] + bitstring = list(str(format(num, f"0{num_qubits}b"))) list_of_bitstrings.append(str(format(num, f"0{num_qubits}b"))) for i, char in enumerate(bitstring): if char == "0" and i == 0: @@ -53,38 +53,39 @@ def create_condition_string(num_qubits: int, num_targets: int) -> tuple[str, lis res_string += combined_bitstring return res_string, list_of_bitstrings -if __name__ == "__main__": - def test_create_condition_string() -> None: - num_qubits = 3 - num_targets = 2 - res_string, list_of_bitstrings = create_condition_string( - num_qubits=num_qubits, num_targets=num_targets - ) - assert isinstance(res_string, str) - assert isinstance(list_of_bitstrings, list) - assert len(res_string) == 26 - assert len(list_of_bitstrings) == num_targets - assert res_string == "~a & ~b & ~c | a & ~b & ~c" +def test_create_condition_string() -> None: + num_qubits = 3 + num_targets = 2 + res_string, list_of_bitstrings = create_condition_string( + num_qubits=num_qubits, num_targets=num_targets + ) + + assert isinstance(res_string, str) + assert isinstance(list_of_bitstrings, list) + assert len(res_string) == 26 + assert len(list_of_bitstrings) == num_targets + assert res_string == "~a & ~b & ~c | ~a & ~b & c" - def test_run() -> None: - num_qubits = 4 - num_targets = 2 - shots = 128 - delta = 0.7 - number_of_processes = 4 - res_states = executer.run(num_qubits, num_targets, shots, delta, number_of_processes) - for process in res_states: - for state in ['1010','0000']: - assert state in process - num_qubits = 6 - num_targets = 10 - shots = 512 - delta = 0.8 - number_of_processes = 4 - res_states = executer.run(num_qubits, num_targets, shots, delta, number_of_processes) - for process in res_states: - for state in ['010000', '100000', '110000', '000000', '100100', '101000', '011000', '001000', '000100', '111000']: - assert state in process \ No newline at end of file +def test_run() -> None: + num_qubits = 3 + num_targets = 2 + shots = 128 + delta = 0.7 + number_of_processes = 8 + miter, solutions = create_condition_string(num_qubits, num_targets) + res_states = executer.find_counter_examples(miter, num_qubits, shots, delta, number_of_processes) + for process in res_states: + assert sorted(process) == sorted(solutions) + + num_qubits = 6 + num_targets = 10 + shots = 512 + delta = 0.8 + number_of_processes = 4 + miter, solutions = create_condition_string(num_qubits, num_targets) + res_states = executer.find_counter_examples(miter, num_qubits, shots, delta, number_of_processes) + for process in res_states: + assert sorted(process) == sorted(solutions) From 824d47bf98780ed22dd130c64ab6c358d27c615e Mon Sep 17 00:00:00 2001 From: tobi-forster Date: Fri, 19 Jul 2024 11:28:03 +0200 Subject: [PATCH 03/27] Converting to development stage, where solutions are known. --- .../equivalence_checker/executer.py | 159 ++++++++++++------ tests/test_equivalence_checking_grover.py | 2 +- 2 files changed, 113 insertions(+), 48 deletions(-) diff --git a/src/mqt/problemsolver/equivalence_checker/executer.py b/src/mqt/problemsolver/equivalence_checker/executer.py index 87b5c49..75a781a 100644 --- a/src/mqt/problemsolver/equivalence_checker/executer.py +++ b/src/mqt/problemsolver/equivalence_checker/executer.py @@ -14,11 +14,62 @@ sim_counts = AerSimulator(method="statevector") +alphabet = list(string.ascii_lowercase) + +def create_condition_string(num_qubits: int, num_counter_examples: int) -> tuple[str, list[str]]: + """ + Creates a string to simulate a miter out of bitstring combinations (e.g. '0000' -> 'a & b & c & d') + + Parameters + ---------- + num_qubits : int + Number of input bits + num_counter_examples : int + Number of counter examples + + Returns + ------- + res_string : str + Resulting condition string + counter_examples : list[str] + The corresponding bitstrings to res_string (e.g. counter_examples is ['0000'] for res_string 'a & b & c & d') + """ + + if num_qubits < 0 or num_counter_examples < 0: + raise ValueError + + counter_examples: list[str] = [] + if num_counter_examples == 0: + res, _ = create_condition_string(num_qubits, 1) + res += " & a" + return res, counter_examples + res_string: str = "" + counter_examples = [] + for num in range(num_counter_examples): + bitstring = list(str(format(num, f"0{num_qubits}b")))[::-1] + counter_examples.append(str(format(num, f"0{num_qubits}b"))) + for i, char in enumerate(bitstring): + if char == "0" and i == 0: + bitstring[i] = "~" + alphabet[i] + elif char == "1" and i == 0: + bitstring[i] = alphabet[i] + elif char == "0": + bitstring[i] = " & " + "~" + alphabet[i] + elif char == "1": + bitstring[i] = " & " + alphabet[i] + combined_bitstring = "".join(bitstring) + if num < num_counter_examples - 1: + res_string += combined_bitstring + " | " + else: + res_string += combined_bitstring + return res_string, counter_examples def sampler( process_number: int, return_dict: dict[int, str | int], miter: str, + counter_examples: list[str], + num_counter_examples: int, start_iterations: int, shots: int, delta: float, @@ -88,56 +139,70 @@ def sampler( if stopping_condition: break + + with open(f'results_{num_qubits}qubits_{num_counter_examples}counter_examples_{delta}delta.txt', 'a') as f: + if sorted(target_states) == sorted(counter_examples): + f.write(f'Correct targets found! Total number of iterations: {total_iterations} \n') + elif len(target_states) == 0: + if len(counter_examples) > 0: + f.write(f'No targets found! Total number of iterations: {total_iterations} \n') + elif len(counter_examples) == 0: + f.write(f'Correct targets found (None)! Total number of iterations: {total_iterations} \n') + else: + f.write(f'At least one wrong target found! Total number of iterations: {total_iterations} \n') + f.close() + for i, state in enumerate(target_states): target_states[i] = state[::-1] # Compensate Qiskit's qubit ordering return_dict[process_number] = target_states - -def run(miter: str, num_qubits:int, shots: int, delta: float, number_of_processes: int) -> list[str | int]: - """ - Runs the grover verification application in multiple processes. - - Parameters - ---------- - miter: str - String that contains the conditions to satisfy - num_qubits : int - Number of input bits - shots: int - Number of shots - delta: float - Threshold parameter between 0 and 1 - number_of_processes: int - Number of processes the algorithm should run in simultaneously - - Returns - ------- - list[str | int] - A list of values representing the targets found by the Grover algorithm - """ - try: - assert 0 <= delta <= 1 - except AssertionError: - print(f'Invalid delta of {delta}. It must be between 0 and 1.') - - total_num_combinations = 2**num_qubits - start_iterations = np.floor(np.pi / (4 * np.arcsin((1 / total_num_combinations) ** 0.5)) - 0.5).astype(int) - manager = multiprocessing.Manager() - return_dict = manager.dict() - jobs = [] - for i in range(number_of_processes): - process = multiprocessing.Process( - target=sampler, - args=(i, return_dict, miter, start_iterations, shots, delta), - ) - jobs.append(process) - process.start() - - for job in jobs: - job.join() - return return_dict.values() - -miter = "a & b" -targets = run(miter, 2, 32, 0.7, 1) \ No newline at end of file +if __name__ == "__main__": + + def find_counter_examples(miter: str, counter_examples: list[str], num_counter_examples: int, num_qubits:int, shots: int, delta: float, number_of_processes: int) -> list[str | int]: + """ + Runs the grover verification application in multiple processes. + + Parameters + ---------- + miter: str + String that contains the conditions to satisfy + num_qubits : int + Number of input bits + shots: int + Number of shots + delta: float + Threshold parameter between 0 and 1 + number_of_processes: int + Number of processes the algorithm should run in simultaneously + + Returns + ------- + list[str | int] + A list of values representing the targets found by the Grover algorithm + """ + try: + assert 0 <= delta <= 1 + except AssertionError: + print(f'Invalid delta of {delta}. It must be between 0 and 1.') + + total_num_combinations = 2**num_qubits + start_iterations = np.floor(np.pi / (4 * np.arcsin((1 / total_num_combinations) ** 0.5)) - 0.5).astype(int) + manager = multiprocessing.Manager() + return_dict = manager.dict() + jobs = [] + for i in range(number_of_processes): + process = multiprocessing.Process( + target=sampler, + args=(i, return_dict, miter, counter_examples, num_counter_examples, start_iterations, shots, delta), + ) + jobs.append(process) + process.start() + + for job in jobs: + job.join() + # return return_dict.values() + + miter, counter_examples = create_condition_string(3,2) + find_counter_examples(miter, counter_examples, 2, 3, 64, 0.7, 5) \ No newline at end of file diff --git a/tests/test_equivalence_checking_grover.py b/tests/test_equivalence_checking_grover.py index 7a29015..93b7093 100644 --- a/tests/test_equivalence_checking_grover.py +++ b/tests/test_equivalence_checking_grover.py @@ -25,7 +25,7 @@ def create_condition_string(num_qubits: int, num_targets: int) -> tuple[str, lis """ if num_qubits < 0 or num_targets < 0: - raise TypeError + raise ValueError list_of_bitstrings: list[str] = [] if num_targets == 0: From a56741676c20e01b1240b7b740f902a9f8c4c9b6 Mon Sep 17 00:00:00 2001 From: tobi-forster Date: Mon, 22 Jul 2024 13:57:01 +0200 Subject: [PATCH 04/27] removed unused "sampler.py" --- pyproject.toml | 3 +- .../equivalence_checker/executer.py | 25 +++--- .../equivalence_checker/sampler.py | 88 ------------------- tests/test_equivalence_checking_grover.py | 21 +++-- 4 files changed, 30 insertions(+), 107 deletions(-) delete mode 100644 src/mqt/problemsolver/equivalence_checker/sampler.py diff --git a/pyproject.toml b/pyproject.toml index bf71e5d..3ed1529 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -28,7 +28,8 @@ dependencies = [ "python_tsp", "docplex", "qiskit_optimization", - "tweedledum==1.0.0" + "tweedledum==1.0.0", + "qiskit_aer" ] classifiers = [ diff --git a/src/mqt/problemsolver/equivalence_checker/executer.py b/src/mqt/problemsolver/equivalence_checker/executer.py index b5d8c65..baf1df5 100644 --- a/src/mqt/problemsolver/equivalence_checker/executer.py +++ b/src/mqt/problemsolver/equivalence_checker/executer.py @@ -1,14 +1,13 @@ from __future__ import annotations + import threading -import string + import numpy as np from qiskit import QuantumCircuit from qiskit.circuit.library import GroverOperator, PhaseOracle from qiskit.compiler import transpile from qiskit_aer import AerSimulator -# from mqt.problemsolver.equivalence_checker import sampler - sim_counts = AerSimulator(method="statevector") @@ -60,7 +59,9 @@ def sampler( counts_list = list(counts_dict.values()) counts_list.sort(reverse=True) - counts_dict = dict(sorted(counts_dict.items(), key=lambda item: item[1])[::-1]) # Sort state dictionary with respect to values (counts) + counts_dict = dict( + sorted(counts_dict.items(), key=lambda item: item[1])[::-1] + ) # Sort state dictionary with respect to values (counts) stopping_condition = False for i in range(round(total_num_combinations * 0.5)): @@ -85,14 +86,16 @@ def sampler( if stopping_condition: break - + for i, state in enumerate(target_states): - target_states[i] = state[::-1] # Compensate Qiskit's qubit ordering + target_states[i] = state[::-1] # Compensate Qiskit's qubit ordering return_dict[process_number] = target_states -def find_counter_examples(miter: str, num_qubits:int, shots: int, delta: float, number_of_threads: int) -> list[str | int]: +def find_counter_examples( + miter: str, num_qubits: int, shots: int, delta: float, number_of_threads: int +) -> list[str | int]: """ Runs the grover verification application in multiple threads. @@ -117,11 +120,11 @@ def find_counter_examples(miter: str, num_qubits:int, shots: int, delta: float, try: assert 0 <= delta <= 1 except AssertionError: - print(f'Invalid delta of {delta}. It must be between 0 and 1.') - + print(f"Invalid delta of {delta}. It must be between 0 and 1.") + total_num_combinations = 2**num_qubits start_iterations = np.floor(np.pi / (4 * np.arcsin((1 / total_num_combinations) ** 0.5)) - 0.5).astype(int) - return_dict = {} + return_dict: dict[int, list[str]] = {} threads = [] for i in range(number_of_threads): thread = threading.Thread( @@ -133,4 +136,4 @@ def find_counter_examples(miter: str, num_qubits:int, shots: int, delta: float, for thread in threads: thread.join() - return list(return_dict.values()) + return [return_dict[i] for i in range(len(return_dict))] diff --git a/src/mqt/problemsolver/equivalence_checker/sampler.py b/src/mqt/problemsolver/equivalence_checker/sampler.py deleted file mode 100644 index 305711f..0000000 --- a/src/mqt/problemsolver/equivalence_checker/sampler.py +++ /dev/null @@ -1,88 +0,0 @@ -from __future__ import annotations - -from qiskit import QuantumCircuit -from qiskit.circuit.library import GroverOperator, PhaseOracle -from qiskit.compiler import transpile -from qiskit_aer import AerSimulator - -sim_counts = AerSimulator(method="statevector") - - -def sampler( - process_number: int, - return_dict: dict[int, str | int], - miter: str, - start_iterations: int, - shots: int, - delta: float, -) -> None: - """ - Runs the algorithm utilizing Grover's algorithm to look for elements satisfying the conditions. - - Parameters - ---------- - process_number : int - Index of the process - return_dict : dict[int, str | int] - Dictionary to share results with parent process - miter: str - String that contains the conditions to satisfy - start_iterations: int - Defines the number of Grover iterations to start with - shots: int - Number of shots the algorithm should run for - delta: float - Threshold parameter between 0 and 1 - """ - total_iterations = 0 - for iterations in reversed(range(1, start_iterations + 1)): - total_iterations += iterations - oracle = PhaseOracle(miter) - - operator = GroverOperator(oracle).decompose() - num_qubits = operator.num_qubits - total_num_combinations = 2**num_qubits - - qc = QuantumCircuit(num_qubits) - qc.h(list(range(num_qubits))) - qc.compose(operator.power(iterations).decompose(), inplace=True) - qc.measure_all() - - qc = transpile(qc, sim_counts) - - job = sim_counts.run(qc, shots=shots) - result = job.result() - counts_dict = dict(result.get_counts()) - counts_list = list(counts_dict.values()) - counts_list.sort(reverse=True) - - counts_dict = dict(sorted(counts_dict.items(), key=lambda item: item[1])[::-1]) # Sort state dictionary with respect to values (counts) - - stopping_condition = False - for i in range(round(total_num_combinations * 0.5)): - if (i + 1) == len(counts_list): - stopping_condition = True - targets_list = counts_list - targets_dict = { - list(counts_dict.keys())[t]: list(counts_dict.values())[t] for t in range(len(targets_list)) - } - target_states = list(targets_dict.keys()) - break - - diff = counts_list[i] - counts_list[i + 1] - if diff > counts_list[i] * delta: - stopping_condition = True - targets_list = counts_list[: i + 1] - targets_dict = { - list(counts_dict.keys())[t]: list(counts_dict.values())[t] for t in range(len(targets_list)) - } - target_states = list(targets_dict.keys()) - break - - if stopping_condition: - break - - for i, state in enumerate(target_states): - target_states[i] = state[::-1] # Compensate Qiskit's qubit ordering - - return_dict[process_number] = target_states \ No newline at end of file diff --git a/tests/test_equivalence_checking_grover.py b/tests/test_equivalence_checking_grover.py index aea83b4..126c11a 100644 --- a/tests/test_equivalence_checking_grover.py +++ b/tests/test_equivalence_checking_grover.py @@ -1,10 +1,12 @@ from __future__ import annotations import string + from mqt.problemsolver.equivalence_checker import executer alphabet = list(string.ascii_lowercase) + def create_condition_string(num_qubits: int, num_targets: int) -> tuple[str, list[str]]: """ Creates a string to simulate a miter out of bitstring combinations (e.g. '0000' -> 'a & b & c & d') @@ -54,14 +56,11 @@ def create_condition_string(num_qubits: int, num_targets: int) -> tuple[str, lis return res_string, list_of_bitstrings - def test_create_condition_string() -> None: num_qubits = 3 num_targets = 2 - res_string, list_of_bitstrings = create_condition_string( - num_qubits=num_qubits, num_targets=num_targets - ) - + res_string, list_of_bitstrings = create_condition_string(num_qubits=num_qubits, num_targets=num_targets) + assert isinstance(res_string, str) assert isinstance(list_of_bitstrings, list) assert len(res_string) == 26 @@ -76,8 +75,11 @@ def test_run() -> None: delta = 0.7 number_of_processes = 8 miter, solutions = create_condition_string(num_qubits, num_targets) - res_states = executer.find_counter_examples(miter, num_qubits, shots, delta, number_of_processes) + res_states: dict[int, list[str]] = executer.find_counter_examples( + miter, num_qubits, shots, delta, number_of_processes + ) for process in res_states: + print(type(res_states)) assert sorted(process) == sorted(solutions) num_qubits = 6 @@ -86,6 +88,11 @@ def test_run() -> None: delta = 0.8 number_of_processes = 4 miter, solutions = create_condition_string(num_qubits, num_targets) - res_states = executer.find_counter_examples(miter, num_qubits, shots, delta, number_of_processes) + res_states: dict[int, list[str]] = executer.find_counter_examples( + miter, num_qubits, shots, delta, number_of_processes + ) for process in res_states: assert sorted(process) == sorted(solutions) + + +test_run() From 6100642405679f64215a3c3841e7c211b919682d Mon Sep 17 00:00:00 2001 From: tobi-forster Date: Mon, 22 Jul 2024 15:22:13 +0200 Subject: [PATCH 05/27] Converted to single process and remove unused file. --- .../equivalence_checker/executer.py | 128 ++++++++---------- .../equivalence_checker/sampler.py | 88 ------------ tests/test_equivalence_checking_grover.py | 60 ++++---- 3 files changed, 83 insertions(+), 193 deletions(-) delete mode 100644 src/mqt/problemsolver/equivalence_checker/sampler.py diff --git a/src/mqt/problemsolver/equivalence_checker/executer.py b/src/mqt/problemsolver/equivalence_checker/executer.py index 75a781a..14bba28 100644 --- a/src/mqt/problemsolver/equivalence_checker/executer.py +++ b/src/mqt/problemsolver/equivalence_checker/executer.py @@ -1,12 +1,10 @@ from __future__ import annotations -import multiprocessing import string import numpy as np # from mqt.problemsolver.equivalence_checker import sampler - from qiskit import QuantumCircuit from qiskit.circuit.library import GroverOperator, PhaseOracle from qiskit.compiler import transpile @@ -16,6 +14,7 @@ alphabet = list(string.ascii_lowercase) + def create_condition_string(num_qubits: int, num_counter_examples: int) -> tuple[str, list[str]]: """ Creates a string to simulate a miter out of bitstring combinations (e.g. '0000' -> 'a & b & c & d') @@ -64,16 +63,14 @@ def create_condition_string(num_qubits: int, num_counter_examples: int) -> tuple res_string += combined_bitstring return res_string, counter_examples + def sampler( - process_number: int, - return_dict: dict[int, str | int], miter: str, counter_examples: list[str], - num_counter_examples: int, start_iterations: int, shots: int, delta: float, -) -> None: +) -> str: """ Runs the algorithm utilizing Grover's algorithm to look for elements satisfying the conditions. @@ -114,7 +111,9 @@ def sampler( counts_list = list(counts_dict.values()) counts_list.sort(reverse=True) - counts_dict = dict(sorted(counts_dict.items(), key=lambda item: item[1])[::-1]) # Sort state dictionary with respect to values (counts) + counts_dict = dict( + sorted(counts_dict.items(), key=lambda item: item[1])[::-1] + ) # Sort state dictionary with respect to values (counts) stopping_condition = False for i in range(round(total_num_combinations * 0.5)): @@ -139,70 +138,51 @@ def sampler( if stopping_condition: break - - with open(f'results_{num_qubits}qubits_{num_counter_examples}counter_examples_{delta}delta.txt', 'a') as f: - if sorted(target_states) == sorted(counter_examples): - f.write(f'Correct targets found! Total number of iterations: {total_iterations} \n') - elif len(target_states) == 0: - if len(counter_examples) > 0: - f.write(f'No targets found! Total number of iterations: {total_iterations} \n') - elif len(counter_examples) == 0: - f.write(f'Correct targets found (None)! Total number of iterations: {total_iterations} \n') - else: - f.write(f'At least one wrong target found! Total number of iterations: {total_iterations} \n') - f.close() - - - for i, state in enumerate(target_states): - target_states[i] = state[::-1] # Compensate Qiskit's qubit ordering - - return_dict[process_number] = target_states - -if __name__ == "__main__": - - def find_counter_examples(miter: str, counter_examples: list[str], num_counter_examples: int, num_qubits:int, shots: int, delta: float, number_of_processes: int) -> list[str | int]: - """ - Runs the grover verification application in multiple processes. - - Parameters - ---------- - miter: str - String that contains the conditions to satisfy - num_qubits : int - Number of input bits - shots: int - Number of shots - delta: float - Threshold parameter between 0 and 1 - number_of_processes: int - Number of processes the algorithm should run in simultaneously - - Returns - ------- - list[str | int] - A list of values representing the targets found by the Grover algorithm - """ - try: - assert 0 <= delta <= 1 - except AssertionError: - print(f'Invalid delta of {delta}. It must be between 0 and 1.') - - total_num_combinations = 2**num_qubits - start_iterations = np.floor(np.pi / (4 * np.arcsin((1 / total_num_combinations) ** 0.5)) - 0.5).astype(int) - manager = multiprocessing.Manager() - return_dict = manager.dict() - jobs = [] - for i in range(number_of_processes): - process = multiprocessing.Process( - target=sampler, - args=(i, return_dict, miter, counter_examples, num_counter_examples, start_iterations, shots, delta), - ) - jobs.append(process) - process.start() - - for job in jobs: - job.join() - # return return_dict.values() - - miter, counter_examples = create_condition_string(3,2) - find_counter_examples(miter, counter_examples, 2, 3, 64, 0.7, 5) \ No newline at end of file + + if sorted(target_states) == sorted(counter_examples): + return f"Correct targets found! Total number of iterations: {total_iterations}" + if len(target_states) == 0: + if len(counter_examples) > 0: + return f"No targets found! Total number of iterations: {total_iterations}" + if len(counter_examples) == 0: + return f"Correct targets found (None)! Total number of iterations: {total_iterations}" + + return f"At least one wrong target found! Total number of iterations: {total_iterations}" + + +def find_counter_examples( + miter: str, + counter_examples: list[str], + num_qubits: int, + shots: int, + delta: float, +) -> str: + """ + Runs the grover verification application in multiple processes. + + Parameters + ---------- + miter: str + String that contains the conditions to satisfy + num_qubits : int + Number of input bits + shots: int + Number of shots + delta: float + Threshold parameter between 0 and 1 + number_of_processes: int + Number of processes the algorithm should run in simultaneously + + Returns + ------- + list[str | int] + A list of values representing the targets found by the Grover algorithm + """ + try: + assert 0 <= delta <= 1 + except AssertionError: + print(f"Invalid delta of {delta}. It must be between 0 and 1.") + + total_num_combinations = 2**num_qubits + start_iterations = np.floor(np.pi / (4 * np.arcsin((1 / total_num_combinations) ** 0.5)) - 0.5).astype(int) + return sampler(miter, counter_examples, start_iterations, shots, delta) diff --git a/src/mqt/problemsolver/equivalence_checker/sampler.py b/src/mqt/problemsolver/equivalence_checker/sampler.py deleted file mode 100644 index 305711f..0000000 --- a/src/mqt/problemsolver/equivalence_checker/sampler.py +++ /dev/null @@ -1,88 +0,0 @@ -from __future__ import annotations - -from qiskit import QuantumCircuit -from qiskit.circuit.library import GroverOperator, PhaseOracle -from qiskit.compiler import transpile -from qiskit_aer import AerSimulator - -sim_counts = AerSimulator(method="statevector") - - -def sampler( - process_number: int, - return_dict: dict[int, str | int], - miter: str, - start_iterations: int, - shots: int, - delta: float, -) -> None: - """ - Runs the algorithm utilizing Grover's algorithm to look for elements satisfying the conditions. - - Parameters - ---------- - process_number : int - Index of the process - return_dict : dict[int, str | int] - Dictionary to share results with parent process - miter: str - String that contains the conditions to satisfy - start_iterations: int - Defines the number of Grover iterations to start with - shots: int - Number of shots the algorithm should run for - delta: float - Threshold parameter between 0 and 1 - """ - total_iterations = 0 - for iterations in reversed(range(1, start_iterations + 1)): - total_iterations += iterations - oracle = PhaseOracle(miter) - - operator = GroverOperator(oracle).decompose() - num_qubits = operator.num_qubits - total_num_combinations = 2**num_qubits - - qc = QuantumCircuit(num_qubits) - qc.h(list(range(num_qubits))) - qc.compose(operator.power(iterations).decompose(), inplace=True) - qc.measure_all() - - qc = transpile(qc, sim_counts) - - job = sim_counts.run(qc, shots=shots) - result = job.result() - counts_dict = dict(result.get_counts()) - counts_list = list(counts_dict.values()) - counts_list.sort(reverse=True) - - counts_dict = dict(sorted(counts_dict.items(), key=lambda item: item[1])[::-1]) # Sort state dictionary with respect to values (counts) - - stopping_condition = False - for i in range(round(total_num_combinations * 0.5)): - if (i + 1) == len(counts_list): - stopping_condition = True - targets_list = counts_list - targets_dict = { - list(counts_dict.keys())[t]: list(counts_dict.values())[t] for t in range(len(targets_list)) - } - target_states = list(targets_dict.keys()) - break - - diff = counts_list[i] - counts_list[i + 1] - if diff > counts_list[i] * delta: - stopping_condition = True - targets_list = counts_list[: i + 1] - targets_dict = { - list(counts_dict.keys())[t]: list(counts_dict.values())[t] for t in range(len(targets_list)) - } - target_states = list(targets_dict.keys()) - break - - if stopping_condition: - break - - for i, state in enumerate(target_states): - target_states[i] = state[::-1] # Compensate Qiskit's qubit ordering - - return_dict[process_number] = target_states \ No newline at end of file diff --git a/tests/test_equivalence_checking_grover.py b/tests/test_equivalence_checking_grover.py index 93b7093..d7d4708 100644 --- a/tests/test_equivalence_checking_grover.py +++ b/tests/test_equivalence_checking_grover.py @@ -1,11 +1,13 @@ from __future__ import annotations import string + from mqt.problemsolver.equivalence_checker import executer alphabet = list(string.ascii_lowercase) -def create_condition_string(num_qubits: int, num_targets: int) -> tuple[str, list[str]]: + +def create_condition_string(num_qubits: int, num_counter_examples: int) -> tuple[str, list[str]]: """ Creates a string to simulate a miter out of bitstring combinations (e.g. '0000' -> 'a & b & c & d') @@ -13,30 +15,30 @@ def create_condition_string(num_qubits: int, num_targets: int) -> tuple[str, lis ---------- num_qubits : int Number of input bits - num_targets : int + num_counter_examples : int Number of counter examples Returns ------- res_string : str Resulting condition string - list_of_bitstrings : list[str] - The corresponding bitstrings to res_string (e.g. list_of_bitstrings is ['0000'] for res_string 'a & b & c & d') + counter_examples : list[str] + The corresponding bitstrings to res_string (e.g. counter_examples is ['0000'] for res_string 'a & b & c & d') """ - if num_qubits < 0 or num_targets < 0: + if num_qubits < 0 or num_counter_examples < 0: raise ValueError - list_of_bitstrings: list[str] = [] - if num_targets == 0: + counter_examples: list[str] = [] + if num_counter_examples == 0: res, _ = create_condition_string(num_qubits, 1) res += " & a" - return res, list_of_bitstrings + return res, counter_examples res_string: str = "" - list_of_bitstrings = [] - for num in range(num_targets): + counter_examples = [] + for num in range(num_counter_examples): bitstring = list(str(format(num, f"0{num_qubits}b")))[::-1] - list_of_bitstrings.append(str(format(num, f"0{num_qubits}b"))) + counter_examples.append(str(format(num, f"0{num_qubits}b"))) for i, char in enumerate(bitstring): if char == "0" and i == 0: bitstring[i] = "~" + alphabet[i] @@ -47,44 +49,40 @@ def create_condition_string(num_qubits: int, num_targets: int) -> tuple[str, lis elif char == "1": bitstring[i] = " & " + alphabet[i] combined_bitstring = "".join(bitstring) - if num < num_targets - 1: + if num < num_counter_examples - 1: res_string += combined_bitstring + " | " else: res_string += combined_bitstring - return res_string, list_of_bitstrings + return res_string, counter_examples + if __name__ == "__main__": def test_create_condition_string() -> None: num_qubits = 3 - num_targets = 2 - res_string, list_of_bitstrings = create_condition_string( - num_qubits=num_qubits, num_targets=num_targets + num_counter_examples = 2 + res_string, counter_examples = create_condition_string( + num_qubits=num_qubits, num_counter_examples=num_counter_examples ) assert isinstance(res_string, str) - assert isinstance(list_of_bitstrings, list) + assert isinstance(counter_examples, list) assert len(res_string) == 26 - assert len(list_of_bitstrings) == num_targets + assert len(counter_examples) == num_counter_examples assert res_string == "~a & ~b & ~c | a & ~b & ~c" - def test_run() -> None: num_qubits = 4 - num_targets = 2 + num_counter_examples = 2 shots = 128 delta = 0.7 - number_of_processes = 4 - res_states = executer.run(num_qubits, num_targets, shots, delta, number_of_processes) - for process in res_states: - for state in ['1010','0000']: - assert state in process + miter, counter_examples = create_condition_string(num_qubits, num_counter_examples) + result = executer.find_counter_examples(miter, counter_examples, num_qubits, shots, delta) + assert result == "Correct targets found! Total number of iterations: 2" num_qubits = 6 - num_targets = 10 + num_counter_examples = 10 shots = 512 delta = 0.8 - number_of_processes = 4 - res_states = executer.run(num_qubits, num_targets, shots, delta, number_of_processes) - for process in res_states: - for state in ['010000', '100000', '110000', '000000', '100100', '101000', '011000', '001000', '000100', '111000']: - assert state in process \ No newline at end of file + miter, counter_examples = create_condition_string(num_qubits, num_counter_examples) + result = executer.find_counter_examples(miter, counter_examples, num_qubits, shots, delta) + assert result == "Correct targets found! Total number of iterations: 5" From b1aaf14691bd133ea894439ed4d3cd502b43d1f1 Mon Sep 17 00:00:00 2001 From: tobi-forster Date: Wed, 14 Aug 2024 09:54:22 +0200 Subject: [PATCH 06/27] Added function to export results as .csv --- pyproject.toml | 3 +- results.csv | 11 +++ .../equivalence_checker/executer.py | 72 ++++++++++++++----- 3 files changed, 68 insertions(+), 18 deletions(-) create mode 100644 results.csv diff --git a/pyproject.toml b/pyproject.toml index bf71e5d..c9715e2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -28,7 +28,8 @@ dependencies = [ "python_tsp", "docplex", "qiskit_optimization", - "tweedledum==1.0.0" + "tweedledum==1.0.0", + "pandas" ] classifiers = [ diff --git a/results.csv b/results.csv new file mode 100644 index 0000000..1707c50 --- /dev/null +++ b/results.csv @@ -0,0 +1,11 @@ +Input Bits,Counter Examples,0.1,0.3,0.5,0.7,0.9 +6.0,0.0,-,15.0,15.0,15.0,15.0 +6.0,0.01,5.0,5.0,5.0,5.0,5.0 +6.0,0.05,5.0,5.0,5.0,5.0,9.0 +6.0,0.1,-,12.0,12.0,12.0,14.0 +6.0,0.2,-,5.0,5.0,5.0,- +7.0,0.0,-,36.0,36.0,36.0,36.0 +7.0,0.01,8.0,8.0,8.0,8.0,8.0 +7.0,0.05,-,8.0,8.0,17.0,30.0 +7.0,0.1,-,8.0,8.0,15.0,15.0 +7.0,0.2,-,8.0,8.0,8.0,8.0 diff --git a/src/mqt/problemsolver/equivalence_checker/executer.py b/src/mqt/problemsolver/equivalence_checker/executer.py index 14bba28..6b8f886 100644 --- a/src/mqt/problemsolver/equivalence_checker/executer.py +++ b/src/mqt/problemsolver/equivalence_checker/executer.py @@ -4,6 +4,8 @@ import numpy as np +import pandas as pd + # from mqt.problemsolver.equivalence_checker import sampler from qiskit import QuantumCircuit from qiskit.circuit.library import GroverOperator, PhaseOracle @@ -15,13 +17,13 @@ alphabet = list(string.ascii_lowercase) -def create_condition_string(num_qubits: int, num_counter_examples: int) -> tuple[str, list[str]]: +def create_condition_string(num_bits: int, num_counter_examples: int) -> tuple[str, list[str]]: """ Creates a string to simulate a miter out of bitstring combinations (e.g. '0000' -> 'a & b & c & d') Parameters ---------- - num_qubits : int + num_bits : int Number of input bits num_counter_examples : int Number of counter examples @@ -34,19 +36,19 @@ def create_condition_string(num_qubits: int, num_counter_examples: int) -> tuple The corresponding bitstrings to res_string (e.g. counter_examples is ['0000'] for res_string 'a & b & c & d') """ - if num_qubits < 0 or num_counter_examples < 0: + if num_bits < 0 or num_counter_examples < 0: raise ValueError counter_examples: list[str] = [] if num_counter_examples == 0: - res, _ = create_condition_string(num_qubits, 1) + res, _ = create_condition_string(num_bits, 1) res += " & a" return res, counter_examples res_string: str = "" counter_examples = [] for num in range(num_counter_examples): - bitstring = list(str(format(num, f"0{num_qubits}b")))[::-1] - counter_examples.append(str(format(num, f"0{num_qubits}b"))) + bitstring = list(str(format(num, f"0{num_bits}b")))[::-1] + counter_examples.append(str(format(num, f"0{num_bits}b"))) for i, char in enumerate(bitstring): if char == "0" and i == 0: bitstring[i] = "~" + alphabet[i] @@ -95,11 +97,11 @@ def sampler( oracle = PhaseOracle(miter) operator = GroverOperator(oracle).decompose() - num_qubits = operator.num_qubits - total_num_combinations = 2**num_qubits + num_bits = operator.num_qubits + total_num_combinations = 2**num_bits - qc = QuantumCircuit(num_qubits) - qc.h(list(range(num_qubits))) + qc = QuantumCircuit(num_bits) + qc.h(list(range(num_bits))) qc.compose(operator.power(iterations).decompose(), inplace=True) qc.measure_all() @@ -115,6 +117,7 @@ def sampler( sorted(counts_dict.items(), key=lambda item: item[1])[::-1] ) # Sort state dictionary with respect to values (counts) + target_states = [] stopping_condition = False for i in range(round(total_num_combinations * 0.5)): if (i + 1) == len(counts_list): @@ -139,21 +142,31 @@ def sampler( if stopping_condition: break + # if sorted(target_states) == sorted(counter_examples): + # return f"Correct targets found! Total number of iterations: {total_iterations}" + # if len(target_states) == 0: + # if len(counter_examples) > 0: + # return f"No targets found! Total number of iterations: {total_iterations}" + # if len(counter_examples) == 0: + # return f"Correct targets found (None)! Total number of iterations: {total_iterations}" + + # return f"At least one wrong target found! Total number of iterations: {total_iterations}" + if sorted(target_states) == sorted(counter_examples): - return f"Correct targets found! Total number of iterations: {total_iterations}" + return total_iterations if len(target_states) == 0: if len(counter_examples) > 0: - return f"No targets found! Total number of iterations: {total_iterations}" + return None if len(counter_examples) == 0: - return f"Correct targets found (None)! Total number of iterations: {total_iterations}" + return total_iterations - return f"At least one wrong target found! Total number of iterations: {total_iterations}" + return None def find_counter_examples( miter: str, counter_examples: list[str], - num_qubits: int, + num_bits: int, shots: int, delta: float, ) -> str: @@ -164,7 +177,7 @@ def find_counter_examples( ---------- miter: str String that contains the conditions to satisfy - num_qubits : int + num_bits : int Number of input bits shots: int Number of shots @@ -183,6 +196,31 @@ def find_counter_examples( except AssertionError: print(f"Invalid delta of {delta}. It must be between 0 and 1.") - total_num_combinations = 2**num_qubits + total_num_combinations = 2**num_bits start_iterations = np.floor(np.pi / (4 * np.arcsin((1 / total_num_combinations) ** 0.5)) - 0.5).astype(int) return sampler(miter, counter_examples, start_iterations, shots, delta) + + +def run(path, range_deltas, range_num_bits, range_fraction_counter_examples, num_runs, verbose=False): + data = pd.DataFrame(columns = ['Input Bits','Counter Examples'] + [delta for delta in deltas]) + i = 0 + for num_bits in range_num_bits: + for fraction_counter_examples in range_fraction_counter_examples: + row = [num_bits, fraction_counter_examples] + for delta in range_deltas: + if verbose: + print(f"num_bits: {num_bits}, fraction_counter_examples: {fraction_counter_examples}, delta: {delta}") + results = [] + for run in range(num_runs): + num_counter_examples = round(fraction_counter_examples * 2**num_bits) + miter, counter_examples = create_condition_string(num_bits, num_counter_examples) + result = find_counter_examples(miter, counter_examples, num_bits, 8 * (2**num_bits), delta) + results.append(result) + if None in results: + row.append("-") + else: + row.append(np.mean(results)) + data.loc[i] = row + i += 1 + + data.to_csv(path, index=False) From 50cda35066139350bf1beb935fe4d40225f235cc Mon Sep 17 00:00:00 2001 From: tobi-forster Date: Wed, 14 Aug 2024 20:41:53 +0200 Subject: [PATCH 07/27] Added the end-user version of the Grover executer --- .pre-commit-config.yaml | 1 + pyproject.toml | 2 +- results.csv | 11 - .../equivalence_checker/executer.py | 213 +++++++++++------- ..._grover.py => test_equivalence_checker.py} | 36 ++- 5 files changed, 162 insertions(+), 101 deletions(-) delete mode 100644 results.csv rename tests/{test_equivalence_checking_grover.py => test_equivalence_checker.py} (72%) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 760d36d..32a499f 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -89,3 +89,4 @@ repos: - networkx - mqt.ddsim - pytest + - pandas-stubs diff --git a/pyproject.toml b/pyproject.toml index c9715e2..33951cd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -82,7 +82,7 @@ explicit_package_bases = true pretty = true [[tool.mypy.overrides]] -module = ["qiskit.*", "matplotlib.*", "python_tsp.*", "networkx.*", "mqt.ddsim.*", "joblib.*", "qiskit_optimization.*", "docplex.*"] +module = ["qiskit.*", "matplotlib.*", "python_tsp.*", "networkx.*", "mqt.ddsim.*", "joblib.*", "qiskit_optimization.*", "docplex.*", "qiskit_aer.*"] ignore_missing_imports = true [tool.ruff] diff --git a/results.csv b/results.csv deleted file mode 100644 index 1707c50..0000000 --- a/results.csv +++ /dev/null @@ -1,11 +0,0 @@ -Input Bits,Counter Examples,0.1,0.3,0.5,0.7,0.9 -6.0,0.0,-,15.0,15.0,15.0,15.0 -6.0,0.01,5.0,5.0,5.0,5.0,5.0 -6.0,0.05,5.0,5.0,5.0,5.0,9.0 -6.0,0.1,-,12.0,12.0,12.0,14.0 -6.0,0.2,-,5.0,5.0,5.0,- -7.0,0.0,-,36.0,36.0,36.0,36.0 -7.0,0.01,8.0,8.0,8.0,8.0,8.0 -7.0,0.05,-,8.0,8.0,17.0,30.0 -7.0,0.1,-,8.0,8.0,15.0,15.0 -7.0,0.2,-,8.0,8.0,8.0,8.0 diff --git a/src/mqt/problemsolver/equivalence_checker/executer.py b/src/mqt/problemsolver/equivalence_checker/executer.py index 6b8f886..1501aee 100644 --- a/src/mqt/problemsolver/equivalence_checker/executer.py +++ b/src/mqt/problemsolver/equivalence_checker/executer.py @@ -3,10 +3,7 @@ import string import numpy as np - import pandas as pd - -# from mqt.problemsolver.equivalence_checker import sampler from qiskit import QuantumCircuit from qiskit.circuit.library import GroverOperator, PhaseOracle from qiskit.compiler import transpile @@ -66,31 +63,41 @@ def create_condition_string(num_bits: int, num_counter_examples: int) -> tuple[s return res_string, counter_examples -def sampler( +def run_parameter_combinations( miter: str, counter_examples: list[str], - start_iterations: int, + num_bits: int, shots: int, delta: float, -) -> str: +) -> int | None: """ - Runs the algorithm utilizing Grover's algorithm to look for elements satisfying the conditions. + Runs Grover's algorithm to find counter examples for a given miter Parameters ---------- - process_number : int - Index of the process - return_dict : dict[int, str | int] - Dictionary to share results with parent process - miter: str - String that contains the conditions to satisfy - start_iterations: int - Defines the number of Grover iterations to start with - shots: int - Number of shots the algorithm should run for - delta: float - Threshold parameter between 0 and 1 + miter : str + Miter condition string + counter_examples : list[str] + List of counter examples + num_bits : int + Number of input bits + shots : int + Number of shots to run the quantum circuit for + delta : float + Threshold for the stopping condition + + Returns + ------- + None if no or wrong counter examples were found, else the number of iterations """ + try: + assert 0 <= delta <= 1 + except ValueError: + print(f"Invalid delta of {delta}. It must be between 0 and 1.") + + total_num_combinations = 2**num_bits + start_iterations = np.floor(np.pi / (4 * np.arcsin((1 / total_num_combinations) ** 0.5)) - 0.5).astype(int) + total_iterations = 0 for iterations in reversed(range(1, start_iterations + 1)): total_iterations += iterations @@ -117,44 +124,36 @@ def sampler( sorted(counts_dict.items(), key=lambda item: item[1])[::-1] ) # Sort state dictionary with respect to values (counts) - target_states = [] + counter_examples = [] stopping_condition = False for i in range(round(total_num_combinations * 0.5)): if (i + 1) == len(counts_list): stopping_condition = True - targets_list = counts_list - targets_dict = { - list(counts_dict.keys())[t]: list(counts_dict.values())[t] for t in range(len(targets_list)) + counter_examples_list = counts_list + counter_examples_dict = { + list(counts_dict.keys())[t]: list(counts_dict.values())[t] + for t in range(len(counter_examples_list)) } - target_states = list(targets_dict.keys()) + counter_examples = list(counter_examples_dict.keys()) break diff = counts_list[i] - counts_list[i + 1] if diff > counts_list[i] * delta: stopping_condition = True - targets_list = counts_list[: i + 1] - targets_dict = { - list(counts_dict.keys())[t]: list(counts_dict.values())[t] for t in range(len(targets_list)) + counter_examples_list = counts_list[: i + 1] + counter_examples_dict = { + list(counts_dict.keys())[t]: list(counts_dict.values())[t] + for t in range(len(counter_examples_list)) } - target_states = list(targets_dict.keys()) + counter_examples = list(counter_examples_dict.keys()) break if stopping_condition: break - # if sorted(target_states) == sorted(counter_examples): - # return f"Correct targets found! Total number of iterations: {total_iterations}" - # if len(target_states) == 0: - # if len(counter_examples) > 0: - # return f"No targets found! Total number of iterations: {total_iterations}" - # if len(counter_examples) == 0: - # return f"Correct targets found (None)! Total number of iterations: {total_iterations}" - - # return f"At least one wrong target found! Total number of iterations: {total_iterations}" - - if sorted(target_states) == sorted(counter_examples): + if sorted(counter_examples) == sorted(counter_examples): return total_iterations - if len(target_states) == 0: + if len(counter_examples) == 0: if len(counter_examples) > 0: return None if len(counter_examples) == 0: @@ -163,64 +162,124 @@ def sampler( return None +def try_parameter_combinations( + path: str, + range_deltas: list[float], + range_num_bits: list[int], + range_fraction_counter_examples: list[float], + num_runs: int, + verbose: bool = False, +) -> None: + data = pd.DataFrame(columns=["Input Bits", "Counter Examples", *range_deltas]) + i = 0 + for num_bits in range_num_bits: + for fraction_counter_examples in range_fraction_counter_examples: + row: list[float | int | str] = [num_bits, fraction_counter_examples] + for delta in range_deltas: + if verbose: + print( + f"num_bits: {num_bits}, fraction_counter_examples: {fraction_counter_examples}, delta: {delta}" + ) + results = [] + for _run in range(num_runs): + num_counter_examples = round(fraction_counter_examples * 2**num_bits) + miter, counter_examples = create_condition_string(num_bits, num_counter_examples) + result = run_parameter_combinations(miter, counter_examples, num_bits, 8 * (2**num_bits), delta) + results.append(result) + if None in results: + row.append("-") + else: + row.append(float(np.mean(np.asarray(results)))) + data.loc[i] = row + i += 1 + + data.to_csv(path, index=False) + + def find_counter_examples( miter: str, - counter_examples: list[str], num_bits: int, shots: int, delta: float, -) -> str: +) -> list[str | None]: """ - Runs the grover verification application in multiple processes. + Runs Grover's algorithm to find counter examples for a given miter Parameters ---------- - miter: str - String that contains the conditions to satisfy + miter : str + Miter condition string num_bits : int Number of input bits - shots: int - Number of shots - delta: float - Threshold parameter between 0 and 1 - number_of_processes: int - Number of processes the algorithm should run in simultaneously + shots : int + Number of shots to run the quantum circuit for + delta : float + Threshold for the stopping condition Returns ------- - list[str | int] - A list of values representing the targets found by the Grover algorithm + counter_examples: list[str] + List of states that are assumed to be counter examples """ try: assert 0 <= delta <= 1 - except AssertionError: + except ValueError: print(f"Invalid delta of {delta}. It must be between 0 and 1.") total_num_combinations = 2**num_bits start_iterations = np.floor(np.pi / (4 * np.arcsin((1 / total_num_combinations) ** 0.5)) - 0.5).astype(int) - return sampler(miter, counter_examples, start_iterations, shots, delta) + total_iterations = 0 + for iterations in reversed(range(1, start_iterations + 1)): + total_iterations += iterations + oracle = PhaseOracle(miter) -def run(path, range_deltas, range_num_bits, range_fraction_counter_examples, num_runs, verbose=False): - data = pd.DataFrame(columns = ['Input Bits','Counter Examples'] + [delta for delta in deltas]) - i = 0 - for num_bits in range_num_bits: - for fraction_counter_examples in range_fraction_counter_examples: - row = [num_bits, fraction_counter_examples] - for delta in range_deltas: - if verbose: - print(f"num_bits: {num_bits}, fraction_counter_examples: {fraction_counter_examples}, delta: {delta}") - results = [] - for run in range(num_runs): - num_counter_examples = round(fraction_counter_examples * 2**num_bits) - miter, counter_examples = create_condition_string(num_bits, num_counter_examples) - result = find_counter_examples(miter, counter_examples, num_bits, 8 * (2**num_bits), delta) - results.append(result) - if None in results: - row.append("-") - else: - row.append(np.mean(results)) - data.loc[i] = row - i += 1 + operator = GroverOperator(oracle).decompose() + num_bits = operator.num_qubits + total_num_combinations = 2**num_bits - data.to_csv(path, index=False) + qc = QuantumCircuit(num_bits) + qc.h(list(range(num_bits))) + qc.compose(operator.power(iterations).decompose(), inplace=True) + qc.measure_all() + + qc = transpile(qc, sim_counts) + + job = sim_counts.run(qc, shots=shots) + result = job.result() + counts_dict = dict(result.get_counts()) + counts_list = list(counts_dict.values()) + counts_list.sort(reverse=True) + + counts_dict = dict( + sorted(counts_dict.items(), key=lambda item: item[1])[::-1] + ) # Sort state dictionary with respect to values (counts) + + counter_examples = [] + stopping_condition = False + for i in range(round(total_num_combinations * 0.5)): + if (i + 1) == len(counts_list): + stopping_condition = True + counter_examples_list = counts_list + counter_examples_dict = { + list(counts_dict.keys())[t]: list(counts_dict.values())[t] + for t in range(len(counter_examples_list)) + } + counter_examples = list(counter_examples_dict.keys()) + break + + diff = counts_list[i] - counts_list[i + 1] + if diff > counts_list[i] * delta: + stopping_condition = True + counter_examples_list = counts_list[: i + 1] + counter_examples_dict = { + list(counts_dict.keys())[t]: list(counts_dict.values())[t] + for t in range(len(counter_examples_list)) + } + counter_examples = list(counter_examples_dict.keys()) + break + + if stopping_condition: + break + + return counter_examples diff --git a/tests/test_equivalence_checking_grover.py b/tests/test_equivalence_checker.py similarity index 72% rename from tests/test_equivalence_checking_grover.py rename to tests/test_equivalence_checker.py index d7d4708..a04260d 100644 --- a/tests/test_equivalence_checking_grover.py +++ b/tests/test_equivalence_checker.py @@ -70,19 +70,31 @@ def test_create_condition_string() -> None: assert len(counter_examples) == num_counter_examples assert res_string == "~a & ~b & ~c | a & ~b & ~c" - def test_run() -> None: - num_qubits = 4 - num_counter_examples = 2 - shots = 128 + def test_try_paramter_combinations() -> None: + num_qubits = 6 + num_counter_examples = 3 + res_string, counter_examples = create_condition_string( + num_qubits=num_qubits, num_counter_examples=num_counter_examples + ) + shots = 512 delta = 0.7 - miter, counter_examples = create_condition_string(num_qubits, num_counter_examples) - result = executer.find_counter_examples(miter, counter_examples, num_qubits, shots, delta) - assert result == "Correct targets found! Total number of iterations: 2" + result = executer.find_counter_examples( + miter=res_string, counter_examples=counter_examples, num_bits=num_qubits, shots=shots, delta=delta + ) + assert result == 5 - num_qubits = 6 + def test_find_counter_examples() -> None: + num_qubits = 8 num_counter_examples = 10 + res_string, counter_examples = create_condition_string( + num_qubits=num_qubits, num_counter_examples=num_counter_examples + ) shots = 512 - delta = 0.8 - miter, counter_examples = create_condition_string(num_qubits, num_counter_examples) - result = executer.find_counter_examples(miter, counter_examples, num_qubits, shots, delta) - assert result == "Correct targets found! Total number of iterations: 5" + delta = 0.7 + found_counter_examples = executer.find_counter_examples( + miter=res_string, num_bits=num_qubits, shots=shots, delta=delta + ) + assert sorted(found_counter_examples) == sorted(counter_examples) + + +test_find_counter_examples() \ No newline at end of file From 209c8fd0d25141a03ce279c61607c9727e7dbdf0 Mon Sep 17 00:00:00 2001 From: tobi-forster Date: Thu, 15 Aug 2024 00:24:31 +0200 Subject: [PATCH 08/27] Removed outdated tests --- tests/test_equivalence_checking_grover.py | 90 ----------------------- 1 file changed, 90 deletions(-) delete mode 100644 tests/test_equivalence_checking_grover.py diff --git a/tests/test_equivalence_checking_grover.py b/tests/test_equivalence_checking_grover.py deleted file mode 100644 index 7a29015..0000000 --- a/tests/test_equivalence_checking_grover.py +++ /dev/null @@ -1,90 +0,0 @@ -from __future__ import annotations - -import string -from mqt.problemsolver.equivalence_checker import executer - -alphabet = list(string.ascii_lowercase) - -def create_condition_string(num_qubits: int, num_targets: int) -> tuple[str, list[str]]: - """ - Creates a string to simulate a miter out of bitstring combinations (e.g. '0000' -> 'a & b & c & d') - - Parameters - ---------- - num_qubits : int - Number of input bits - num_targets : int - Number of counter examples - - Returns - ------- - res_string : str - Resulting condition string - list_of_bitstrings : list[str] - The corresponding bitstrings to res_string (e.g. list_of_bitstrings is ['0000'] for res_string 'a & b & c & d') - """ - - if num_qubits < 0 or num_targets < 0: - raise TypeError - - list_of_bitstrings: list[str] = [] - if num_targets == 0: - res, _ = create_condition_string(num_qubits, 1) - res += " & a" - return res, list_of_bitstrings - res_string: str = "" - list_of_bitstrings = [] - for num in range(num_targets): - bitstring = list(str(format(num, f"0{num_qubits}b")))[::-1] - list_of_bitstrings.append(str(format(num, f"0{num_qubits}b"))) - for i, char in enumerate(bitstring): - if char == "0" and i == 0: - bitstring[i] = "~" + alphabet[i] - elif char == "1" and i == 0: - bitstring[i] = alphabet[i] - elif char == "0": - bitstring[i] = " & " + "~" + alphabet[i] - elif char == "1": - bitstring[i] = " & " + alphabet[i] - combined_bitstring = "".join(bitstring) - if num < num_targets - 1: - res_string += combined_bitstring + " | " - else: - res_string += combined_bitstring - return res_string, list_of_bitstrings - -if __name__ == "__main__": - - def test_create_condition_string() -> None: - num_qubits = 3 - num_targets = 2 - res_string, list_of_bitstrings = create_condition_string( - num_qubits=num_qubits, num_targets=num_targets - ) - assert isinstance(res_string, str) - assert isinstance(list_of_bitstrings, list) - assert len(res_string) == 26 - assert len(list_of_bitstrings) == num_targets - assert res_string == "~a & ~b & ~c | a & ~b & ~c" - - - def test_run() -> None: - num_qubits = 4 - num_targets = 2 - shots = 128 - delta = 0.7 - number_of_processes = 4 - res_states = executer.run(num_qubits, num_targets, shots, delta, number_of_processes) - for process in res_states: - for state in ['1010','0000']: - assert state in process - - num_qubits = 6 - num_targets = 10 - shots = 512 - delta = 0.8 - number_of_processes = 4 - res_states = executer.run(num_qubits, num_targets, shots, delta, number_of_processes) - for process in res_states: - for state in ['010000', '100000', '110000', '000000', '100100', '101000', '011000', '001000', '000100', '111000']: - assert state in process \ No newline at end of file From 2712e918b494ed18506e2c8a389a34641d1cd93e Mon Sep 17 00:00:00 2001 From: tobi-forster Date: Thu, 15 Aug 2024 13:23:33 +0200 Subject: [PATCH 09/27] Adjusted equivalence_checker tests and commenting --- .../equivalence_checker/executer.py | 27 ++++++++++++++++--- tests/test_equivalence_checker.py | 11 ++++---- 2 files changed, 28 insertions(+), 10 deletions(-) diff --git a/src/mqt/problemsolver/equivalence_checker/executer.py b/src/mqt/problemsolver/equivalence_checker/executer.py index 1501aee..7d7cee4 100644 --- a/src/mqt/problemsolver/equivalence_checker/executer.py +++ b/src/mqt/problemsolver/equivalence_checker/executer.py @@ -71,7 +71,7 @@ def run_parameter_combinations( delta: float, ) -> int | None: """ - Runs Grover's algorithm to find counter examples for a given miter + Runs Grover's algorithm to find counter examples for a given miter when knowing the counter examples to test parameters Parameters ---------- @@ -170,11 +170,31 @@ def try_parameter_combinations( num_runs: int, verbose: bool = False, ) -> None: + """ + Tries different parameter combinations for Grover's algorithm to find the optimal parameters + + Parameters + ---------- + path : str + Path to save the results + range_deltas : list[float] + List of delta values to try + range_num_bits : list[int] + List of numbers of input bits to try + range_fraction_counter_examples : list[float] + List of fractions of counter examples to try + num_runs : int + Number of runs for each parameter combination + verbose : bool + If True, print the current parameter combination + + """ data = pd.DataFrame(columns=["Input Bits", "Counter Examples", *range_deltas]) i = 0 for num_bits in range_num_bits: for fraction_counter_examples in range_fraction_counter_examples: - row: list[float | int | str] = [num_bits, fraction_counter_examples] + num_counter_examples = round(fraction_counter_examples * 2**num_bits) + row: list[float | int | str] = [num_bits, num_counter_examples] for delta in range_deltas: if verbose: print( @@ -182,7 +202,6 @@ def try_parameter_combinations( ) results = [] for _run in range(num_runs): - num_counter_examples = round(fraction_counter_examples * 2**num_bits) miter, counter_examples = create_condition_string(num_bits, num_counter_examples) result = run_parameter_combinations(miter, counter_examples, num_bits, 8 * (2**num_bits), delta) results.append(result) @@ -203,7 +222,7 @@ def find_counter_examples( delta: float, ) -> list[str | None]: """ - Runs Grover's algorithm to find counter examples for a given miter + Runs Grover's algorithm to find counter examples for a given miter without knowing the counter examples Parameters ---------- diff --git a/tests/test_equivalence_checker.py b/tests/test_equivalence_checker.py index a04260d..35c7eaf 100644 --- a/tests/test_equivalence_checker.py +++ b/tests/test_equivalence_checker.py @@ -70,7 +70,7 @@ def test_create_condition_string() -> None: assert len(counter_examples) == num_counter_examples assert res_string == "~a & ~b & ~c | a & ~b & ~c" - def test_try_paramter_combinations() -> None: + def test_run_paramter_combinations() -> None: num_qubits = 6 num_counter_examples = 3 res_string, counter_examples = create_condition_string( @@ -78,7 +78,7 @@ def test_try_paramter_combinations() -> None: ) shots = 512 delta = 0.7 - result = executer.find_counter_examples( + result = executer.run_parameter_combinations( miter=res_string, counter_examples=counter_examples, num_bits=num_qubits, shots=shots, delta=delta ) assert result == 5 @@ -94,7 +94,6 @@ def test_find_counter_examples() -> None: found_counter_examples = executer.find_counter_examples( miter=res_string, num_bits=num_qubits, shots=shots, delta=delta ) - assert sorted(found_counter_examples) == sorted(counter_examples) - - -test_find_counter_examples() \ No newline at end of file + found_counter_examples.sort() + counter_examples.sort() + assert found_counter_examples == counter_examples From 99c704d85d12eb5d7a04a132182e91eb6fe71719 Mon Sep 17 00:00:00 2001 From: tobi-forster Date: Mon, 19 Aug 2024 21:09:36 +0200 Subject: [PATCH 10/27] Bug fixes --- .../equivalence_checker.py | 300 ++++++++++++++++++ 1 file changed, 300 insertions(+) create mode 100644 src/mqt/problemsolver/equivalence_checker/equivalence_checker.py diff --git a/src/mqt/problemsolver/equivalence_checker/equivalence_checker.py b/src/mqt/problemsolver/equivalence_checker/equivalence_checker.py new file mode 100644 index 0000000..7c043a8 --- /dev/null +++ b/src/mqt/problemsolver/equivalence_checker/equivalence_checker.py @@ -0,0 +1,300 @@ +"""This module provides functions to check the equivalence of two circuits using Grover's algorithm.""" +from __future__ import annotations + +import string + +import numpy as np +import pandas as pd +from qiskit import QuantumCircuit +from qiskit.circuit.library import GroverOperator, PhaseOracle +from qiskit.compiler import transpile +from qiskit_aer import AerSimulator + +sim_counts = AerSimulator(method="statevector") + +alphabet = list(string.ascii_lowercase) + + +def create_condition_string(num_bits: int, num_counter_examples: int) -> tuple[str, list[str]]: + """Creates a string to simulate a miter out of bitstring combinations (e.g. '0000' -> 'a & b & c & d'). + + Parameters + ---------- + num_bits : int + Number of input bits + num_counter_examples : int + Number of counter examples + + Returns: + ------- + res_string : str + Resulting condition string + counter_examples : list[str] + The corresponding bitstrings to res_string (e.g. counter_examples is ['0000'] for res_string 'a & b & c & d') + """ + if num_bits < 0 or num_counter_examples < 0: + raise ValueError + + counter_examples: list[str] = [] + if num_counter_examples == 0: + res, _ = create_condition_string(num_bits, 1) + res += " & a" + return res, counter_examples + res_string: str = "" + counter_examples = [] + for num in range(num_counter_examples): + bitstring = list(str(format(num, f"0{num_bits}b")))[::-1] + counter_examples.append(str(format(num, f"0{num_bits}b"))) + for i, char in enumerate(bitstring): + if char == "0" and i == 0: + bitstring[i] = "~" + alphabet[i] + elif char == "1" and i == 0: + bitstring[i] = alphabet[i] + elif char == "0": + bitstring[i] = " & " + "~" + alphabet[i] + elif char == "1": + bitstring[i] = " & " + alphabet[i] + combined_bitstring = "".join(bitstring) + if num < num_counter_examples - 1: + res_string += combined_bitstring + " | " + else: + res_string += combined_bitstring + return res_string, counter_examples + + +def run_parameter_combinations( + miter: str, + counter_examples: list[str], + num_bits: int, + shots: int, + delta: float, +) -> int | None: + """Runs Grover's algorithm to find counter examples for a given miter when knowing the counter examples to test parameters. + + Parameters + ---------- + miter : str + Miter condition string + counter_examples : list[str] + List of counter examples + num_bits : int + Number of input bits + shots : int + Number of shots to run the quantum circuit for + delta : float + Threshold for the stopping condition + + Returns: + ------- + None if no or wrong counter examples were found, else the number of iterations + """ + try: + assert 0 <= delta <= 1 + except ValueError: + print(f"Invalid delta of {delta}. It must be between 0 and 1.") + + total_num_combinations = 2**num_bits + start_iterations = np.floor(np.pi / (4 * np.arcsin((1 / total_num_combinations) ** 0.5)) - 0.5).astype(int) + + total_iterations = 0 + for iterations in reversed(range(1, start_iterations + 1)): + total_iterations += iterations + oracle = PhaseOracle(miter) + + operator = GroverOperator(oracle).decompose() + num_bits = operator.num_qubits + total_num_combinations = 2**num_bits + + qc = QuantumCircuit(num_bits) + qc.h(list(range(num_bits))) + qc.compose(operator.power(iterations).decompose(), inplace=True) + qc.measure_all() + + qc = transpile(qc, sim_counts) + + job = sim_counts.run(qc, shots=shots) + result = job.result() + counts_dict = dict(result.get_counts()) + counts_list = list(counts_dict.values()) + counts_list.sort(reverse=True) + + counts_dict = dict( + sorted(counts_dict.items(), key=lambda item: item[1])[::-1] + ) # Sort state dictionary with respect to values (counts) + + found_counter_examples = [] + stopping_condition = False + for i in range(round(total_num_combinations * 0.5)): + if (i + 1) == len(counts_list): + stopping_condition = True + found_counter_examples_list = counts_list + found_counter_examples_dict = { + list(counts_dict.keys())[t]: list(counts_dict.values())[t] + for t in range(len(found_counter_examples_list)) + } + found_counter_examples = list(found_counter_examples_dict.keys()) + break + + diff = counts_list[i] - counts_list[i + 1] + if diff > counts_list[i] * delta: + stopping_condition = True + found_counter_examples_list = counts_list[: i + 1] + found_counter_examples_dict = { + list(counts_dict.keys())[t]: list(counts_dict.values())[t] + for t in range(len(found_counter_examples_list)) + } + found_counter_examples = list(found_counter_examples_dict.keys()) + break + + if stopping_condition: + break + + if sorted(found_counter_examples) == sorted(counter_examples): + return total_iterations + if len(found_counter_examples) == 0: + if len(counter_examples) > 0: + return None + if len(counter_examples) == 0: + return total_iterations + + return None + + +def try_parameter_combinations( + path: str, + range_deltas: list[float], + range_num_bits: list[int], + range_fraction_counter_examples: list[float], + num_runs: int, + verbose: bool = False, +) -> None: + """Tries different parameter combinations for Grover's algorithm to find the optimal parameters. + + Parameters + ---------- + path : str + Path to save the results + range_deltas : list[float] + List of delta values to try + range_num_bits : list[int] + List of numbers of input bits to try + range_fraction_counter_examples : list[float] + List of fractions of counter examples to try + num_runs : int + Number of runs for each parameter combination + verbose : bool + If True, print the current parameter combination + + """ + data = pd.DataFrame(columns=["Input Bits", "Counter Examples", *range_deltas]) + i = 0 + for num_bits in range_num_bits: + for fraction_counter_examples in range_fraction_counter_examples: + num_counter_examples = round(fraction_counter_examples * 2**num_bits) + row: list[float | int | str] = [num_bits, num_counter_examples] + for delta in range_deltas: + if verbose: + print( + f"num_bits: {num_bits}, fraction_counter_examples: {fraction_counter_examples}, delta: {delta}" + ) + results = [] + for _run in range(num_runs): + miter, counter_examples = create_condition_string(num_bits, num_counter_examples) + result = run_parameter_combinations(miter, counter_examples, num_bits, 8 * (2**num_bits), delta) + results.append(result) + if None in results: + row.append("-") + else: + row.append(float(np.mean(np.asarray(results)))) + data.loc[i] = row + i += 1 + + data.to_csv(path, index=False) + + +def find_counter_examples( + miter: str, + num_bits: int, + shots: int, + delta: float, +) -> list[str | None]: + """Runs Grover's algorithm to find counter examples for a given miter without knowing the counter examples. + + Parameters + ---------- + miter : str + Miter condition string + num_bits : int + Number of input bits + shots : int + Number of shots to run the quantum circuit for + delta : float + Threshold for the stopping condition + + Returns: + ------- + counter_examples: list[str] + List of states that are assumed to be counter examples + """ + try: + assert 0 <= delta <= 1 + except ValueError: + print(f"Invalid delta of {delta}. It must be between 0 and 1.") + + total_num_combinations = 2**num_bits + start_iterations = np.floor(np.pi / (4 * np.arcsin((1 / total_num_combinations) ** 0.5)) - 0.5).astype(int) + + total_iterations = 0 + for iterations in reversed(range(1, start_iterations + 1)): + total_iterations += iterations + oracle = PhaseOracle(miter) + + operator = GroverOperator(oracle).decompose() + num_bits = operator.num_qubits + total_num_combinations = 2**num_bits + + qc = QuantumCircuit(num_bits) + qc.h(list(range(num_bits))) + qc.compose(operator.power(iterations).decompose(), inplace=True) + qc.measure_all() + + qc = transpile(qc, sim_counts) + + job = sim_counts.run(qc, shots=shots) + result = job.result() + counts_dict = dict(result.get_counts()) + counts_list = list(counts_dict.values()) + counts_list.sort(reverse=True) + + counts_dict = dict( + sorted(counts_dict.items(), key=lambda item: item[1])[::-1] + ) # Sort state dictionary with respect to values (counts) + + counter_examples = [] + stopping_condition = False + for i in range(round(total_num_combinations * 0.5)): + if (i + 1) == len(counts_list): + stopping_condition = True + counter_examples_list = counts_list + counter_examples_dict = { + list(counts_dict.keys())[t]: list(counts_dict.values())[t] + for t in range(len(counter_examples_list)) + } + counter_examples = list(counter_examples_dict.keys()) + break + + diff = counts_list[i] - counts_list[i + 1] + if diff > counts_list[i] * delta: + stopping_condition = True + counter_examples_list = counts_list[: i + 1] + counter_examples_dict = { + list(counts_dict.keys())[t]: list(counts_dict.values())[t] + for t in range(len(counter_examples_list)) + } + counter_examples = list(counter_examples_dict.keys()) + break + + if stopping_condition: + break + + return counter_examples \ No newline at end of file From 37b12ea2d8527d0cb00c2201ecdd7c4e0f73fc20 Mon Sep 17 00:00:00 2001 From: tobi-forster Date: Mon, 19 Aug 2024 23:43:36 +0200 Subject: [PATCH 11/27] Bug fixes and Documentation Updates --- README.md | 19 ++ .../equivalence_checker_example.ipynb | 113 +++++++ .../res_equivalence_checker.csv | 21 ++ notebooks/precompilation/evaluation.ipynb | 7 +- src/mqt/problemsolver/csp.py | 9 +- .../equivalence_checker.py | 8 +- .../equivalence_checker/executer.py | 304 ------------------ src/mqt/problemsolver/partialcompiler/qaoa.py | 14 +- .../satellitesolver/ImagingLocation.py | 2 +- .../satellitesolver/algorithms.py | 8 +- .../problemsolver/satellitesolver/utils.py | 32 +- src/mqt/problemsolver/tsp.py | 20 +- tests/test_equivalence_checker.py | 17 +- tests/test_satellitesolver.py | 2 +- 14 files changed, 210 insertions(+), 366 deletions(-) create mode 100644 notebooks/equivalence_checker/equivalence_checker_example.ipynb create mode 100644 notebooks/equivalence_checker/res_equivalence_checker.csv delete mode 100644 src/mqt/problemsolver/equivalence_checker/executer.py diff --git a/README.md b/README.md index 4ac1a4f..601b461 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,7 @@ This repository covers the implementations of multiple research papers in the do 2. [A Hybrid Classical Quantum Computing Approach to the Satellite Mission Planning Problem](#a-hybrid-classical-quantum-computing-approach-to-the-satellite-mission-planning-problem) 3. [Reducing the Compilation Time of Quantum Circuits Using Pre-Compilation on the Gate Level](#reducing-the-compilation-time-of-quantum-circuits-using-pre-compilation-on-the-gate-level) 4. [Utilizing Resource Estimation for the Development of Quantum Computing Applications](#utilizing-resource-estimation-for-the-development-of-quantum-computing-applications) +5. [Towards Equivalence Checking of Classical Circuits Using Quantum Computing](#towards-equivalence-checking-of-classical-circuits-using-quantum-computing) In the following, each implementation is briefly introduced. @@ -117,6 +118,13 @@ In this evaluation, we investigate - different design trade-offs, and - hypothesis on how quantum hardware might improve and how it affects the required resources. +# Towards Equivalence Checking of Classical Circuits Using Quantum Computing + +Equivalence checking, i.e., verifying whether two circuits realize the same functionality or not, is a typical task in the semiconductor industry. Due to the fact, that the designs grow faster than the ability to efficiently verify them, all alternative directions to close the resulting verification gap should be considered. In the `equivalence_checker.py` module, our approach to this problem by utilizing quantum computing is implemented in two versions: + +- With `try_parameter_combinations()` different parameter combinations can be evaluated with miters for which the counter examples are known +- `find_counter_examples()` is used to find counter examples for a miter for which counter examples should be found in the case of non-equivalence + # Usage MQT ProblemSolver is available via [PyPI](https://pypi.org/project/mqt.problemsolver/): @@ -182,6 +190,17 @@ In case you are using our Resources Estimation approach, we would be thankful if } ``` +In case you are using our Equivalence-Checking approach, we would be thankful if you referred to it by citing the following publication: + +```bibtex +@INPROCEEDINGS{quetschlich2024equivalence_checking, + title = {{Towards Equivalence Checking of Classical Circuits Using Quantum Computing}}, + author = {N. Quetschlich and T. Forster and A. Osterwind and D. Helms and R. Wille}, + booktitle = {IEEE International Conference on Quantum Computing and Engineering (QCE)}, + year = {2024}, +} +``` + which is also available on arXiv: [![a](https://img.shields.io/static/v1?label=arXiv&message=2402.12434&color=inactive&style=flat-square)](https://arxiv.org/abs/2402.12434) diff --git a/notebooks/equivalence_checker/equivalence_checker_example.ipynb b/notebooks/equivalence_checker/equivalence_checker_example.ipynb new file mode 100644 index 0000000..70252cb --- /dev/null +++ b/notebooks/equivalence_checker/equivalence_checker_example.ipynb @@ -0,0 +1,113 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Import" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "from mqt.problemsolver.equivalence_checker import equivalence_checker" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Testing different Parameter Combinations for a Miter with known Counter Examples" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [], + "source": [ + "num_bits = 6\n", + "num_counter_examples = 5\n", + "\n", + "# Create synthetic data\n", + "miter, counter_examples = equivalence_checker.create_condition_string(\n", + " num_bits=num_bits,\n", + " num_counter_examples=num_counter_examples\n", + ")\n", + "\n", + "# Generate table with all possible parameter combinations\n", + "# that shows the used Grover iterations if correct counter examples \n", + "# are found in all runs. \n", + "equivalence_checker.try_parameter_combinations(\n", + " path='res_equivalence_checker_test.csv', # Path to save the results\n", + " range_deltas=[0.5,0.7], # Range of \"delta\" values, a threshold parameter introduced in the paper\n", + " range_num_bits=[6,7], # Range of number of bits of the circuits to be verified\n", + " range_fraction_counter_examples=[0,0.01,0.05,0.1,0.2], # Range of fraction of counter examples to be used\n", + " num_runs = 10 # Number of individual runs for each parameter combination\n", + " )" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Find Counter Examples for given Miter and Parameter Combination" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "['000010', '000100', '000011', '000000', '000001']\n" + ] + } + ], + "source": [ + "# Create synthetic data\n", + "miter, _ = equivalence_checker.create_condition_string(\n", + " num_bits=num_bits,\n", + " num_counter_examples=num_counter_examples\n", + ")\n", + "\n", + "# Run the equivalence checker\n", + "counter_examples = equivalence_checker.find_counter_examples(\n", + " miter=miter, # The condition string\n", + " num_bits=num_bits, # Number of bits of the circuits to be verified\n", + " shots=512, # Number of shots for the quantum circuit\n", + " delta=0.7 # Threshold parameter introduced in the paper\n", + ")\n", + "\n", + "print(counter_examples)" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": ".venv", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.11" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/notebooks/equivalence_checker/res_equivalence_checker.csv b/notebooks/equivalence_checker/res_equivalence_checker.csv new file mode 100644 index 0000000..77e8074 --- /dev/null +++ b/notebooks/equivalence_checker/res_equivalence_checker.csv @@ -0,0 +1,21 @@ +Input Bits,Counter Examples,0.1,0.3,0.5,0.7,0.9 +6.0,0.0,-,15.0,15.0,15.0,15.0 +6.0,1.0,5.0,5.0,5.0,5.0,5.0 +6.0,3.0,-,5.0,5.0,5.0,9.0 +6.0,6.0,-,12.0,12.0,12.0,14.0 +6.0,13.0,-,5.0,5.0,5.0,- +7.0,0.0,-,36.0,36.0,36.0,36.0 +7.0,1.0,8.0,8.0,8.0,8.0,8.0 +7.0,6.0,-,8.0,8.0,17.0,30.0 +7.0,13.0,-,8.0,9.0,15.0,15.0 +7.0,26.0,-,8.0,8.0,8.0,8.0 +8.0,0.0,-,78.0,78.0,78.0,78.0 +8.0,3.0,-,12.0,12.0,12.0,23.0 +8.0,13.0,-,12.0,12.0,22.0,23.0 +8.0,26.0,-,12.0,12.0,12.0,12.0 +8.0,51,-,12.0,20.0,23.0,50.0 +9.0,0.0,-,153.0,153.0,153.0,153.0 +9.0,5.0,-,17.0,17.0,17.0,80.0 +9.0,26.0,-,17.0,17.0,17.0,17.0 +9.0,51.0,-,17.0,17.0,17.0,17.0 +9.0,102.0,-,48.0,48.0,48.0,- \ No newline at end of file diff --git a/notebooks/precompilation/evaluation.ipynb b/notebooks/precompilation/evaluation.ipynb index c285896..14183c5 100644 --- a/notebooks/precompilation/evaluation.ipynb +++ b/notebooks/precompilation/evaluation.ipynb @@ -32,10 +32,11 @@ "outputs": [], "source": [ "import math\n", + "from typing import Optional\n", "\n", "\n", "def orderOfMagnitude(number):\n", - " return -math.ceil(math.log(number, 10))" + " return -math.ceil(math.log10(number))" ] }, { @@ -65,7 +66,7 @@ "metadata": {}, "outputs": [], "source": [ - "def label_encoding(row):\n", + "def label_encoding(row) -> Optional[str]:\n", " if row[\"considered_following_qubits\"] == 1:\n", " return \"Only Direct Neighbor\"\n", " if row[\"considered_following_qubits\"] == 1000:\n", @@ -73,7 +74,7 @@ " return None\n", "\n", "\n", - "df_maxcut[\"Encoding Prediction\"] = df_maxcut.apply(lambda row: label_encoding(row), axis=1)" + "df_maxcut[\"Encoding Prediction\"] = df_maxcut.apply(label_encoding, axis=1)" ] }, { diff --git a/src/mqt/problemsolver/csp.py b/src/mqt/problemsolver/csp.py index 045b979..4f2a580 100644 --- a/src/mqt/problemsolver/csp.py +++ b/src/mqt/problemsolver/csp.py @@ -12,9 +12,7 @@ class Constraint(TypedDict, total=False): - """ - Class to store the properties of a single constraint. - """ + """Class to store the properties of a single constraint.""" constraint_type: str operand_one: str @@ -30,7 +28,7 @@ def solve( ) -> tuple[int, int, int, int] | bool: """Method to solve the problem. - Keyword arguments: + Keyword Arguments: constraints -- List of to be satisfied constraints. quantum_algorithm -- Selected quantum algorithm to solve problem. @@ -66,11 +64,10 @@ def print_problem( ) -> None: """Method to visualize the problem. - Keyword arguments: + Keyword Arguments: sum_* -- Sums to be satisfied. a to d -- Variable values satisfying the respective sums. """ - print(" | ", sum_s0, " | ", sum_s1, " | ") print("------------------") print(" ", sum_s2, " | ", a, " | ", b, " |") diff --git a/src/mqt/problemsolver/equivalence_checker/equivalence_checker.py b/src/mqt/problemsolver/equivalence_checker/equivalence_checker.py index 7c043a8..e72847b 100644 --- a/src/mqt/problemsolver/equivalence_checker/equivalence_checker.py +++ b/src/mqt/problemsolver/equivalence_checker/equivalence_checker.py @@ -1,7 +1,9 @@ """This module provides functions to check the equivalence of two circuits using Grover's algorithm.""" + from __future__ import annotations import string +from operator import itemgetter import numpy as np import pandas as pd @@ -119,7 +121,7 @@ def run_parameter_combinations( counts_list.sort(reverse=True) counts_dict = dict( - sorted(counts_dict.items(), key=lambda item: item[1])[::-1] + sorted(counts_dict.items(), key=itemgetter(1))[::-1] ) # Sort state dictionary with respect to values (counts) found_counter_examples = [] @@ -267,7 +269,7 @@ def find_counter_examples( counts_list.sort(reverse=True) counts_dict = dict( - sorted(counts_dict.items(), key=lambda item: item[1])[::-1] + sorted(counts_dict.items(), key=itemgetter(1))[::-1] ) # Sort state dictionary with respect to values (counts) counter_examples = [] @@ -297,4 +299,4 @@ def find_counter_examples( if stopping_condition: break - return counter_examples \ No newline at end of file + return counter_examples diff --git a/src/mqt/problemsolver/equivalence_checker/executer.py b/src/mqt/problemsolver/equivalence_checker/executer.py deleted file mode 100644 index 7d7cee4..0000000 --- a/src/mqt/problemsolver/equivalence_checker/executer.py +++ /dev/null @@ -1,304 +0,0 @@ -from __future__ import annotations - -import string - -import numpy as np -import pandas as pd -from qiskit import QuantumCircuit -from qiskit.circuit.library import GroverOperator, PhaseOracle -from qiskit.compiler import transpile -from qiskit_aer import AerSimulator - -sim_counts = AerSimulator(method="statevector") - -alphabet = list(string.ascii_lowercase) - - -def create_condition_string(num_bits: int, num_counter_examples: int) -> tuple[str, list[str]]: - """ - Creates a string to simulate a miter out of bitstring combinations (e.g. '0000' -> 'a & b & c & d') - - Parameters - ---------- - num_bits : int - Number of input bits - num_counter_examples : int - Number of counter examples - - Returns - ------- - res_string : str - Resulting condition string - counter_examples : list[str] - The corresponding bitstrings to res_string (e.g. counter_examples is ['0000'] for res_string 'a & b & c & d') - """ - - if num_bits < 0 or num_counter_examples < 0: - raise ValueError - - counter_examples: list[str] = [] - if num_counter_examples == 0: - res, _ = create_condition_string(num_bits, 1) - res += " & a" - return res, counter_examples - res_string: str = "" - counter_examples = [] - for num in range(num_counter_examples): - bitstring = list(str(format(num, f"0{num_bits}b")))[::-1] - counter_examples.append(str(format(num, f"0{num_bits}b"))) - for i, char in enumerate(bitstring): - if char == "0" and i == 0: - bitstring[i] = "~" + alphabet[i] - elif char == "1" and i == 0: - bitstring[i] = alphabet[i] - elif char == "0": - bitstring[i] = " & " + "~" + alphabet[i] - elif char == "1": - bitstring[i] = " & " + alphabet[i] - combined_bitstring = "".join(bitstring) - if num < num_counter_examples - 1: - res_string += combined_bitstring + " | " - else: - res_string += combined_bitstring - return res_string, counter_examples - - -def run_parameter_combinations( - miter: str, - counter_examples: list[str], - num_bits: int, - shots: int, - delta: float, -) -> int | None: - """ - Runs Grover's algorithm to find counter examples for a given miter when knowing the counter examples to test parameters - - Parameters - ---------- - miter : str - Miter condition string - counter_examples : list[str] - List of counter examples - num_bits : int - Number of input bits - shots : int - Number of shots to run the quantum circuit for - delta : float - Threshold for the stopping condition - - Returns - ------- - None if no or wrong counter examples were found, else the number of iterations - """ - try: - assert 0 <= delta <= 1 - except ValueError: - print(f"Invalid delta of {delta}. It must be between 0 and 1.") - - total_num_combinations = 2**num_bits - start_iterations = np.floor(np.pi / (4 * np.arcsin((1 / total_num_combinations) ** 0.5)) - 0.5).astype(int) - - total_iterations = 0 - for iterations in reversed(range(1, start_iterations + 1)): - total_iterations += iterations - oracle = PhaseOracle(miter) - - operator = GroverOperator(oracle).decompose() - num_bits = operator.num_qubits - total_num_combinations = 2**num_bits - - qc = QuantumCircuit(num_bits) - qc.h(list(range(num_bits))) - qc.compose(operator.power(iterations).decompose(), inplace=True) - qc.measure_all() - - qc = transpile(qc, sim_counts) - - job = sim_counts.run(qc, shots=shots) - result = job.result() - counts_dict = dict(result.get_counts()) - counts_list = list(counts_dict.values()) - counts_list.sort(reverse=True) - - counts_dict = dict( - sorted(counts_dict.items(), key=lambda item: item[1])[::-1] - ) # Sort state dictionary with respect to values (counts) - - counter_examples = [] - stopping_condition = False - for i in range(round(total_num_combinations * 0.5)): - if (i + 1) == len(counts_list): - stopping_condition = True - counter_examples_list = counts_list - counter_examples_dict = { - list(counts_dict.keys())[t]: list(counts_dict.values())[t] - for t in range(len(counter_examples_list)) - } - counter_examples = list(counter_examples_dict.keys()) - break - - diff = counts_list[i] - counts_list[i + 1] - if diff > counts_list[i] * delta: - stopping_condition = True - counter_examples_list = counts_list[: i + 1] - counter_examples_dict = { - list(counts_dict.keys())[t]: list(counts_dict.values())[t] - for t in range(len(counter_examples_list)) - } - counter_examples = list(counter_examples_dict.keys()) - break - - if stopping_condition: - break - - if sorted(counter_examples) == sorted(counter_examples): - return total_iterations - if len(counter_examples) == 0: - if len(counter_examples) > 0: - return None - if len(counter_examples) == 0: - return total_iterations - - return None - - -def try_parameter_combinations( - path: str, - range_deltas: list[float], - range_num_bits: list[int], - range_fraction_counter_examples: list[float], - num_runs: int, - verbose: bool = False, -) -> None: - """ - Tries different parameter combinations for Grover's algorithm to find the optimal parameters - - Parameters - ---------- - path : str - Path to save the results - range_deltas : list[float] - List of delta values to try - range_num_bits : list[int] - List of numbers of input bits to try - range_fraction_counter_examples : list[float] - List of fractions of counter examples to try - num_runs : int - Number of runs for each parameter combination - verbose : bool - If True, print the current parameter combination - - """ - data = pd.DataFrame(columns=["Input Bits", "Counter Examples", *range_deltas]) - i = 0 - for num_bits in range_num_bits: - for fraction_counter_examples in range_fraction_counter_examples: - num_counter_examples = round(fraction_counter_examples * 2**num_bits) - row: list[float | int | str] = [num_bits, num_counter_examples] - for delta in range_deltas: - if verbose: - print( - f"num_bits: {num_bits}, fraction_counter_examples: {fraction_counter_examples}, delta: {delta}" - ) - results = [] - for _run in range(num_runs): - miter, counter_examples = create_condition_string(num_bits, num_counter_examples) - result = run_parameter_combinations(miter, counter_examples, num_bits, 8 * (2**num_bits), delta) - results.append(result) - if None in results: - row.append("-") - else: - row.append(float(np.mean(np.asarray(results)))) - data.loc[i] = row - i += 1 - - data.to_csv(path, index=False) - - -def find_counter_examples( - miter: str, - num_bits: int, - shots: int, - delta: float, -) -> list[str | None]: - """ - Runs Grover's algorithm to find counter examples for a given miter without knowing the counter examples - - Parameters - ---------- - miter : str - Miter condition string - num_bits : int - Number of input bits - shots : int - Number of shots to run the quantum circuit for - delta : float - Threshold for the stopping condition - - Returns - ------- - counter_examples: list[str] - List of states that are assumed to be counter examples - """ - try: - assert 0 <= delta <= 1 - except ValueError: - print(f"Invalid delta of {delta}. It must be between 0 and 1.") - - total_num_combinations = 2**num_bits - start_iterations = np.floor(np.pi / (4 * np.arcsin((1 / total_num_combinations) ** 0.5)) - 0.5).astype(int) - - total_iterations = 0 - for iterations in reversed(range(1, start_iterations + 1)): - total_iterations += iterations - oracle = PhaseOracle(miter) - - operator = GroverOperator(oracle).decompose() - num_bits = operator.num_qubits - total_num_combinations = 2**num_bits - - qc = QuantumCircuit(num_bits) - qc.h(list(range(num_bits))) - qc.compose(operator.power(iterations).decompose(), inplace=True) - qc.measure_all() - - qc = transpile(qc, sim_counts) - - job = sim_counts.run(qc, shots=shots) - result = job.result() - counts_dict = dict(result.get_counts()) - counts_list = list(counts_dict.values()) - counts_list.sort(reverse=True) - - counts_dict = dict( - sorted(counts_dict.items(), key=lambda item: item[1])[::-1] - ) # Sort state dictionary with respect to values (counts) - - counter_examples = [] - stopping_condition = False - for i in range(round(total_num_combinations * 0.5)): - if (i + 1) == len(counts_list): - stopping_condition = True - counter_examples_list = counts_list - counter_examples_dict = { - list(counts_dict.keys())[t]: list(counts_dict.values())[t] - for t in range(len(counter_examples_list)) - } - counter_examples = list(counter_examples_dict.keys()) - break - - diff = counts_list[i] - counts_list[i + 1] - if diff > counts_list[i] * delta: - stopping_condition = True - counter_examples_list = counts_list[: i + 1] - counter_examples_dict = { - list(counts_dict.keys())[t]: list(counts_dict.values())[t] - for t in range(len(counter_examples_list)) - } - counter_examples = list(counter_examples_dict.keys()) - break - - if stopping_condition: - break - - return counter_examples diff --git a/src/mqt/problemsolver/partialcompiler/qaoa.py b/src/mqt/problemsolver/partialcompiler/qaoa.py index 81393fb..471da59 100644 --- a/src/mqt/problemsolver/partialcompiler/qaoa.py +++ b/src/mqt/problemsolver/partialcompiler/qaoa.py @@ -19,7 +19,7 @@ def __init__( sample_probability: float = 0.5, considered_following_qubits: int = 3, satellite_use_case: bool = False, - ): + ) -> None: self.num_qubits = num_qubits self.repetitions = repetitions @@ -41,8 +41,8 @@ def get_uncompiled_circuits( considered_following_qubits: int, ) -> tuple[QuantumCircuit, QuantumCircuit, list[bool | str], list[tuple[int, int]]]: """Returns the uncompiled circuits (both with only the actual needed two-qubit gates and with all possible - two-qubit gates) and the list of gates to be removed.""" - + two-qubit gates) and the list of gates to be removed. + """ qc = QuantumCircuit(self.num_qubits) # QC with all gates qc_baseline = QuantumCircuit(self.num_qubits) # QC with only the sampled gates qc.h(range(self.num_qubits)) @@ -101,7 +101,7 @@ def get_uncompiled_circuits( return qc, qc_baseline, remove_gates, remove_pairs def compile_qc(self, baseline: bool = False, opt_level: int = 3) -> QuantumCircuit: - """Compiles the circuit""" + """Compiles the circuit.""" circ = self.qc_baseline if baseline else self.qc assert self.backend is not None qc_comp = transpile(circ, backend=self.backend, optimization_level=opt_level, seed_transpiler=42) @@ -110,7 +110,7 @@ def compile_qc(self, baseline: bool = False, opt_level: int = 3) -> QuantumCircu return qc_comp def get_to_be_removed_gate_indices(self) -> list[int]: - """Returns the indices of the gates to be removed""" + """Returns the indices of the gates to be removed.""" indices_to_be_removed_parameterized_gates = [] for i, gate in enumerate(self.qc_compiled._data): if ( @@ -123,7 +123,7 @@ def get_to_be_removed_gate_indices(self) -> list[int]: return indices_to_be_removed_parameterized_gates def remove_unnecessary_gates(self, qc: QuantumCircuit, optimize_swaps: bool = True) -> QuantumCircuit: - """Removes the gates to be checked from the circuit at online time""" + """Removes the gates to be checked from the circuit at online time.""" indices = set() # Iterate over all gates to be removed @@ -165,7 +165,7 @@ def apply_factors_to_qc(self, qc: QuantumCircuit) -> QuantumCircuit: return qc def create_model_from_pair_list(self) -> Model: - """Creates a model from the interaction pairs""" + """Creates a model from the interaction pairs.""" mdl = Model("satellite model") locations = mdl.binary_var_list(self.num_qubits, name="locations") for i, j in self.remove_pairs: diff --git a/src/mqt/problemsolver/satellitesolver/ImagingLocation.py b/src/mqt/problemsolver/satellitesolver/ImagingLocation.py index 5885d56..c0f27af 100644 --- a/src/mqt/problemsolver/satellitesolver/ImagingLocation.py +++ b/src/mqt/problemsolver/satellitesolver/ImagingLocation.py @@ -15,7 +15,7 @@ def __init__( self, position: np.ndarray[Any, np.dtype[np.float64]], imaging_attempt_score: float, - ): + ) -> None: self.position = position self.imaging_attempt = self.get_imaging_attempt() self.imaging_attempt_score = imaging_attempt_score diff --git a/src/mqt/problemsolver/satellitesolver/algorithms.py b/src/mqt/problemsolver/satellitesolver/algorithms.py index 1f9dc8b..d6ab456 100644 --- a/src/mqt/problemsolver/satellitesolver/algorithms.py +++ b/src/mqt/problemsolver/satellitesolver/algorithms.py @@ -29,7 +29,7 @@ def solve_using_w_qaoa(qubo: QuadraticProgram, noisy_flag: bool = False) -> Mini "sampler": Sampler(), } ) - qc_wqaoa, res_wqaoa = wqaoa.get_solution(qubo) + _qc_wqaoa, res_wqaoa = wqaoa.get_solution(qubo) return res_wqaoa @@ -46,7 +46,7 @@ def solve_using_qaoa(qubo: QuadraticProgram, noisy_flag: bool = False) -> Any: "sampler": Sampler(), } ) - qc_qaoa, res_qaoa = qaoa.get_solution(qubo) + _qc_qaoa, res_qaoa = qaoa.get_solution(qubo) return res_qaoa @@ -61,7 +61,7 @@ def solve_using_vqe(qubo: QuadraticProgram, noisy_flag: bool = False) -> Any: "ansatz": RealAmplitudes(num_qubits=qubo.get_num_binary_vars(), reps=3), } ) - qc_vqe, res_vqe = vqe.get_solution(qubo) + _qc_vqe, res_vqe = vqe.get_solution(qubo) return res_vqe @@ -110,7 +110,6 @@ def get_solution(self, qubo: QuadraticProgram) -> tuple[QuantumCircuit, MinimumE class W_QAOA: def __init__(self, W_QAOA_params: dict[str, Any] | None = None, QAOA_params: dict[str, Any] | None = None) -> None: """Function which initializes the QAOA class.""" - if not isinstance(W_QAOA_params, dict): W_QAOA_params = {} if W_QAOA_params.get("pre_solver") is None: @@ -128,7 +127,6 @@ def __init__(self, W_QAOA_params: dict[str, Any] | None = None, QAOA_params: dic def get_solution(self, qubo: QuadraticProgram) -> tuple[QuantumCircuit, MinimumEigensolverResult]: """Function which returns the quantum circuit of the W-QAOA algorithm and the resulting solution.""" - ws_qaoa = WarmStartQAOAOptimizer(**self.W_QAOA_params) res = ws_qaoa.solve(qubo) qc = self.W_QAOA_params["qaoa"].ansatz diff --git a/src/mqt/problemsolver/satellitesolver/utils.py b/src/mqt/problemsolver/satellitesolver/utils.py index 81bb694..7a55e74 100644 --- a/src/mqt/problemsolver/satellitesolver/utils.py +++ b/src/mqt/problemsolver/satellitesolver/utils.py @@ -23,7 +23,7 @@ def init_random_location_requests(n: int) -> list[LocationRequest]: - """Returns list of n random acquisition requests""" + """Returns list of n random acquisition requests.""" np.random.seed(10) acquisition_requests = [ LocationRequest(position=create_acquisition_position(), imaging_attempt_score=np.random.randint(1, 3)) @@ -39,16 +39,14 @@ def get_success_ratio(ac_reqs: list[LocationRequest], qubo: QuadraticProgram, so exact_mes = NumPyMinimumEigensolver() exact_result = MinimumEigenOptimizer(exact_mes).solve(qubo).fval # sum over all LocationRequests and sum over their imaging_attempt_score if the respective indicator in sol[index] is 1 - solution_vector = solution_vector[::-1] + solution_vector.reverse() return cast( float, ( sum( - [ - -ac_req.imaging_attempt_score - for ac_req, index in zip(ac_reqs, range(len(ac_reqs))) - if solution_vector[index] == 1 - ] + -ac_req.imaging_attempt_score + for ac_req, index in zip(ac_reqs, range(len(ac_reqs))) + if solution_vector[index] == 1 ) / exact_result ), @@ -64,13 +62,11 @@ def create_acquisition_position( if latitude is None: latitude = np.random.uniform(np.pi / 2 - 15 / 360 * 2 * np.pi, np.pi / 2 + 15 / 360 * 2 * np.pi) - res = R_E * np.array( - [ - np.cos(longitude) * np.sin(latitude), - np.sin(longitude) * np.sin(latitude), - np.cos(latitude), - ] - ) + res = R_E * np.array([ + np.cos(longitude) * np.sin(latitude), + np.sin(longitude) * np.sin(latitude), + np.cos(latitude), + ]) return cast(np.ndarray[Any, np.dtype[np.float64]], res) @@ -89,7 +85,7 @@ def calc_needed_time_between_acquisition_attempts( def transition_possible(acq_1: LocationRequest, acq_2: LocationRequest) -> bool: - """Returns True if transition between acq_1 and acq_2 is possible, False otherwise""" + """Returns True if transition between acq_1 and acq_2 is possible, False otherwise.""" t_maneuver = cast(float, calc_needed_time_between_acquisition_attempts(acq_1, acq_2)) t1 = acq_1.imaging_attempt t2 = acq_2.imaging_attempt @@ -151,7 +147,7 @@ def sample_most_likely(state_vector: dict[str, int]) -> list[int]: def check_solution(ac_reqs: list[LocationRequest], solution_vector: list[int]) -> bool: """Checks if the determined solution is valid and does not violate any constraints.""" - solution_vector = solution_vector[::-1] + solution_vector.reverse() for i in range(len(ac_reqs) - 1): for j in range(i + 1, len(ac_reqs)): if (solution_vector[i] + solution_vector[j] == 2) and not transition_possible(ac_reqs[i], ac_reqs[j]): @@ -160,7 +156,7 @@ def check_solution(ac_reqs: list[LocationRequest], solution_vector: list[int]) - def create_satellite_doxplex(all_acqs: list[LocationRequest]) -> Model: - """Returns a doxplex model for the satellite problem""" + """Returns a doxplex model for the satellite problem.""" mdl = Model("satellite model") # Create binary variables for each acquisition request requests = mdl.binary_var_list(len(all_acqs), name="location") @@ -180,7 +176,7 @@ def create_satellite_doxplex(all_acqs: list[LocationRequest]) -> Model: def convert_docplex_to_qubo(model: Model, penalty: int | None = None) -> QuadraticProgram: - """Converts a docplex model to a qubo""" + """Converts a docplex model to a qubo.""" return QuadraticProgramToQubo(penalty=penalty).convert(from_docplex_mp(model)) diff --git a/src/mqt/problemsolver/tsp.py b/src/mqt/problemsolver/tsp.py index c807b14..001da55 100644 --- a/src/mqt/problemsolver/tsp.py +++ b/src/mqt/problemsolver/tsp.py @@ -20,7 +20,7 @@ class TSP: def print_problem(self, solution: list[int] | None = None) -> None: """Method to visualize the problem. - Keyword arguments: + Keyword Arguments: solution -- If provided, the solution is visualized. Otherwise, the problem without solution is shown. """ @@ -111,7 +111,7 @@ def solve( ) -> list[int] | bool: """Method to solve the problem. - Keyword arguments: + Keyword Arguments: dist_*_* -- Defining the adjacency matrix by the distances between the vertices. objective_function -- Optimization goal. quantum_algorithm -- Selected quantum algorithm to solve problem. @@ -219,15 +219,13 @@ def simulate(self, qc: QuantumCircuit) -> str: return cast(str, count.most_frequent()) def get_classical_result(self) -> list[int]: - distance_matrix = np.array( - [ - [0, self.dist_1_2, self.dist_1_3, self.dist_1_4], - [self.dist_1_2, 0, self.dist_2_3, self.dist_2_4], - [self.dist_1_3, self.dist_2_3, 0, self.dist_3_4], - [self.dist_1_4, self.dist_1_3, self.dist_3_4, 0], - ] - ) - permutation, distance = solve_tsp_dynamic_programming(distance_matrix) + distance_matrix = np.array([ + [0, self.dist_1_2, self.dist_1_3, self.dist_1_4], + [self.dist_1_2, 0, self.dist_2_3, self.dist_2_4], + [self.dist_1_3, self.dist_2_3, 0, self.dist_3_4], + [self.dist_1_4, self.dist_1_3, self.dist_3_4, 0], + ]) + permutation, _distance = solve_tsp_dynamic_programming(distance_matrix) return cast(list[int], (np.array(permutation) + 1).T) diff --git a/tests/test_equivalence_checker.py b/tests/test_equivalence_checker.py index 35c7eaf..2a30f97 100644 --- a/tests/test_equivalence_checker.py +++ b/tests/test_equivalence_checker.py @@ -1,15 +1,16 @@ +"""Test the equivalence_checker.py module.""" + from __future__ import annotations import string -from mqt.problemsolver.equivalence_checker import executer +from mqt.problemsolver.equivalence_checker import equivalence_checker alphabet = list(string.ascii_lowercase) def create_condition_string(num_qubits: int, num_counter_examples: int) -> tuple[str, list[str]]: - """ - Creates a string to simulate a miter out of bitstring combinations (e.g. '0000' -> 'a & b & c & d') + """Creates a string to simulate a miter out of bitstring combinations (e.g. '0000' -> 'a & b & c & d'). Parameters ---------- @@ -18,14 +19,13 @@ def create_condition_string(num_qubits: int, num_counter_examples: int) -> tuple num_counter_examples : int Number of counter examples - Returns + Returns: ------- res_string : str Resulting condition string counter_examples : list[str] The corresponding bitstrings to res_string (e.g. counter_examples is ['0000'] for res_string 'a & b & c & d') """ - if num_qubits < 0 or num_counter_examples < 0: raise ValueError @@ -59,6 +59,7 @@ def create_condition_string(num_qubits: int, num_counter_examples: int) -> tuple if __name__ == "__main__": def test_create_condition_string() -> None: + """Test the function create_condition_string.""" num_qubits = 3 num_counter_examples = 2 res_string, counter_examples = create_condition_string( @@ -71,6 +72,7 @@ def test_create_condition_string() -> None: assert res_string == "~a & ~b & ~c | a & ~b & ~c" def test_run_paramter_combinations() -> None: + """Test the function run_parameter_combinations.""" num_qubits = 6 num_counter_examples = 3 res_string, counter_examples = create_condition_string( @@ -78,12 +80,13 @@ def test_run_paramter_combinations() -> None: ) shots = 512 delta = 0.7 - result = executer.run_parameter_combinations( + result = equivalence_checker.run_parameter_combinations( miter=res_string, counter_examples=counter_examples, num_bits=num_qubits, shots=shots, delta=delta ) assert result == 5 def test_find_counter_examples() -> None: + """Test the function find_counter_examples.""" num_qubits = 8 num_counter_examples = 10 res_string, counter_examples = create_condition_string( @@ -91,7 +94,7 @@ def test_find_counter_examples() -> None: ) shots = 512 delta = 0.7 - found_counter_examples = executer.find_counter_examples( + found_counter_examples = equivalence_checker.find_counter_examples( miter=res_string, num_bits=num_qubits, shots=shots, delta=delta ) found_counter_examples.sort() diff --git a/tests/test_satellitesolver.py b/tests/test_satellitesolver.py index 192d935..ca8c7c9 100644 --- a/tests/test_satellitesolver.py +++ b/tests/test_satellitesolver.py @@ -13,7 +13,7 @@ from qiskit_optimization import QuadraticProgram -@pytest.fixture() +@pytest.fixture def qubo() -> QuadraticProgram: ac_reqs = utils.init_random_location_requests(3) mdl = utils.create_satellite_doxplex(ac_reqs) From ed9ec98d4e1d433fe10d369e482cb168b619f707 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 19 Aug 2024 21:46:44 +0000 Subject: [PATCH 12/27] =?UTF-8?q?=F0=9F=8E=A8=20pre-commit=20fixes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../equivalence_checker_example.ipynb | 51 +++++++------------ .../res_equivalence_checker.csv | 2 +- .../problemsolver/satellitesolver/utils.py | 12 +++-- src/mqt/problemsolver/tsp.py | 14 ++--- 4 files changed, 35 insertions(+), 44 deletions(-) diff --git a/notebooks/equivalence_checker/equivalence_checker_example.ipynb b/notebooks/equivalence_checker/equivalence_checker_example.ipynb index 70252cb..8adec24 100644 --- a/notebooks/equivalence_checker/equivalence_checker_example.ipynb +++ b/notebooks/equivalence_checker/equivalence_checker_example.ipynb @@ -9,7 +9,7 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -25,7 +25,7 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -34,20 +34,19 @@ "\n", "# Create synthetic data\n", "miter, counter_examples = equivalence_checker.create_condition_string(\n", - " num_bits=num_bits,\n", - " num_counter_examples=num_counter_examples\n", + " num_bits=num_bits, num_counter_examples=num_counter_examples\n", ")\n", "\n", "# Generate table with all possible parameter combinations\n", - "# that shows the used Grover iterations if correct counter examples \n", - "# are found in all runs. \n", + "# that shows the used Grover iterations if correct counter examples\n", + "# are found in all runs.\n", "equivalence_checker.try_parameter_combinations(\n", - " path='res_equivalence_checker_test.csv', # Path to save the results\n", - " range_deltas=[0.5,0.7], # Range of \"delta\" values, a threshold parameter introduced in the paper\n", - " range_num_bits=[6,7], # Range of number of bits of the circuits to be verified\n", - " range_fraction_counter_examples=[0,0.01,0.05,0.1,0.2], # Range of fraction of counter examples to be used\n", - " num_runs = 10 # Number of individual runs for each parameter combination\n", - " )" + " path=\"res_equivalence_checker_test.csv\", # Path to save the results\n", + " range_deltas=[0.5, 0.7], # Range of \"delta\" values, a threshold parameter introduced in the paper\n", + " range_num_bits=[6, 7], # Range of number of bits of the circuits to be verified\n", + " range_fraction_counter_examples=[0, 0.01, 0.05, 0.1, 0.2], # Range of fraction of counter examples to be used\n", + " num_runs=10, # Number of individual runs for each parameter combination\n", + ")" ] }, { @@ -59,30 +58,19 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "['000010', '000100', '000011', '000000', '000001']\n" - ] - } - ], + "outputs": [], "source": [ "# Create synthetic data\n", - "miter, _ = equivalence_checker.create_condition_string(\n", - " num_bits=num_bits,\n", - " num_counter_examples=num_counter_examples\n", - ")\n", + "miter, _ = equivalence_checker.create_condition_string(num_bits=num_bits, num_counter_examples=num_counter_examples)\n", "\n", "# Run the equivalence checker\n", "counter_examples = equivalence_checker.find_counter_examples(\n", - " miter=miter, # The condition string\n", - " num_bits=num_bits, # Number of bits of the circuits to be verified\n", - " shots=512, # Number of shots for the quantum circuit\n", - " delta=0.7 # Threshold parameter introduced in the paper\n", + " miter=miter, # The condition string\n", + " num_bits=num_bits, # Number of bits of the circuits to be verified\n", + " shots=512, # Number of shots for the quantum circuit\n", + " delta=0.7, # Threshold parameter introduced in the paper\n", ")\n", "\n", "print(counter_examples)" @@ -104,8 +92,7 @@ "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.10.11" + "pygments_lexer": "ipython3" } }, "nbformat": 4, diff --git a/notebooks/equivalence_checker/res_equivalence_checker.csv b/notebooks/equivalence_checker/res_equivalence_checker.csv index 77e8074..39d5df6 100644 --- a/notebooks/equivalence_checker/res_equivalence_checker.csv +++ b/notebooks/equivalence_checker/res_equivalence_checker.csv @@ -18,4 +18,4 @@ Input Bits,Counter Examples,0.1,0.3,0.5,0.7,0.9 9.0,5.0,-,17.0,17.0,17.0,80.0 9.0,26.0,-,17.0,17.0,17.0,17.0 9.0,51.0,-,17.0,17.0,17.0,17.0 -9.0,102.0,-,48.0,48.0,48.0,- \ No newline at end of file +9.0,102.0,-,48.0,48.0,48.0,- diff --git a/src/mqt/problemsolver/satellitesolver/utils.py b/src/mqt/problemsolver/satellitesolver/utils.py index 7a55e74..583f741 100644 --- a/src/mqt/problemsolver/satellitesolver/utils.py +++ b/src/mqt/problemsolver/satellitesolver/utils.py @@ -62,11 +62,13 @@ def create_acquisition_position( if latitude is None: latitude = np.random.uniform(np.pi / 2 - 15 / 360 * 2 * np.pi, np.pi / 2 + 15 / 360 * 2 * np.pi) - res = R_E * np.array([ - np.cos(longitude) * np.sin(latitude), - np.sin(longitude) * np.sin(latitude), - np.cos(latitude), - ]) + res = R_E * np.array( + [ + np.cos(longitude) * np.sin(latitude), + np.sin(longitude) * np.sin(latitude), + np.cos(latitude), + ] + ) return cast(np.ndarray[Any, np.dtype[np.float64]], res) diff --git a/src/mqt/problemsolver/tsp.py b/src/mqt/problemsolver/tsp.py index 001da55..4f0a876 100644 --- a/src/mqt/problemsolver/tsp.py +++ b/src/mqt/problemsolver/tsp.py @@ -219,12 +219,14 @@ def simulate(self, qc: QuantumCircuit) -> str: return cast(str, count.most_frequent()) def get_classical_result(self) -> list[int]: - distance_matrix = np.array([ - [0, self.dist_1_2, self.dist_1_3, self.dist_1_4], - [self.dist_1_2, 0, self.dist_2_3, self.dist_2_4], - [self.dist_1_3, self.dist_2_3, 0, self.dist_3_4], - [self.dist_1_4, self.dist_1_3, self.dist_3_4, 0], - ]) + distance_matrix = np.array( + [ + [0, self.dist_1_2, self.dist_1_3, self.dist_1_4], + [self.dist_1_2, 0, self.dist_2_3, self.dist_2_4], + [self.dist_1_3, self.dist_2_3, 0, self.dist_3_4], + [self.dist_1_4, self.dist_1_3, self.dist_3_4, 0], + ] + ) permutation, _distance = solve_tsp_dynamic_programming(distance_matrix) return cast(list[int], (np.array(permutation) + 1).T) From c79a5cd69e1fe56c12d59e9a597d24563a806e02 Mon Sep 17 00:00:00 2001 From: tobi-forster Date: Tue, 20 Aug 2024 09:43:10 +0200 Subject: [PATCH 13/27] Updated tests --- .../equivalence_checker_example.ipynb | 51 +++---- notebooks/precompilation/evaluation.ipynb | 5 +- tests/test_equivalence_checker.py | 135 ++++++------------ 3 files changed, 66 insertions(+), 125 deletions(-) diff --git a/notebooks/equivalence_checker/equivalence_checker_example.ipynb b/notebooks/equivalence_checker/equivalence_checker_example.ipynb index 70252cb..8adec24 100644 --- a/notebooks/equivalence_checker/equivalence_checker_example.ipynb +++ b/notebooks/equivalence_checker/equivalence_checker_example.ipynb @@ -9,7 +9,7 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -25,7 +25,7 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -34,20 +34,19 @@ "\n", "# Create synthetic data\n", "miter, counter_examples = equivalence_checker.create_condition_string(\n", - " num_bits=num_bits,\n", - " num_counter_examples=num_counter_examples\n", + " num_bits=num_bits, num_counter_examples=num_counter_examples\n", ")\n", "\n", "# Generate table with all possible parameter combinations\n", - "# that shows the used Grover iterations if correct counter examples \n", - "# are found in all runs. \n", + "# that shows the used Grover iterations if correct counter examples\n", + "# are found in all runs.\n", "equivalence_checker.try_parameter_combinations(\n", - " path='res_equivalence_checker_test.csv', # Path to save the results\n", - " range_deltas=[0.5,0.7], # Range of \"delta\" values, a threshold parameter introduced in the paper\n", - " range_num_bits=[6,7], # Range of number of bits of the circuits to be verified\n", - " range_fraction_counter_examples=[0,0.01,0.05,0.1,0.2], # Range of fraction of counter examples to be used\n", - " num_runs = 10 # Number of individual runs for each parameter combination\n", - " )" + " path=\"res_equivalence_checker_test.csv\", # Path to save the results\n", + " range_deltas=[0.5, 0.7], # Range of \"delta\" values, a threshold parameter introduced in the paper\n", + " range_num_bits=[6, 7], # Range of number of bits of the circuits to be verified\n", + " range_fraction_counter_examples=[0, 0.01, 0.05, 0.1, 0.2], # Range of fraction of counter examples to be used\n", + " num_runs=10, # Number of individual runs for each parameter combination\n", + ")" ] }, { @@ -59,30 +58,19 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "['000010', '000100', '000011', '000000', '000001']\n" - ] - } - ], + "outputs": [], "source": [ "# Create synthetic data\n", - "miter, _ = equivalence_checker.create_condition_string(\n", - " num_bits=num_bits,\n", - " num_counter_examples=num_counter_examples\n", - ")\n", + "miter, _ = equivalence_checker.create_condition_string(num_bits=num_bits, num_counter_examples=num_counter_examples)\n", "\n", "# Run the equivalence checker\n", "counter_examples = equivalence_checker.find_counter_examples(\n", - " miter=miter, # The condition string\n", - " num_bits=num_bits, # Number of bits of the circuits to be verified\n", - " shots=512, # Number of shots for the quantum circuit\n", - " delta=0.7 # Threshold parameter introduced in the paper\n", + " miter=miter, # The condition string\n", + " num_bits=num_bits, # Number of bits of the circuits to be verified\n", + " shots=512, # Number of shots for the quantum circuit\n", + " delta=0.7, # Threshold parameter introduced in the paper\n", ")\n", "\n", "print(counter_examples)" @@ -104,8 +92,7 @@ "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.10.11" + "pygments_lexer": "ipython3" } }, "nbformat": 4, diff --git a/notebooks/precompilation/evaluation.ipynb b/notebooks/precompilation/evaluation.ipynb index 14183c5..9e50376 100644 --- a/notebooks/precompilation/evaluation.ipynb +++ b/notebooks/precompilation/evaluation.ipynb @@ -31,8 +31,9 @@ "metadata": {}, "outputs": [], "source": [ + "from __future__ import annotations\n", + "\n", "import math\n", - "from typing import Optional\n", "\n", "\n", "def orderOfMagnitude(number):\n", @@ -66,7 +67,7 @@ "metadata": {}, "outputs": [], "source": [ - "def label_encoding(row) -> Optional[str]:\n", + "def label_encoding(row) -> str | None:\n", " if row[\"considered_following_qubits\"] == 1:\n", " return \"Only Direct Neighbor\"\n", " if row[\"considered_following_qubits\"] == 1000:\n", diff --git a/tests/test_equivalence_checker.py b/tests/test_equivalence_checker.py index 2a30f97..4fd866c 100644 --- a/tests/test_equivalence_checker.py +++ b/tests/test_equivalence_checker.py @@ -9,94 +9,47 @@ alphabet = list(string.ascii_lowercase) -def create_condition_string(num_qubits: int, num_counter_examples: int) -> tuple[str, list[str]]: - """Creates a string to simulate a miter out of bitstring combinations (e.g. '0000' -> 'a & b & c & d'). - - Parameters - ---------- - num_qubits : int - Number of input bits - num_counter_examples : int - Number of counter examples - - Returns: - ------- - res_string : str - Resulting condition string - counter_examples : list[str] - The corresponding bitstrings to res_string (e.g. counter_examples is ['0000'] for res_string 'a & b & c & d') - """ - if num_qubits < 0 or num_counter_examples < 0: - raise ValueError - - counter_examples: list[str] = [] - if num_counter_examples == 0: - res, _ = create_condition_string(num_qubits, 1) - res += " & a" - return res, counter_examples - res_string: str = "" - counter_examples = [] - for num in range(num_counter_examples): - bitstring = list(str(format(num, f"0{num_qubits}b")))[::-1] - counter_examples.append(str(format(num, f"0{num_qubits}b"))) - for i, char in enumerate(bitstring): - if char == "0" and i == 0: - bitstring[i] = "~" + alphabet[i] - elif char == "1" and i == 0: - bitstring[i] = alphabet[i] - elif char == "0": - bitstring[i] = " & " + "~" + alphabet[i] - elif char == "1": - bitstring[i] = " & " + alphabet[i] - combined_bitstring = "".join(bitstring) - if num < num_counter_examples - 1: - res_string += combined_bitstring + " | " - else: - res_string += combined_bitstring - return res_string, counter_examples - - -if __name__ == "__main__": - - def test_create_condition_string() -> None: - """Test the function create_condition_string.""" - num_qubits = 3 - num_counter_examples = 2 - res_string, counter_examples = create_condition_string( - num_qubits=num_qubits, num_counter_examples=num_counter_examples - ) - assert isinstance(res_string, str) - assert isinstance(counter_examples, list) - assert len(res_string) == 26 - assert len(counter_examples) == num_counter_examples - assert res_string == "~a & ~b & ~c | a & ~b & ~c" - - def test_run_paramter_combinations() -> None: - """Test the function run_parameter_combinations.""" - num_qubits = 6 - num_counter_examples = 3 - res_string, counter_examples = create_condition_string( - num_qubits=num_qubits, num_counter_examples=num_counter_examples - ) - shots = 512 - delta = 0.7 - result = equivalence_checker.run_parameter_combinations( - miter=res_string, counter_examples=counter_examples, num_bits=num_qubits, shots=shots, delta=delta - ) - assert result == 5 - - def test_find_counter_examples() -> None: - """Test the function find_counter_examples.""" - num_qubits = 8 - num_counter_examples = 10 - res_string, counter_examples = create_condition_string( - num_qubits=num_qubits, num_counter_examples=num_counter_examples - ) - shots = 512 - delta = 0.7 - found_counter_examples = equivalence_checker.find_counter_examples( - miter=res_string, num_bits=num_qubits, shots=shots, delta=delta - ) - found_counter_examples.sort() - counter_examples.sort() - assert found_counter_examples == counter_examples +def test_create_condition_string() -> None: + """Test the function create_condition_string.""" + num_bits = 3 + num_counter_examples = 2 + res_string, counter_examples = equivalence_checker.create_condition_string( + num_bits=num_bits, num_counter_examples=num_counter_examples + ) + assert isinstance(res_string, str) + assert isinstance(counter_examples, list) + assert len(res_string) == 26 + assert len(counter_examples) == num_counter_examples + assert res_string == "~a & ~b & ~c | a & ~b & ~c" + + +def test_run_paramter_combinations() -> None: + """Test the function run_parameter_combinations.""" + num_bits = 6 + num_counter_examples = 3 + res_string, counter_examples = equivalence_checker.create_condition_string( + num_bits=num_bits, num_counter_examples=num_counter_examples + ) + shots = 512 + delta = 0.7 + result = equivalence_checker.run_parameter_combinations( + miter=res_string, counter_examples=counter_examples, num_bits=num_bits, shots=shots, delta=delta + ) + assert result == 5 + + +def test_find_counter_examples() -> None: + """Test the function find_counter_examples.""" + num_bits = 8 + num_counter_examples = 10 + res_string, counter_examples = equivalence_checker.create_condition_string( + num_bits=num_bits, num_counter_examples=num_counter_examples + ) + shots = 512 + delta = 0.7 + found_counter_examples = equivalence_checker.find_counter_examples( + miter=res_string, num_bits=num_bits, shots=shots, delta=delta + ) + found_counter_examples.sort() + counter_examples.sort() + assert found_counter_examples == counter_examples From 1e0214711544901a2ccd81219c038576485ee424 Mon Sep 17 00:00:00 2001 From: tobi-forster Date: Tue, 20 Aug 2024 14:38:30 +0200 Subject: [PATCH 14/27] Updated tests --- .../equivalence_checker_example.ipynb | 4 +-- .../problemsolver/satellitesolver/utils.py | 12 ++++---- src/mqt/problemsolver/tsp.py | 14 ++++----- tests/test_equivalence_checker.py | 30 +++++++++++++++++++ 4 files changed, 43 insertions(+), 17 deletions(-) diff --git a/notebooks/equivalence_checker/equivalence_checker_example.ipynb b/notebooks/equivalence_checker/equivalence_checker_example.ipynb index 8adec24..3f8fd8d 100644 --- a/notebooks/equivalence_checker/equivalence_checker_example.ipynb +++ b/notebooks/equivalence_checker/equivalence_checker_example.ipynb @@ -42,8 +42,8 @@ "# are found in all runs.\n", "equivalence_checker.try_parameter_combinations(\n", " path=\"res_equivalence_checker_test.csv\", # Path to save the results\n", - " range_deltas=[0.5, 0.7], # Range of \"delta\" values, a threshold parameter introduced in the paper\n", - " range_num_bits=[6, 7], # Range of number of bits of the circuits to be verified\n", + " range_deltas=[0.1, 0.3, 0.5, 0.7, 0.9], # Range of \"delta\" values, a threshold parameter introduced in the paper\n", + " range_num_bits=[6, 7, 8, 9], # Range of number of bits of the circuits to be verified\n", " range_fraction_counter_examples=[0, 0.01, 0.05, 0.1, 0.2], # Range of fraction of counter examples to be used\n", " num_runs=10, # Number of individual runs for each parameter combination\n", ")" diff --git a/src/mqt/problemsolver/satellitesolver/utils.py b/src/mqt/problemsolver/satellitesolver/utils.py index 583f741..7a55e74 100644 --- a/src/mqt/problemsolver/satellitesolver/utils.py +++ b/src/mqt/problemsolver/satellitesolver/utils.py @@ -62,13 +62,11 @@ def create_acquisition_position( if latitude is None: latitude = np.random.uniform(np.pi / 2 - 15 / 360 * 2 * np.pi, np.pi / 2 + 15 / 360 * 2 * np.pi) - res = R_E * np.array( - [ - np.cos(longitude) * np.sin(latitude), - np.sin(longitude) * np.sin(latitude), - np.cos(latitude), - ] - ) + res = R_E * np.array([ + np.cos(longitude) * np.sin(latitude), + np.sin(longitude) * np.sin(latitude), + np.cos(latitude), + ]) return cast(np.ndarray[Any, np.dtype[np.float64]], res) diff --git a/src/mqt/problemsolver/tsp.py b/src/mqt/problemsolver/tsp.py index 4f0a876..001da55 100644 --- a/src/mqt/problemsolver/tsp.py +++ b/src/mqt/problemsolver/tsp.py @@ -219,14 +219,12 @@ def simulate(self, qc: QuantumCircuit) -> str: return cast(str, count.most_frequent()) def get_classical_result(self) -> list[int]: - distance_matrix = np.array( - [ - [0, self.dist_1_2, self.dist_1_3, self.dist_1_4], - [self.dist_1_2, 0, self.dist_2_3, self.dist_2_4], - [self.dist_1_3, self.dist_2_3, 0, self.dist_3_4], - [self.dist_1_4, self.dist_1_3, self.dist_3_4, 0], - ] - ) + distance_matrix = np.array([ + [0, self.dist_1_2, self.dist_1_3, self.dist_1_4], + [self.dist_1_2, 0, self.dist_2_3, self.dist_2_4], + [self.dist_1_3, self.dist_2_3, 0, self.dist_3_4], + [self.dist_1_4, self.dist_1_3, self.dist_3_4, 0], + ]) permutation, _distance = solve_tsp_dynamic_programming(distance_matrix) return cast(list[int], (np.array(permutation) + 1).T) diff --git a/tests/test_equivalence_checker.py b/tests/test_equivalence_checker.py index 4fd866c..52d1ca5 100644 --- a/tests/test_equivalence_checker.py +++ b/tests/test_equivalence_checker.py @@ -3,12 +3,23 @@ from __future__ import annotations import string +from pathlib import Path + +import pytest from mqt.problemsolver.equivalence_checker import equivalence_checker alphabet = list(string.ascii_lowercase) +@pytest.fixture +def output_path() -> str: + """Fixture to create the output path for the tests.""" + output_path = Path("./tests/test_output/") + output_path.mkdir(parents=True, exist_ok=True) + return str(output_path) + + def test_create_condition_string() -> None: """Test the function create_condition_string.""" num_bits = 3 @@ -38,6 +49,17 @@ def test_run_paramter_combinations() -> None: assert result == 5 +def test_try_parameter_combinations(output_path: str) -> None: + """Test the function try_parameter_combinations.""" + equivalence_checker.try_parameter_combinations( + path=output_path, + range_deltas=[0.7, 0.8], + range_num_bits=[5], + range_fraction_counter_examples=[0.1, 0.2], + num_runs=5, + ) + + def test_find_counter_examples() -> None: """Test the function find_counter_examples.""" num_bits = 8 @@ -53,3 +75,11 @@ def test_find_counter_examples() -> None: found_counter_examples.sort() counter_examples.sort() assert found_counter_examples == counter_examples + + +def test_configure_end(output_path: str) -> None: + """Removes all temporarily created files while testing.""" + # delete all files in the test directory and the directory itself + for f in Path(output_path).iterdir(): + f.unlink() + Path(output_path).rmdir() From 9bf9010ab068ac544709e3e20cf397f2930325a4 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 20 Aug 2024 12:38:40 +0000 Subject: [PATCH 15/27] =?UTF-8?q?=F0=9F=8E=A8=20pre-commit=20fixes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/mqt/problemsolver/satellitesolver/utils.py | 12 +++++++----- src/mqt/problemsolver/tsp.py | 14 ++++++++------ 2 files changed, 15 insertions(+), 11 deletions(-) diff --git a/src/mqt/problemsolver/satellitesolver/utils.py b/src/mqt/problemsolver/satellitesolver/utils.py index 7a55e74..583f741 100644 --- a/src/mqt/problemsolver/satellitesolver/utils.py +++ b/src/mqt/problemsolver/satellitesolver/utils.py @@ -62,11 +62,13 @@ def create_acquisition_position( if latitude is None: latitude = np.random.uniform(np.pi / 2 - 15 / 360 * 2 * np.pi, np.pi / 2 + 15 / 360 * 2 * np.pi) - res = R_E * np.array([ - np.cos(longitude) * np.sin(latitude), - np.sin(longitude) * np.sin(latitude), - np.cos(latitude), - ]) + res = R_E * np.array( + [ + np.cos(longitude) * np.sin(latitude), + np.sin(longitude) * np.sin(latitude), + np.cos(latitude), + ] + ) return cast(np.ndarray[Any, np.dtype[np.float64]], res) diff --git a/src/mqt/problemsolver/tsp.py b/src/mqt/problemsolver/tsp.py index 001da55..4f0a876 100644 --- a/src/mqt/problemsolver/tsp.py +++ b/src/mqt/problemsolver/tsp.py @@ -219,12 +219,14 @@ def simulate(self, qc: QuantumCircuit) -> str: return cast(str, count.most_frequent()) def get_classical_result(self) -> list[int]: - distance_matrix = np.array([ - [0, self.dist_1_2, self.dist_1_3, self.dist_1_4], - [self.dist_1_2, 0, self.dist_2_3, self.dist_2_4], - [self.dist_1_3, self.dist_2_3, 0, self.dist_3_4], - [self.dist_1_4, self.dist_1_3, self.dist_3_4, 0], - ]) + distance_matrix = np.array( + [ + [0, self.dist_1_2, self.dist_1_3, self.dist_1_4], + [self.dist_1_2, 0, self.dist_2_3, self.dist_2_4], + [self.dist_1_3, self.dist_2_3, 0, self.dist_3_4], + [self.dist_1_4, self.dist_1_3, self.dist_3_4, 0], + ] + ) permutation, _distance = solve_tsp_dynamic_programming(distance_matrix) return cast(list[int], (np.array(permutation) + 1).T) From 77ebcf86733a451b42edc02563ebc68b65f61eae Mon Sep 17 00:00:00 2001 From: tobi-forster Date: Tue, 20 Aug 2024 16:15:18 +0200 Subject: [PATCH 16/27] Updated tests --- pyproject.toml | 2 +- tests/test_equivalence_checker.py | 29 ++++++++--------------------- 2 files changed, 9 insertions(+), 22 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 362e34c..2728974 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -83,7 +83,7 @@ explicit_package_bases = true pretty = true [[tool.mypy.overrides]] -module = ["qiskit.*", "matplotlib.*", "python_tsp.*", "networkx.*", "mqt.ddsim.*", "joblib.*", "qiskit_optimization.*", "docplex.*", "qiskit_aer.*"] +module = ["qiskit.*", "matplotlib.*", "python_tsp.*", "networkx.*", "mqt.ddsim.*", "joblib.*", "qiskit_optimization.*", "docplex.*", "qiskit_aer.*", "py.*"] ignore_missing_imports = true [tool.ruff] diff --git a/tests/test_equivalence_checker.py b/tests/test_equivalence_checker.py index 52d1ca5..a529df8 100644 --- a/tests/test_equivalence_checker.py +++ b/tests/test_equivalence_checker.py @@ -3,21 +3,14 @@ from __future__ import annotations import string -from pathlib import Path - -import pytest +from typing import TYPE_CHECKING from mqt.problemsolver.equivalence_checker import equivalence_checker -alphabet = list(string.ascii_lowercase) - +if TYPE_CHECKING: + import py -@pytest.fixture -def output_path() -> str: - """Fixture to create the output path for the tests.""" - output_path = Path("./tests/test_output/") - output_path.mkdir(parents=True, exist_ok=True) - return str(output_path) +alphabet = list(string.ascii_lowercase) def test_create_condition_string() -> None: @@ -49,15 +42,17 @@ def test_run_paramter_combinations() -> None: assert result == 5 -def test_try_parameter_combinations(output_path: str) -> None: +def test_try_parameter_combinations(tmpdir: py.path.local) -> None: """Test the function try_parameter_combinations.""" + p = tmpdir.mkdir("sub") equivalence_checker.try_parameter_combinations( - path=output_path, + path=(p / "test.csv"), range_deltas=[0.7, 0.8], range_num_bits=[5], range_fraction_counter_examples=[0.1, 0.2], num_runs=5, ) + assert len(tmpdir.listdir()) == 1 def test_find_counter_examples() -> None: @@ -75,11 +70,3 @@ def test_find_counter_examples() -> None: found_counter_examples.sort() counter_examples.sort() assert found_counter_examples == counter_examples - - -def test_configure_end(output_path: str) -> None: - """Removes all temporarily created files while testing.""" - # delete all files in the test directory and the directory itself - for f in Path(output_path).iterdir(): - f.unlink() - Path(output_path).rmdir() From 7b6266a19132341a623525c420318d52946f9977 Mon Sep 17 00:00:00 2001 From: tobi-forster Date: Wed, 21 Aug 2024 15:00:45 +0200 Subject: [PATCH 17/27] Reset unwanted changes --- notebooks/precompilation/evaluation.ipynb | 10 ++++----- src/mqt/problemsolver/csp.py | 11 ++++++---- src/mqt/problemsolver/partialcompiler/qaoa.py | 16 +++++++------- .../satellitesolver/ImagingLocation.py | 4 ++-- .../satellitesolver/algorithms.py | 10 +++++---- .../problemsolver/satellitesolver/utils.py | 22 ++++++++++--------- src/mqt/problemsolver/tsp.py | 8 +++---- 7 files changed, 44 insertions(+), 37 deletions(-) diff --git a/notebooks/precompilation/evaluation.ipynb b/notebooks/precompilation/evaluation.ipynb index 9e50376..7deeec5 100644 --- a/notebooks/precompilation/evaluation.ipynb +++ b/notebooks/precompilation/evaluation.ipynb @@ -15,6 +15,8 @@ "metadata": {}, "outputs": [], "source": [ + "from __future__ import annotations\n", + "\n", "import pandas as pd\n", "\n", "%matplotlib inline\n", @@ -31,13 +33,11 @@ "metadata": {}, "outputs": [], "source": [ - "from __future__ import annotations\n", - "\n", "import math\n", "\n", "\n", "def orderOfMagnitude(number):\n", - " return -math.ceil(math.log10(number))" + " return -math.ceil(math.log(number, 10))" ] }, { @@ -67,7 +67,7 @@ "metadata": {}, "outputs": [], "source": [ - "def label_encoding(row) -> str | None:\n", + "def label_encoding(row):\n", " if row[\"considered_following_qubits\"] == 1:\n", " return \"Only Direct Neighbor\"\n", " if row[\"considered_following_qubits\"] == 1000:\n", @@ -75,7 +75,7 @@ " return None\n", "\n", "\n", - "df_maxcut[\"Encoding Prediction\"] = df_maxcut.apply(label_encoding, axis=1)" + "df_maxcut[\"Encoding Prediction\"] = df_maxcut.apply(lambda row: label_encoding(row), axis=1)" ] }, { diff --git a/src/mqt/problemsolver/csp.py b/src/mqt/problemsolver/csp.py index 4f2a580..b268f94 100644 --- a/src/mqt/problemsolver/csp.py +++ b/src/mqt/problemsolver/csp.py @@ -12,7 +12,9 @@ class Constraint(TypedDict, total=False): - """Class to store the properties of a single constraint.""" + """ + Class to store the properties of a single constraint. + """ constraint_type: str operand_one: str @@ -28,7 +30,7 @@ def solve( ) -> tuple[int, int, int, int] | bool: """Method to solve the problem. - Keyword Arguments: + Keyword arguments: constraints -- List of to be satisfied constraints. quantum_algorithm -- Selected quantum algorithm to solve problem. @@ -64,10 +66,11 @@ def print_problem( ) -> None: """Method to visualize the problem. - Keyword Arguments: + Keyword arguments: sum_* -- Sums to be satisfied. a to d -- Variable values satisfying the respective sums. """ + print(" | ", sum_s0, " | ", sum_s1, " | ") print("------------------") print(" ", sum_s2, " | ", a, " | ", b, " |") @@ -395,4 +398,4 @@ def get_kakuro_constraints(self, sum_s0: int, sum_s1: int, sum_s2: int, sum_s3: "operand_two": "d", } list_of_constraints.append(constraint_8) - return list_of_constraints + return list_of_constraints \ No newline at end of file diff --git a/src/mqt/problemsolver/partialcompiler/qaoa.py b/src/mqt/problemsolver/partialcompiler/qaoa.py index 471da59..f78a13f 100644 --- a/src/mqt/problemsolver/partialcompiler/qaoa.py +++ b/src/mqt/problemsolver/partialcompiler/qaoa.py @@ -19,7 +19,7 @@ def __init__( sample_probability: float = 0.5, considered_following_qubits: int = 3, satellite_use_case: bool = False, - ) -> None: + ): self.num_qubits = num_qubits self.repetitions = repetitions @@ -41,8 +41,8 @@ def get_uncompiled_circuits( considered_following_qubits: int, ) -> tuple[QuantumCircuit, QuantumCircuit, list[bool | str], list[tuple[int, int]]]: """Returns the uncompiled circuits (both with only the actual needed two-qubit gates and with all possible - two-qubit gates) and the list of gates to be removed. - """ + two-qubit gates) and the list of gates to be removed.""" + qc = QuantumCircuit(self.num_qubits) # QC with all gates qc_baseline = QuantumCircuit(self.num_qubits) # QC with only the sampled gates qc.h(range(self.num_qubits)) @@ -101,7 +101,7 @@ def get_uncompiled_circuits( return qc, qc_baseline, remove_gates, remove_pairs def compile_qc(self, baseline: bool = False, opt_level: int = 3) -> QuantumCircuit: - """Compiles the circuit.""" + """Compiles the circuit""" circ = self.qc_baseline if baseline else self.qc assert self.backend is not None qc_comp = transpile(circ, backend=self.backend, optimization_level=opt_level, seed_transpiler=42) @@ -110,7 +110,7 @@ def compile_qc(self, baseline: bool = False, opt_level: int = 3) -> QuantumCircu return qc_comp def get_to_be_removed_gate_indices(self) -> list[int]: - """Returns the indices of the gates to be removed.""" + """Returns the indices of the gates to be removed""" indices_to_be_removed_parameterized_gates = [] for i, gate in enumerate(self.qc_compiled._data): if ( @@ -123,7 +123,7 @@ def get_to_be_removed_gate_indices(self) -> list[int]: return indices_to_be_removed_parameterized_gates def remove_unnecessary_gates(self, qc: QuantumCircuit, optimize_swaps: bool = True) -> QuantumCircuit: - """Removes the gates to be checked from the circuit at online time.""" + """Removes the gates to be checked from the circuit at online time""" indices = set() # Iterate over all gates to be removed @@ -165,7 +165,7 @@ def apply_factors_to_qc(self, qc: QuantumCircuit) -> QuantumCircuit: return qc def create_model_from_pair_list(self) -> Model: - """Creates a model from the interaction pairs.""" + """Creates a model from the interaction pairs""" mdl = Model("satellite model") locations = mdl.binary_var_list(self.num_qubits, name="locations") for i, j in self.remove_pairs: @@ -187,4 +187,4 @@ def get_backend(num_qubits: int) -> FakeBackend: if num_qubits <= washington.configuration().n_qubits: return washington - return None + return None \ No newline at end of file diff --git a/src/mqt/problemsolver/satellitesolver/ImagingLocation.py b/src/mqt/problemsolver/satellitesolver/ImagingLocation.py index c0f27af..0d296f0 100644 --- a/src/mqt/problemsolver/satellitesolver/ImagingLocation.py +++ b/src/mqt/problemsolver/satellitesolver/ImagingLocation.py @@ -15,7 +15,7 @@ def __init__( self, position: np.ndarray[Any, np.dtype[np.float64]], imaging_attempt_score: float, - ) -> None: + ): self.position = position self.imaging_attempt = self.get_imaging_attempt() self.imaging_attempt_score = imaging_attempt_score @@ -81,4 +81,4 @@ def get_coordinates(self) -> tuple[str, str]: str(int(lat)) + "° " + str(int(60 * (lat % 1))) + "' " + str(int(60 * ((10 * lat) % 1))) + "'' " + "S" ) - return latitude, longitude + return latitude, longitude \ No newline at end of file diff --git a/src/mqt/problemsolver/satellitesolver/algorithms.py b/src/mqt/problemsolver/satellitesolver/algorithms.py index d6ab456..7b83ded 100644 --- a/src/mqt/problemsolver/satellitesolver/algorithms.py +++ b/src/mqt/problemsolver/satellitesolver/algorithms.py @@ -29,7 +29,7 @@ def solve_using_w_qaoa(qubo: QuadraticProgram, noisy_flag: bool = False) -> Mini "sampler": Sampler(), } ) - _qc_wqaoa, res_wqaoa = wqaoa.get_solution(qubo) + qc_wqaoa, res_wqaoa = wqaoa.get_solution(qubo) return res_wqaoa @@ -46,7 +46,7 @@ def solve_using_qaoa(qubo: QuadraticProgram, noisy_flag: bool = False) -> Any: "sampler": Sampler(), } ) - _qc_qaoa, res_qaoa = qaoa.get_solution(qubo) + qc_qaoa, res_qaoa = qaoa.get_solution(qubo) return res_qaoa @@ -61,7 +61,7 @@ def solve_using_vqe(qubo: QuadraticProgram, noisy_flag: bool = False) -> Any: "ansatz": RealAmplitudes(num_qubits=qubo.get_num_binary_vars(), reps=3), } ) - _qc_vqe, res_vqe = vqe.get_solution(qubo) + qc_vqe, res_vqe = vqe.get_solution(qubo) return res_vqe @@ -110,6 +110,7 @@ def get_solution(self, qubo: QuadraticProgram) -> tuple[QuantumCircuit, MinimumE class W_QAOA: def __init__(self, W_QAOA_params: dict[str, Any] | None = None, QAOA_params: dict[str, Any] | None = None) -> None: """Function which initializes the QAOA class.""" + if not isinstance(W_QAOA_params, dict): W_QAOA_params = {} if W_QAOA_params.get("pre_solver") is None: @@ -127,8 +128,9 @@ def __init__(self, W_QAOA_params: dict[str, Any] | None = None, QAOA_params: dic def get_solution(self, qubo: QuadraticProgram) -> tuple[QuantumCircuit, MinimumEigensolverResult]: """Function which returns the quantum circuit of the W-QAOA algorithm and the resulting solution.""" + ws_qaoa = WarmStartQAOAOptimizer(**self.W_QAOA_params) res = ws_qaoa.solve(qubo) qc = self.W_QAOA_params["qaoa"].ansatz - return qc, res + return qc, res \ No newline at end of file diff --git a/src/mqt/problemsolver/satellitesolver/utils.py b/src/mqt/problemsolver/satellitesolver/utils.py index 583f741..297d259 100644 --- a/src/mqt/problemsolver/satellitesolver/utils.py +++ b/src/mqt/problemsolver/satellitesolver/utils.py @@ -23,7 +23,7 @@ def init_random_location_requests(n: int) -> list[LocationRequest]: - """Returns list of n random acquisition requests.""" + """Returns list of n random acquisition requests""" np.random.seed(10) acquisition_requests = [ LocationRequest(position=create_acquisition_position(), imaging_attempt_score=np.random.randint(1, 3)) @@ -39,14 +39,16 @@ def get_success_ratio(ac_reqs: list[LocationRequest], qubo: QuadraticProgram, so exact_mes = NumPyMinimumEigensolver() exact_result = MinimumEigenOptimizer(exact_mes).solve(qubo).fval # sum over all LocationRequests and sum over their imaging_attempt_score if the respective indicator in sol[index] is 1 - solution_vector.reverse() + solution_vector = solution_vector[::-1] return cast( float, ( sum( - -ac_req.imaging_attempt_score - for ac_req, index in zip(ac_reqs, range(len(ac_reqs))) - if solution_vector[index] == 1 + [ + -ac_req.imaging_attempt_score + for ac_req, index in zip(ac_reqs, range(len(ac_reqs))) + if solution_vector[index] == 1 + ] ) / exact_result ), @@ -87,7 +89,7 @@ def calc_needed_time_between_acquisition_attempts( def transition_possible(acq_1: LocationRequest, acq_2: LocationRequest) -> bool: - """Returns True if transition between acq_1 and acq_2 is possible, False otherwise.""" + """Returns True if transition between acq_1 and acq_2 is possible, False otherwise""" t_maneuver = cast(float, calc_needed_time_between_acquisition_attempts(acq_1, acq_2)) t1 = acq_1.imaging_attempt t2 = acq_2.imaging_attempt @@ -149,7 +151,7 @@ def sample_most_likely(state_vector: dict[str, int]) -> list[int]: def check_solution(ac_reqs: list[LocationRequest], solution_vector: list[int]) -> bool: """Checks if the determined solution is valid and does not violate any constraints.""" - solution_vector.reverse() + solution_vector = solution_vector[::-1] for i in range(len(ac_reqs) - 1): for j in range(i + 1, len(ac_reqs)): if (solution_vector[i] + solution_vector[j] == 2) and not transition_possible(ac_reqs[i], ac_reqs[j]): @@ -158,7 +160,7 @@ def check_solution(ac_reqs: list[LocationRequest], solution_vector: list[int]) - def create_satellite_doxplex(all_acqs: list[LocationRequest]) -> Model: - """Returns a doxplex model for the satellite problem.""" + """Returns a doxplex model for the satellite problem""" mdl = Model("satellite model") # Create binary variables for each acquisition request requests = mdl.binary_var_list(len(all_acqs), name="location") @@ -178,11 +180,11 @@ def create_satellite_doxplex(all_acqs: list[LocationRequest]) -> Model: def convert_docplex_to_qubo(model: Model, penalty: int | None = None) -> QuadraticProgram: - """Converts a docplex model to a qubo.""" + """Converts a docplex model to a qubo""" return QuadraticProgramToQubo(penalty=penalty).convert(from_docplex_mp(model)) def get_longitude(vector: np.ndarray[Any, np.dtype[np.float64]]) -> float: temp = vector * np.array([1, 1, 0]) temp /= np.linalg.norm(temp) - return cast(float, np.arccos(temp[0]) if temp[1] >= 0 else 2 * np.pi - np.arccos(temp[0])) + return cast(float, np.arccos(temp[0]) if temp[1] >= 0 else 2 * np.pi - np.arccos(temp[0])) \ No newline at end of file diff --git a/src/mqt/problemsolver/tsp.py b/src/mqt/problemsolver/tsp.py index 4f0a876..0ac2cca 100644 --- a/src/mqt/problemsolver/tsp.py +++ b/src/mqt/problemsolver/tsp.py @@ -20,7 +20,7 @@ class TSP: def print_problem(self, solution: list[int] | None = None) -> None: """Method to visualize the problem. - Keyword Arguments: + Keyword arguments: solution -- If provided, the solution is visualized. Otherwise, the problem without solution is shown. """ @@ -111,7 +111,7 @@ def solve( ) -> list[int] | bool: """Method to solve the problem. - Keyword Arguments: + Keyword arguments: dist_*_* -- Defining the adjacency matrix by the distances between the vertices. objective_function -- Optimization goal. quantum_algorithm -- Selected quantum algorithm to solve problem. @@ -227,7 +227,7 @@ def get_classical_result(self) -> list[int]: [self.dist_1_4, self.dist_1_3, self.dist_3_4, 0], ] ) - permutation, _distance = solve_tsp_dynamic_programming(distance_matrix) + permutation, distance = solve_tsp_dynamic_programming(distance_matrix) return cast(list[int], (np.array(permutation) + 1).T) @@ -335,4 +335,4 @@ def get_available_quantum_algorithms(self) -> list[str]: def show_classical_solution(self) -> None: """Method to visualize the solution of a classical solver.""" - self.print_problem(solution=self.get_classical_result()) + self.print_problem(solution=self.get_classical_result()) \ No newline at end of file From 69a5758d379f3e43489c39e7ec16c3b2acaf1901 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 21 Aug 2024 19:27:36 +0000 Subject: [PATCH 18/27] =?UTF-8?q?=F0=9F=8E=A8=20pre-commit=20fixes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/mqt/problemsolver/csp.py | 2 +- src/mqt/problemsolver/partialcompiler/qaoa.py | 2 +- src/mqt/problemsolver/satellitesolver/ImagingLocation.py | 2 +- src/mqt/problemsolver/satellitesolver/algorithms.py | 2 +- src/mqt/problemsolver/satellitesolver/utils.py | 2 +- src/mqt/problemsolver/tsp.py | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/mqt/problemsolver/csp.py b/src/mqt/problemsolver/csp.py index b268f94..045b979 100644 --- a/src/mqt/problemsolver/csp.py +++ b/src/mqt/problemsolver/csp.py @@ -398,4 +398,4 @@ def get_kakuro_constraints(self, sum_s0: int, sum_s1: int, sum_s2: int, sum_s3: "operand_two": "d", } list_of_constraints.append(constraint_8) - return list_of_constraints \ No newline at end of file + return list_of_constraints diff --git a/src/mqt/problemsolver/partialcompiler/qaoa.py b/src/mqt/problemsolver/partialcompiler/qaoa.py index f78a13f..81393fb 100644 --- a/src/mqt/problemsolver/partialcompiler/qaoa.py +++ b/src/mqt/problemsolver/partialcompiler/qaoa.py @@ -187,4 +187,4 @@ def get_backend(num_qubits: int) -> FakeBackend: if num_qubits <= washington.configuration().n_qubits: return washington - return None \ No newline at end of file + return None diff --git a/src/mqt/problemsolver/satellitesolver/ImagingLocation.py b/src/mqt/problemsolver/satellitesolver/ImagingLocation.py index 0d296f0..5885d56 100644 --- a/src/mqt/problemsolver/satellitesolver/ImagingLocation.py +++ b/src/mqt/problemsolver/satellitesolver/ImagingLocation.py @@ -81,4 +81,4 @@ def get_coordinates(self) -> tuple[str, str]: str(int(lat)) + "° " + str(int(60 * (lat % 1))) + "' " + str(int(60 * ((10 * lat) % 1))) + "'' " + "S" ) - return latitude, longitude \ No newline at end of file + return latitude, longitude diff --git a/src/mqt/problemsolver/satellitesolver/algorithms.py b/src/mqt/problemsolver/satellitesolver/algorithms.py index 7b83ded..1f9dc8b 100644 --- a/src/mqt/problemsolver/satellitesolver/algorithms.py +++ b/src/mqt/problemsolver/satellitesolver/algorithms.py @@ -133,4 +133,4 @@ def get_solution(self, qubo: QuadraticProgram) -> tuple[QuantumCircuit, MinimumE res = ws_qaoa.solve(qubo) qc = self.W_QAOA_params["qaoa"].ansatz - return qc, res \ No newline at end of file + return qc, res diff --git a/src/mqt/problemsolver/satellitesolver/utils.py b/src/mqt/problemsolver/satellitesolver/utils.py index 297d259..81bb694 100644 --- a/src/mqt/problemsolver/satellitesolver/utils.py +++ b/src/mqt/problemsolver/satellitesolver/utils.py @@ -187,4 +187,4 @@ def convert_docplex_to_qubo(model: Model, penalty: int | None = None) -> Quadrat def get_longitude(vector: np.ndarray[Any, np.dtype[np.float64]]) -> float: temp = vector * np.array([1, 1, 0]) temp /= np.linalg.norm(temp) - return cast(float, np.arccos(temp[0]) if temp[1] >= 0 else 2 * np.pi - np.arccos(temp[0])) \ No newline at end of file + return cast(float, np.arccos(temp[0]) if temp[1] >= 0 else 2 * np.pi - np.arccos(temp[0])) diff --git a/src/mqt/problemsolver/tsp.py b/src/mqt/problemsolver/tsp.py index 0ac2cca..c807b14 100644 --- a/src/mqt/problemsolver/tsp.py +++ b/src/mqt/problemsolver/tsp.py @@ -335,4 +335,4 @@ def get_available_quantum_algorithms(self) -> list[str]: def show_classical_solution(self) -> None: """Method to visualize the solution of a classical solver.""" - self.print_problem(solution=self.get_classical_result()) \ No newline at end of file + self.print_problem(solution=self.get_classical_result()) From 6d04af49cbf207d68081b15ad17048bcbde8c7bd Mon Sep 17 00:00:00 2001 From: tobi-forster Date: Thu, 22 Aug 2024 15:29:28 +0200 Subject: [PATCH 19/27] Changed tweedledum to optional dependency and updated tests --- .github/workflows/coverage.yml | 2 +- pyproject.toml | 5 ++-- .../equivalence_checker.py | 21 ++++++++++---- tests/test_equivalence_checker.py | 29 +++++++++++++++---- 4 files changed, 42 insertions(+), 15 deletions(-) diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index 233805f..2b0eff2 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -17,7 +17,7 @@ jobs: with: python-version: "3.9" - name: Install MQT ProblemSolver - run: pip install .[coverage] + run: pip install .[coverage, tweedledum] - name: Generate Report run: pytest -v --cov --cov-config=pyproject.toml --cov-report=xml - name: Upload coverage to Codecov diff --git a/pyproject.toml b/pyproject.toml index 2728974..e4cea96 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -28,7 +28,6 @@ dependencies = [ "python_tsp", "docplex", "qiskit_optimization", - "tweedledum==1.0.0", "qiskit_aer", "pandas" ] @@ -44,6 +43,7 @@ classifiers = [ "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", "Intended Audience :: Science/Research", "Natural Language :: English", "Topic :: Scientific/Engineering :: Electronic Design Automation (EDA)", @@ -53,6 +53,7 @@ classifiers = [ test = ["pytest>=7"] coverage = ["mqt.problemsolver[test]", "coverage[toml]~=6.5.0", "pytest-cov~=4.0.0"] dev = ["mqt.problemsolver[coverage]"] +tweedledum = ["tweedledum==1.0.0"] [project.urls] Homepage = "https://github.com/cda-tum/mqtproblemsolver" @@ -83,7 +84,7 @@ explicit_package_bases = true pretty = true [[tool.mypy.overrides]] -module = ["qiskit.*", "matplotlib.*", "python_tsp.*", "networkx.*", "mqt.ddsim.*", "joblib.*", "qiskit_optimization.*", "docplex.*", "qiskit_aer.*", "py.*"] +module = ["qiskit.*", "matplotlib.*", "python_tsp.*", "networkx.*", "mqt.ddsim.*", "joblib.*", "qiskit_optimization.*", "docplex.*", "qiskit_aer.*"] ignore_missing_imports = true [tool.ruff] diff --git a/src/mqt/problemsolver/equivalence_checker/equivalence_checker.py b/src/mqt/problemsolver/equivalence_checker/equivalence_checker.py index e72847b..9f328a4 100644 --- a/src/mqt/problemsolver/equivalence_checker/equivalence_checker.py +++ b/src/mqt/problemsolver/equivalence_checker/equivalence_checker.py @@ -10,6 +10,8 @@ from qiskit import QuantumCircuit from qiskit.circuit.library import GroverOperator, PhaseOracle from qiskit.compiler import transpile + +# from qiskit.utils import optionals as _optionals from qiskit_aer import AerSimulator sim_counts = AerSimulator(method="statevector") @@ -34,8 +36,13 @@ def create_condition_string(num_bits: int, num_counter_examples: int) -> tuple[s counter_examples : list[str] The corresponding bitstrings to res_string (e.g. counter_examples is ['0000'] for res_string 'a & b & c & d') """ - if num_bits < 0 or num_counter_examples < 0: - raise ValueError + try: + assert num_bits > 0 + assert num_counter_examples >= 0 + except AssertionError as e: + print("Number of bits and number of counter examples must be greater than 0.") + msg = "The number of bits or counter examples cannot be used." + raise ValueError(msg) from e counter_examples: list[str] = [] if num_counter_examples == 0: @@ -92,8 +99,9 @@ def run_parameter_combinations( """ try: assert 0 <= delta <= 1 - except ValueError: - print(f"Invalid delta of {delta}. It must be between 0 and 1.") + except AssertionError as e: + msg = f"Invalid value for delta {delta}, which must be between 0 and 1." + raise ValueError(msg) from e total_num_combinations = 2**num_bits start_iterations = np.floor(np.pi / (4 * np.arcsin((1 / total_num_combinations) ** 0.5)) - 0.5).astype(int) @@ -240,8 +248,9 @@ def find_counter_examples( """ try: assert 0 <= delta <= 1 - except ValueError: - print(f"Invalid delta of {delta}. It must be between 0 and 1.") + except AssertionError as e: + msg = f"Invalid value for delta {delta}, which must be between 0 and 1." + raise ValueError(msg) from e total_num_combinations = 2**num_bits start_iterations = np.floor(np.pi / (4 * np.arcsin((1 / total_num_combinations) ** 0.5)) - 0.5).astype(int) diff --git a/tests/test_equivalence_checker.py b/tests/test_equivalence_checker.py index a529df8..f72fd30 100644 --- a/tests/test_equivalence_checker.py +++ b/tests/test_equivalence_checker.py @@ -2,13 +2,16 @@ from __future__ import annotations +import importlib import string from typing import TYPE_CHECKING +import pytest + from mqt.problemsolver.equivalence_checker import equivalence_checker if TYPE_CHECKING: - import py + from pathlib import Path alphabet = list(string.ascii_lowercase) @@ -26,7 +29,11 @@ def test_create_condition_string() -> None: assert len(counter_examples) == num_counter_examples assert res_string == "~a & ~b & ~c | a & ~b & ~c" + with pytest.raises(ValueError, match="The number of bits or counter examples cannot be used."): + equivalence_checker.create_condition_string(num_bits=-5, num_counter_examples=-2) + +@pytest.mark.skipif(not importlib.util.find_spec("tweedledum"), reason="tweedledum is not installed") def test_run_paramter_combinations() -> None: """Test the function run_parameter_combinations.""" num_bits = 6 @@ -40,21 +47,31 @@ def test_run_paramter_combinations() -> None: miter=res_string, counter_examples=counter_examples, num_bits=num_bits, shots=shots, delta=delta ) assert result == 5 + with pytest.raises(ValueError, match="Invalid value for delta 1.2, which must be between 0 and 1."): + equivalence_checker.run_parameter_combinations( + miter=res_string, counter_examples=counter_examples, num_bits=num_bits, shots=shots, delta=1.2 + ) -def test_try_parameter_combinations(tmpdir: py.path.local) -> None: +@pytest.mark.skipif(not importlib.util.find_spec("tweedledum"), reason="tweedledum is not installed") +def test_try_parameter_combinations(tmp_path: Path) -> None: """Test the function try_parameter_combinations.""" - p = tmpdir.mkdir("sub") + d = tmp_path / "sub" + d.mkdir() + d = d / "test1.csv" + d = d.absolute() + string_path = d.as_posix() equivalence_checker.try_parameter_combinations( - path=(p / "test.csv"), + path=string_path, range_deltas=[0.7, 0.8], range_num_bits=[5], - range_fraction_counter_examples=[0.1, 0.2], + range_fraction_counter_examples=[0.00, 0.05, 0.10, 0.20], num_runs=5, ) - assert len(tmpdir.listdir()) == 1 + assert len(list(tmp_path.iterdir())) == 1 +@pytest.mark.skipif(not importlib.util.find_spec("tweedledum"), reason="tweedledum is not installed") def test_find_counter_examples() -> None: """Test the function find_counter_examples.""" num_bits = 8 From 9d7abd6d97a23c1ddc9ccca6043034a57de8230c Mon Sep 17 00:00:00 2001 From: tobi-forster Date: Fri, 23 Aug 2024 13:53:22 +0200 Subject: [PATCH 20/27] Optimizations --- README.md | 19 +- img/miter_structure.png | Bin 0 -> 264607 bytes .../equivalence_checking_example.ipynb} | 38 ++-- .../res_equivalence_checker.csv | 0 res_satellite_solver.csv | 2 - .../equivalence_checker.py | 198 ++++++------------ tests/test_equivalence_checker.py | 39 +--- 7 files changed, 107 insertions(+), 189 deletions(-) create mode 100644 img/miter_structure.png rename notebooks/{equivalence_checker/equivalence_checker_example.ipynb => equivalence_checking/equivalence_checking_example.ipynb} (76%) rename notebooks/{equivalence_checker => equivalence_checking}/res_equivalence_checker.csv (100%) delete mode 100644 res_satellite_solver.csv diff --git a/README.md b/README.md index a06bde5..4773f3f 100644 --- a/README.md +++ b/README.md @@ -120,10 +120,18 @@ In this evaluation, we investigate # Towards Equivalence Checking of Classical Circuits Using Quantum Computing -Equivalence checking, i.e., verifying whether two circuits realize the same functionality or not, is a typical task in the semiconductor industry. Due to the fact, that the designs grow faster than the ability to efficiently verify them, all alternative directions to close the resulting verification gap should be considered. In the `equivalence_checker.py` module, our approach to this problem by utilizing quantum computing is implemented in two versions: +Equivalence checking, i.e., verifying whether two circuits realize the same functionality or not, is a typical task in the semiconductor industry. Due to the fact, that the designs grow faster than the ability to efficiently verify them, all alternative directions to close the resulting verification gap should be considered. In this work, we consider the problem through the miter structure. Here, two circuits to be checked are applied with the same primary inputs. Then, for each pair of to-be-equal output bits, an exclusive-OR (XOR) gate is applied-evaluating to 1 if the two outputs generate different values (which only happens in the case of non-equivalence). By OR-ing the outputs of all these XOR gates, eventually an indicator results that shows whether both circuits are equivalent. Then, the goal is to determine an input assignment so that this indicator evaluates to 1 (providing a counter example that shows non-equivalence) or to prove that no such assignment exists (proving equivalence). -- With `try_parameter_combinations()` different parameter combinations can be evaluated with miters for which the counter examples are known -- `find_counter_examples()` is used to find counter examples for a miter for which counter examples should be found in the case of non-equivalence +

+ +

+ +In the `equivalence_checker.py` module, our approach to this problem by utilizing quantum computing is implemented. There are two different ways to run this code. + +- One to test, how well certain parameter combinations work. The parameters consist of the number of bits of the circuits to be verified, the threshold parameter delta (which is explained in detail in the paper), the fraction of input combinations that induce non-equivalence of the circuits (further called "counter examples"), the number of shots to run the quantum circuit for and the number of individual runs of the experiment. Multiple parameter combinations can be tested and exported as a .csv-file at a provided location. +- A second one to actually input a miter expression (in form of a string) together with some parameters independent from the miter (shots and delta) and use our approach to find the counter examples (if the circuits are non-equivalent). + +These two implementations are provided by the functions `try_parameter_combinations()` and `find_counter_examples()`, respectively. Examples for their usages are shown in `notebooks/equivalence_checking/example.ipynb`. # Usage @@ -196,6 +204,9 @@ In case you are using our Resources Estimation approach, we would be thankful if } ``` +which is also available on arXiv: +[![a](https://img.shields.io/static/v1?label=arXiv&message=2402.12434&color=inactive&style=flat-square)](https://arxiv.org/abs/2402.12434) + In case you are using our Equivalence-Checking approach, we would be thankful if you referred to it by citing the following publication: ```bibtex @@ -208,7 +219,7 @@ In case you are using our Equivalence-Checking approach, we would be thankful if ``` which is also available on arXiv: -[![a](https://img.shields.io/static/v1?label=arXiv&message=2402.12434&color=inactive&style=flat-square)](https://arxiv.org/abs/2402.12434) +[![a]() ## Acknowledgements diff --git a/img/miter_structure.png b/img/miter_structure.png new file mode 100644 index 0000000000000000000000000000000000000000..4b5a052f6c5d38d0692af21505a2fc986950de98 GIT binary patch literal 264607 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𝔯KrC$c?*&i31F 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

|A3Pp;rcV>!qjvQ$D=ey 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[!{^7lQ^=;1 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%0A4Gr0R8?Ve9$q>^HCwMdsVfn)K)^M0(iwkCr+Z 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

A4L_t(co&#gD#D9V^)ae>$J?(J)5GprsBwBFnr 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 z

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 literal 0 HcmV?d00001 diff --git a/notebooks/equivalence_checker/equivalence_checker_example.ipynb b/notebooks/equivalence_checking/equivalence_checking_example.ipynb similarity index 76% rename from notebooks/equivalence_checker/equivalence_checker_example.ipynb rename to notebooks/equivalence_checking/equivalence_checking_example.ipynb index 3f8fd8d..bc05023 100644 --- a/notebooks/equivalence_checker/equivalence_checker_example.ipynb +++ b/notebooks/equivalence_checking/equivalence_checking_example.ipynb @@ -9,7 +9,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 1, "metadata": {}, "outputs": [], "source": [ @@ -25,27 +25,21 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 6, "metadata": {}, "outputs": [], "source": [ - "num_bits = 6\n", - "num_counter_examples = 5\n", - "\n", - "# Create synthetic data\n", - "miter, counter_examples = equivalence_checker.create_condition_string(\n", - " num_bits=num_bits, num_counter_examples=num_counter_examples\n", - ")\n", - "\n", "# Generate table with all possible parameter combinations\n", "# that shows the used Grover iterations if correct counter examples\n", "# are found in all runs.\n", "equivalence_checker.try_parameter_combinations(\n", - " path=\"res_equivalence_checker_test.csv\", # Path to save the results\n", + " path=\"res_equivalence_checker.csv\", # Path to save the results\n", " range_deltas=[0.1, 0.3, 0.5, 0.7, 0.9], # Range of \"delta\" values, a threshold parameter introduced in the paper\n", - " range_num_bits=[6, 7, 8, 9], # Range of number of bits of the circuits to be verified\n", + " range_num_bits=[6,7,8,9], # Range of number of bits of the circuits to be verified\n", " range_fraction_counter_examples=[0, 0.01, 0.05, 0.1, 0.2], # Range of fraction of counter examples to be used\n", + " shots_factor=8.0, # The number of shots for the quantum circuit is calculated as shots_factor * 2^num_bits\n", " num_runs=10, # Number of individual runs for each parameter combination\n", + " verbose=False, # If True, the progress is printed\n", ")" ] }, @@ -58,11 +52,22 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 5, "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "['000000', '000001', '000100', '000010', '000011']\n" + ] + } + ], "source": [ - "# Create synthetic data\n", + "num_bits = 6\n", + "num_counter_examples = 5\n", + "\n", + "# Create synthetic data for showing the example\n", "miter, _ = equivalence_checker.create_condition_string(num_bits=num_bits, num_counter_examples=num_counter_examples)\n", "\n", "# Run the equivalence checker\n", @@ -92,7 +97,8 @@ "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", - "pygments_lexer": "ipython3" + "pygments_lexer": "ipython3", + "version": "3.10.11" } }, "nbformat": 4, diff --git a/notebooks/equivalence_checker/res_equivalence_checker.csv b/notebooks/equivalence_checking/res_equivalence_checker.csv similarity index 100% rename from notebooks/equivalence_checker/res_equivalence_checker.csv rename to notebooks/equivalence_checking/res_equivalence_checker.csv diff --git a/res_satellite_solver.csv b/res_satellite_solver.csv deleted file mode 100644 index 9695412..0000000 --- a/res_satellite_solver.csv +++ /dev/null @@ -1,2 +0,0 @@ -num_qubits,calculation_time_qaoa,calculation_time_wqaoa,calculation_time_vqe,success_rate_qaoa,success_rate_wqaoa,success_rate_vqe -3,0.3417479991912842,0.2661118507385254,0.12465500831604004,1.0,1.0,1.0 diff --git a/src/mqt/problemsolver/equivalence_checker/equivalence_checker.py b/src/mqt/problemsolver/equivalence_checker/equivalence_checker.py index 9f328a4..e695c72 100644 --- a/src/mqt/problemsolver/equivalence_checker/equivalence_checker.py +++ b/src/mqt/problemsolver/equivalence_checker/equivalence_checker.py @@ -4,23 +4,25 @@ import string from operator import itemgetter +from typing import TYPE_CHECKING import numpy as np import pandas as pd from qiskit import QuantumCircuit from qiskit.circuit.library import GroverOperator, PhaseOracle from qiskit.compiler import transpile - -# from qiskit.utils import optionals as _optionals from qiskit_aer import AerSimulator -sim_counts = AerSimulator(method="statevector") - -alphabet = list(string.ascii_lowercase) +if TYPE_CHECKING: + from pathlib import Path def create_condition_string(num_bits: int, num_counter_examples: int) -> tuple[str, list[str]]: - """Creates a string to simulate a miter out of bitstring combinations (e.g. '0000' -> 'a & b & c & d'). + """Creates a synthetic miter string with multiple variables in form of letters. + + These represent the input bits of to be verified circuits and can be either 0 or 1. + The function also returns the corresponding counter examples to the miter string, that is, the bitstrings that satisfy the miter condition. + A miter can be of the form "a & b & c & d" where a, b, c, d are input bits. The counter examples are the bitstrings that satisfy the miter condition, e.g. "0000" for the miter "a & b & c & d". Parameters ---------- @@ -34,30 +36,33 @@ def create_condition_string(num_bits: int, num_counter_examples: int) -> tuple[s res_string : str Resulting condition string counter_examples : list[str] - The corresponding bitstrings to res_string (e.g. counter_examples is ['0000'] for res_string 'a & b & c & d') + The corresponding bitstrings to res_string (e.g. counter_examples is ['1111'] for res_string 'a & b & c & d') """ - try: - assert num_bits > 0 - assert num_counter_examples >= 0 - except AssertionError as e: - print("Number of bits and number of counter examples must be greater than 0.") + if num_bits < 0 or num_counter_examples < 0: msg = "The number of bits or counter examples cannot be used." - raise ValueError(msg) from e + raise ValueError(msg) + alphabet = list(string.ascii_lowercase) counter_examples: list[str] = [] - if num_counter_examples == 0: - res, _ = create_condition_string(num_bits, 1) - res += " & a" + if ( + num_counter_examples == 0 + ): # Since the qiskit PhaseOracle does not support empty conditions, we need to add a condition that is always false + res, _ = create_condition_string(num_bits, 1) # returns e.g. "~a & ~b & ~c & ~d" for num_bits = 4 + res += " & a" # turns the miter into "~a & ~b & ~c & ~d & a" which is always false return res, counter_examples res_string: str = "" counter_examples = [] for num in range(num_counter_examples): - bitstring = list(str(format(num, f"0{num_bits}b")))[::-1] - counter_examples.append(str(format(num, f"0{num_bits}b"))) - for i, char in enumerate(bitstring): - if char == "0" and i == 0: - bitstring[i] = "~" + alphabet[i] - elif char == "1" and i == 0: + bitstring = list(str(format(num, f"0{num_bits}b")))[ + ::-1 + ] # e.g. ['0', '0', '0', '0'] for num = 0 and num_bits = 4 + counter_examples.append(str(format(num, f"0{num_bits}b"))) # appends ['0000'] for num = 0 and num_bits = 4 + for i, char in enumerate(bitstring): # the following lines add a negated letter (e.g. "~a") for + if char == "0" and i == 0: # each "0" and a letter (e.g. "a") for each "1" in the bitstring list. + bitstring[i] = ( + "~" + alphabet[i] + ) # If the first letter is added (so i > 0), the subsequent letters are added together + elif char == "1" and i == 0: # with a logical AND operator (e.g. "~a & ~b & ~c & ~d"). bitstring[i] = alphabet[i] elif char == "0": bitstring[i] = " & " + "~" + alphabet[i] @@ -71,41 +76,48 @@ def create_condition_string(num_bits: int, num_counter_examples: int) -> tuple[s return res_string, counter_examples -def run_parameter_combinations( +miter, counter = create_condition_string(4, 2) + + +def find_counter_examples( miter: str, - counter_examples: list[str], num_bits: int, shots: int, delta: float, -) -> int | None: - """Runs Grover's algorithm to find counter examples for a given miter when knowing the counter examples to test parameters. + counter_examples: list[str] | None = None, +) -> list[str] | int | None: + """Runs our approach utilizing Grover's algorithm to find counter examples for a given miter. + + The function is also used in the "try_parameter_combinations" function to test different parameter combinations of our approach. + In this case, a synthetic miter is used for which the counter examples are known and one can check, if for given parameters (such as shots and delta) + our approach can find the correct counter examples. Parameters ---------- miter : str Miter condition string - counter_examples : list[str] - List of counter examples num_bits : int Number of input bits shots : int Number of shots to run the quantum circuit for delta : float Threshold for the stopping condition + counter_examples : list[str] | NoneType + List of counter examples Returns: ------- - None if no or wrong counter examples were found, else the number of iterations + Found counter examples for a given miter (counter_examples=None) or the number of iterations to find the counter examples (or "-" if no/wrong counter examples were found) if the counter examples are known beforehand. """ - try: - assert 0 <= delta <= 1 - except AssertionError as e: + if not 0 <= delta <= 1: msg = f"Invalid value for delta {delta}, which must be between 0 and 1." - raise ValueError(msg) from e + raise ValueError(msg) total_num_combinations = 2**num_bits start_iterations = np.floor(np.pi / (4 * np.arcsin((1 / total_num_combinations) ** 0.5)) - 0.5).astype(int) + simulator = AerSimulator(method="statevector") + total_iterations = 0 for iterations in reversed(range(1, start_iterations + 1)): total_iterations += iterations @@ -120,9 +132,9 @@ def run_parameter_combinations( qc.compose(operator.power(iterations).decompose(), inplace=True) qc.measure_all() - qc = transpile(qc, sim_counts) + qc = transpile(qc, simulator) - job = sim_counts.run(qc, shots=shots) + job = simulator.run(qc, shots=shots) result = job.result() counts_dict = dict(result.get_counts()) counts_list = list(counts_dict.values()) @@ -133,10 +145,8 @@ def run_parameter_combinations( ) # Sort state dictionary with respect to values (counts) found_counter_examples = [] - stopping_condition = False - for i in range(round(total_num_combinations * 0.5)): + for i in range(int(total_num_combinations * 0.5)): if (i + 1) == len(counts_list): - stopping_condition = True found_counter_examples_list = counts_list found_counter_examples_dict = { list(counts_dict.keys())[t]: list(counts_dict.values())[t] @@ -147,7 +157,6 @@ def run_parameter_combinations( diff = counts_list[i] - counts_list[i + 1] if diff > counts_list[i] * delta: - stopping_condition = True found_counter_examples_list = counts_list[: i + 1] found_counter_examples_dict = { list(counts_dict.keys())[t]: list(counts_dict.values())[t] @@ -156,25 +165,21 @@ def run_parameter_combinations( found_counter_examples = list(found_counter_examples_dict.keys()) break - if stopping_condition: - break + if counter_examples is None: + return found_counter_examples if sorted(found_counter_examples) == sorted(counter_examples): return total_iterations - if len(found_counter_examples) == 0: - if len(counter_examples) > 0: - return None - if len(counter_examples) == 0: - return total_iterations return None def try_parameter_combinations( - path: str, + path: Path, range_deltas: list[float], range_num_bits: list[int], range_fraction_counter_examples: list[float], + shots_factor: float, num_runs: int, verbose: bool = False, ) -> None: @@ -190,6 +195,8 @@ def try_parameter_combinations( List of numbers of input bits to try range_fraction_counter_examples : list[float] List of fractions of counter examples to try + shots_factor : float + Factor to scale the number of shots with the number of input bits (shots_factor * 2^num_bits) num_runs : int Number of runs for each parameter combination verbose : bool @@ -210,7 +217,13 @@ def try_parameter_combinations( results = [] for _run in range(num_runs): miter, counter_examples = create_condition_string(num_bits, num_counter_examples) - result = run_parameter_combinations(miter, counter_examples, num_bits, 8 * (2**num_bits), delta) + result = find_counter_examples( + miter=miter, + num_bits=num_bits, + shots=shots_factor * (2**num_bits), + delta=delta, + counter_examples=counter_examples, + ) results.append(result) if None in results: row.append("-") @@ -220,92 +233,3 @@ def try_parameter_combinations( i += 1 data.to_csv(path, index=False) - - -def find_counter_examples( - miter: str, - num_bits: int, - shots: int, - delta: float, -) -> list[str | None]: - """Runs Grover's algorithm to find counter examples for a given miter without knowing the counter examples. - - Parameters - ---------- - miter : str - Miter condition string - num_bits : int - Number of input bits - shots : int - Number of shots to run the quantum circuit for - delta : float - Threshold for the stopping condition - - Returns: - ------- - counter_examples: list[str] - List of states that are assumed to be counter examples - """ - try: - assert 0 <= delta <= 1 - except AssertionError as e: - msg = f"Invalid value for delta {delta}, which must be between 0 and 1." - raise ValueError(msg) from e - - total_num_combinations = 2**num_bits - start_iterations = np.floor(np.pi / (4 * np.arcsin((1 / total_num_combinations) ** 0.5)) - 0.5).astype(int) - - total_iterations = 0 - for iterations in reversed(range(1, start_iterations + 1)): - total_iterations += iterations - oracle = PhaseOracle(miter) - - operator = GroverOperator(oracle).decompose() - num_bits = operator.num_qubits - total_num_combinations = 2**num_bits - - qc = QuantumCircuit(num_bits) - qc.h(list(range(num_bits))) - qc.compose(operator.power(iterations).decompose(), inplace=True) - qc.measure_all() - - qc = transpile(qc, sim_counts) - - job = sim_counts.run(qc, shots=shots) - result = job.result() - counts_dict = dict(result.get_counts()) - counts_list = list(counts_dict.values()) - counts_list.sort(reverse=True) - - counts_dict = dict( - sorted(counts_dict.items(), key=itemgetter(1))[::-1] - ) # Sort state dictionary with respect to values (counts) - - counter_examples = [] - stopping_condition = False - for i in range(round(total_num_combinations * 0.5)): - if (i + 1) == len(counts_list): - stopping_condition = True - counter_examples_list = counts_list - counter_examples_dict = { - list(counts_dict.keys())[t]: list(counts_dict.values())[t] - for t in range(len(counter_examples_list)) - } - counter_examples = list(counter_examples_dict.keys()) - break - - diff = counts_list[i] - counts_list[i + 1] - if diff > counts_list[i] * delta: - stopping_condition = True - counter_examples_list = counts_list[: i + 1] - counter_examples_dict = { - list(counts_dict.keys())[t]: list(counts_dict.values())[t] - for t in range(len(counter_examples_list)) - } - counter_examples = list(counter_examples_dict.keys()) - break - - if stopping_condition: - break - - return counter_examples diff --git a/tests/test_equivalence_checker.py b/tests/test_equivalence_checker.py index f72fd30..cea737b 100644 --- a/tests/test_equivalence_checker.py +++ b/tests/test_equivalence_checker.py @@ -23,8 +23,6 @@ def test_create_condition_string() -> None: res_string, counter_examples = equivalence_checker.create_condition_string( num_bits=num_bits, num_counter_examples=num_counter_examples ) - assert isinstance(res_string, str) - assert isinstance(counter_examples, list) assert len(res_string) == 26 assert len(counter_examples) == num_counter_examples assert res_string == "~a & ~b & ~c | a & ~b & ~c" @@ -33,48 +31,28 @@ def test_create_condition_string() -> None: equivalence_checker.create_condition_string(num_bits=-5, num_counter_examples=-2) -@pytest.mark.skipif(not importlib.util.find_spec("tweedledum"), reason="tweedledum is not installed") -def test_run_paramter_combinations() -> None: - """Test the function run_parameter_combinations.""" - num_bits = 6 - num_counter_examples = 3 - res_string, counter_examples = equivalence_checker.create_condition_string( - num_bits=num_bits, num_counter_examples=num_counter_examples - ) - shots = 512 - delta = 0.7 - result = equivalence_checker.run_parameter_combinations( - miter=res_string, counter_examples=counter_examples, num_bits=num_bits, shots=shots, delta=delta - ) - assert result == 5 - with pytest.raises(ValueError, match="Invalid value for delta 1.2, which must be between 0 and 1."): - equivalence_checker.run_parameter_combinations( - miter=res_string, counter_examples=counter_examples, num_bits=num_bits, shots=shots, delta=1.2 - ) - - @pytest.mark.skipif(not importlib.util.find_spec("tweedledum"), reason="tweedledum is not installed") def test_try_parameter_combinations(tmp_path: Path) -> None: """Test the function try_parameter_combinations.""" - d = tmp_path / "sub" - d.mkdir() - d = d / "test1.csv" - d = d.absolute() - string_path = d.as_posix() + path_to_csv = tmp_path / "sub" / "test1.csv" + path_to_csv.parent.mkdir(parents=True, exist_ok=True) equivalence_checker.try_parameter_combinations( - path=string_path, + path=path_to_csv, range_deltas=[0.7, 0.8], range_num_bits=[5], range_fraction_counter_examples=[0.00, 0.05, 0.10, 0.20], + shots_factor=8, num_runs=5, ) assert len(list(tmp_path.iterdir())) == 1 + path_to_csv.unlink() + path_to_csv.parent.rmdir() @pytest.mark.skipif(not importlib.util.find_spec("tweedledum"), reason="tweedledum is not installed") def test_find_counter_examples() -> None: """Test the function find_counter_examples.""" - num_bits = 8 + num_bits = 6 num_counter_examples = 10 res_string, counter_examples = equivalence_checker.create_condition_string( num_bits=num_bits, num_counter_examples=num_counter_examples @@ -84,6 +62,7 @@ def test_find_counter_examples() -> None: found_counter_examples = equivalence_checker.find_counter_examples( miter=res_string, num_bits=num_bits, shots=shots, delta=delta ) - found_counter_examples.sort() + if isinstance(found_counter_examples, list): + found_counter_examples.sort() counter_examples.sort() assert found_counter_examples == counter_examples From 001563f467ff820ebd903610379d962a5068d5ae Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 23 Aug 2024 11:53:32 +0000 Subject: [PATCH 21/27] =?UTF-8?q?=F0=9F=8E=A8=20pre-commit=20fixes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../equivalence_checking_example.ipynb | 25 ++++++------------- 1 file changed, 8 insertions(+), 17 deletions(-) diff --git a/notebooks/equivalence_checking/equivalence_checking_example.ipynb b/notebooks/equivalence_checking/equivalence_checking_example.ipynb index bc05023..0316780 100644 --- a/notebooks/equivalence_checking/equivalence_checking_example.ipynb +++ b/notebooks/equivalence_checking/equivalence_checking_example.ipynb @@ -9,7 +9,7 @@ }, { "cell_type": "code", - "execution_count": 1, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -25,7 +25,7 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -35,11 +35,11 @@ "equivalence_checker.try_parameter_combinations(\n", " path=\"res_equivalence_checker.csv\", # Path to save the results\n", " range_deltas=[0.1, 0.3, 0.5, 0.7, 0.9], # Range of \"delta\" values, a threshold parameter introduced in the paper\n", - " range_num_bits=[6,7,8,9], # Range of number of bits of the circuits to be verified\n", + " range_num_bits=[6, 7, 8, 9], # Range of number of bits of the circuits to be verified\n", " range_fraction_counter_examples=[0, 0.01, 0.05, 0.1, 0.2], # Range of fraction of counter examples to be used\n", - " shots_factor=8.0, # The number of shots for the quantum circuit is calculated as shots_factor * 2^num_bits\n", + " shots_factor=8.0, # The number of shots for the quantum circuit is calculated as shots_factor * 2^num_bits\n", " num_runs=10, # Number of individual runs for each parameter combination\n", - " verbose=False, # If True, the progress is printed\n", + " verbose=False, # If True, the progress is printed\n", ")" ] }, @@ -52,17 +52,9 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "['000000', '000001', '000100', '000010', '000011']\n" - ] - } - ], + "outputs": [], "source": [ "num_bits = 6\n", "num_counter_examples = 5\n", @@ -97,8 +89,7 @@ "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.10.11" + "pygments_lexer": "ipython3" } }, "nbformat": 4, From 9640d3e5522093fd64ea38ec200c2c7a14e0a463 Mon Sep 17 00:00:00 2001 From: tobi-forster Date: Fri, 23 Aug 2024 15:31:56 +0200 Subject: [PATCH 22/27] Optimizations --- .github/workflows/coverage.yml | 2 +- README.md | 2 +- .../equivalence_checking_example.ipynb | 40 +++++++++---------- ...ecker.csv => res_equivalence_checking.csv} | 0 .../equivalence_checking.py} | 13 +++--- ...hecker.py => test_equivalence_checking.py} | 14 +++---- 6 files changed, 31 insertions(+), 40 deletions(-) rename notebooks/equivalence_checking/{res_equivalence_checker.csv => res_equivalence_checking.csv} (100%) rename src/mqt/problemsolver/{equivalence_checker/equivalence_checker.py => equivalence_checking/equivalence_checking.py} (95%) rename tests/{test_equivalence_checker.py => test_equivalence_checking.py} (79%) diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index 2b0eff2..909f691 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -17,7 +17,7 @@ jobs: with: python-version: "3.9" - name: Install MQT ProblemSolver - run: pip install .[coverage, tweedledum] + run: pip install .[coverage,tweedledum] - name: Generate Report run: pytest -v --cov --cov-config=pyproject.toml --cov-report=xml - name: Upload coverage to Codecov diff --git a/README.md b/README.md index 4773f3f..b70bc0f 100644 --- a/README.md +++ b/README.md @@ -126,7 +126,7 @@ Equivalence checking, i.e., verifying whether two circuits realize the same func

-In the `equivalence_checker.py` module, our approach to this problem by utilizing quantum computing is implemented. There are two different ways to run this code. +In the `equivalence_checking` module, our approach to this problem by utilizing quantum computing is implemented. There are two different ways to run this code. - One to test, how well certain parameter combinations work. The parameters consist of the number of bits of the circuits to be verified, the threshold parameter delta (which is explained in detail in the paper), the fraction of input combinations that induce non-equivalence of the circuits (further called "counter examples"), the number of shots to run the quantum circuit for and the number of individual runs of the experiment. Multiple parameter combinations can be tested and exported as a .csv-file at a provided location. - A second one to actually input a miter expression (in form of a string) together with some parameters independent from the miter (shots and delta) and use our approach to find the counter examples (if the circuits are non-equivalent). diff --git a/notebooks/equivalence_checking/equivalence_checking_example.ipynb b/notebooks/equivalence_checking/equivalence_checking_example.ipynb index bc05023..6003bff 100644 --- a/notebooks/equivalence_checking/equivalence_checking_example.ipynb +++ b/notebooks/equivalence_checking/equivalence_checking_example.ipynb @@ -7,13 +7,18 @@ "# Import" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [] + }, { "cell_type": "code", - "execution_count": 1, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ - "from mqt.problemsolver.equivalence_checker import equivalence_checker" + "from mqt.problemsolver.equivalence_checking import equivalence_checking" ] }, { @@ -25,21 +30,21 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# Generate table with all possible parameter combinations\n", "# that shows the used Grover iterations if correct counter examples\n", "# are found in all runs.\n", - "equivalence_checker.try_parameter_combinations(\n", - " path=\"res_equivalence_checker.csv\", # Path to save the results\n", + "equivalence_checking.try_parameter_combinations(\n", + " path=\"res_equivalence_checking.csv\", # Path to save the results\n", " range_deltas=[0.1, 0.3, 0.5, 0.7, 0.9], # Range of \"delta\" values, a threshold parameter introduced in the paper\n", - " range_num_bits=[6,7,8,9], # Range of number of bits of the circuits to be verified\n", + " range_num_bits=[6, 7, 8, 9], # Range of number of bits of the circuits to be verified\n", " range_fraction_counter_examples=[0, 0.01, 0.05, 0.1, 0.2], # Range of fraction of counter examples to be used\n", - " shots_factor=8.0, # The number of shots for the quantum circuit is calculated as shots_factor * 2^num_bits\n", + " shots_factor=8.0, # The number of shots for the quantum circuit is calculated as shots_factor * 2^num_bits\n", " num_runs=10, # Number of individual runs for each parameter combination\n", - " verbose=False, # If True, the progress is printed\n", + " verbose=False, # If True, the progress is printed\n", ")" ] }, @@ -52,26 +57,18 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "['000000', '000001', '000100', '000010', '000011']\n" - ] - } - ], + "outputs": [], "source": [ "num_bits = 6\n", "num_counter_examples = 5\n", "\n", "# Create synthetic data for showing the example\n", - "miter, _ = equivalence_checker.create_condition_string(num_bits=num_bits, num_counter_examples=num_counter_examples)\n", + "miter, _ = equivalence_checking.create_condition_string(num_bits=num_bits, num_counter_examples=num_counter_examples)\n", "\n", "# Run the equivalence checker\n", - "counter_examples = equivalence_checker.find_counter_examples(\n", + "counter_examples = equivalence_checking.find_counter_examples(\n", " miter=miter, # The condition string\n", " num_bits=num_bits, # Number of bits of the circuits to be verified\n", " shots=512, # Number of shots for the quantum circuit\n", @@ -97,8 +94,7 @@ "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.10.11" + "pygments_lexer": "ipython3" } }, "nbformat": 4, diff --git a/notebooks/equivalence_checking/res_equivalence_checker.csv b/notebooks/equivalence_checking/res_equivalence_checking.csv similarity index 100% rename from notebooks/equivalence_checking/res_equivalence_checker.csv rename to notebooks/equivalence_checking/res_equivalence_checking.csv diff --git a/src/mqt/problemsolver/equivalence_checker/equivalence_checker.py b/src/mqt/problemsolver/equivalence_checking/equivalence_checking.py similarity index 95% rename from src/mqt/problemsolver/equivalence_checker/equivalence_checker.py rename to src/mqt/problemsolver/equivalence_checking/equivalence_checking.py index e695c72..895a5a1 100644 --- a/src/mqt/problemsolver/equivalence_checker/equivalence_checker.py +++ b/src/mqt/problemsolver/equivalence_checking/equivalence_checking.py @@ -58,11 +58,11 @@ def create_condition_string(num_bits: int, num_counter_examples: int) -> tuple[s ] # e.g. ['0', '0', '0', '0'] for num = 0 and num_bits = 4 counter_examples.append(str(format(num, f"0{num_bits}b"))) # appends ['0000'] for num = 0 and num_bits = 4 for i, char in enumerate(bitstring): # the following lines add a negated letter (e.g. "~a") for - if char == "0" and i == 0: # each "0" and a letter (e.g. "a") for each "1" in the bitstring list. - bitstring[i] = ( - "~" + alphabet[i] - ) # If the first letter is added (so i > 0), the subsequent letters are added together - elif char == "1" and i == 0: # with a logical AND operator (e.g. "~a & ~b & ~c & ~d"). + if char == "0" and i == 0: # each "0" and a letter (e.g. "a") for each "1" in the bitstring list. + bitstring[i] = ( # If the first letter is added (so i > 0), the subsequent letters are added together + "~" + alphabet[i] # with a logical AND operator (e.g. "~a & ~b & ~c & ~d"). + ) + elif char == "1" and i == 0: bitstring[i] = alphabet[i] elif char == "0": bitstring[i] = " & " + "~" + alphabet[i] @@ -76,9 +76,6 @@ def create_condition_string(num_bits: int, num_counter_examples: int) -> tuple[s return res_string, counter_examples -miter, counter = create_condition_string(4, 2) - - def find_counter_examples( miter: str, num_bits: int, diff --git a/tests/test_equivalence_checker.py b/tests/test_equivalence_checking.py similarity index 79% rename from tests/test_equivalence_checker.py rename to tests/test_equivalence_checking.py index cea737b..2f0854f 100644 --- a/tests/test_equivalence_checker.py +++ b/tests/test_equivalence_checking.py @@ -8,19 +8,17 @@ import pytest -from mqt.problemsolver.equivalence_checker import equivalence_checker +from mqt.problemsolver.equivalence_checking import equivalence_checking if TYPE_CHECKING: from pathlib import Path -alphabet = list(string.ascii_lowercase) - def test_create_condition_string() -> None: """Test the function create_condition_string.""" num_bits = 3 num_counter_examples = 2 - res_string, counter_examples = equivalence_checker.create_condition_string( + res_string, counter_examples = equivalence_checking.create_condition_string( num_bits=num_bits, num_counter_examples=num_counter_examples ) assert len(res_string) == 26 @@ -28,7 +26,7 @@ def test_create_condition_string() -> None: assert res_string == "~a & ~b & ~c | a & ~b & ~c" with pytest.raises(ValueError, match="The number of bits or counter examples cannot be used."): - equivalence_checker.create_condition_string(num_bits=-5, num_counter_examples=-2) + equivalence_checking.create_condition_string(num_bits=-5, num_counter_examples=-2) @pytest.mark.skipif(not importlib.util.find_spec("tweedledum"), reason="tweedledum is not installed") @@ -36,7 +34,7 @@ def test_try_parameter_combinations(tmp_path: Path) -> None: """Test the function try_parameter_combinations.""" path_to_csv = tmp_path / "sub" / "test1.csv" path_to_csv.parent.mkdir(parents=True, exist_ok=True) - equivalence_checker.try_parameter_combinations( + equivalence_checking.try_parameter_combinations( path=path_to_csv, range_deltas=[0.7, 0.8], range_num_bits=[5], @@ -54,12 +52,12 @@ def test_find_counter_examples() -> None: """Test the function find_counter_examples.""" num_bits = 6 num_counter_examples = 10 - res_string, counter_examples = equivalence_checker.create_condition_string( + res_string, counter_examples = equivalence_checking.create_condition_string( num_bits=num_bits, num_counter_examples=num_counter_examples ) shots = 512 delta = 0.7 - found_counter_examples = equivalence_checker.find_counter_examples( + found_counter_examples = equivalence_checking.find_counter_examples( miter=res_string, num_bits=num_bits, shots=shots, delta=delta ) if isinstance(found_counter_examples, list): From 5b977f45bc39412b318f1e317a2e948d226011f4 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 23 Aug 2024 13:36:22 +0000 Subject: [PATCH 23/27] =?UTF-8?q?=F0=9F=8E=A8=20pre-commit=20fixes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../equivalence_checking/equivalence_checking.py | 10 +++++----- tests/test_equivalence_checking.py | 1 - 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/src/mqt/problemsolver/equivalence_checking/equivalence_checking.py b/src/mqt/problemsolver/equivalence_checking/equivalence_checking.py index 895a5a1..5a110b9 100644 --- a/src/mqt/problemsolver/equivalence_checking/equivalence_checking.py +++ b/src/mqt/problemsolver/equivalence_checking/equivalence_checking.py @@ -58,11 +58,11 @@ def create_condition_string(num_bits: int, num_counter_examples: int) -> tuple[s ] # e.g. ['0', '0', '0', '0'] for num = 0 and num_bits = 4 counter_examples.append(str(format(num, f"0{num_bits}b"))) # appends ['0000'] for num = 0 and num_bits = 4 for i, char in enumerate(bitstring): # the following lines add a negated letter (e.g. "~a") for - if char == "0" and i == 0: # each "0" and a letter (e.g. "a") for each "1" in the bitstring list. - bitstring[i] = ( # If the first letter is added (so i > 0), the subsequent letters are added together - "~" + alphabet[i] # with a logical AND operator (e.g. "~a & ~b & ~c & ~d"). - ) - elif char == "1" and i == 0: + if char == "0" and i == 0: # each "0" and a letter (e.g. "a") for each "1" in the bitstring list. + bitstring[i] = ( # If the first letter is added (so i > 0), the subsequent letters are added together + "~" + alphabet[i] # with a logical AND operator (e.g. "~a & ~b & ~c & ~d"). + ) + elif char == "1" and i == 0: bitstring[i] = alphabet[i] elif char == "0": bitstring[i] = " & " + "~" + alphabet[i] diff --git a/tests/test_equivalence_checking.py b/tests/test_equivalence_checking.py index 2f0854f..98a30d9 100644 --- a/tests/test_equivalence_checking.py +++ b/tests/test_equivalence_checking.py @@ -3,7 +3,6 @@ from __future__ import annotations import importlib -import string from typing import TYPE_CHECKING import pytest From 4de52e498c5e645b5412699bc54075ab08ff8a6b Mon Sep 17 00:00:00 2001 From: tobi-forster Date: Fri, 23 Aug 2024 16:26:45 +0200 Subject: [PATCH 24/27] Updated tests --- .../equivalence_checking/equivalence_checking.py | 12 ++++++------ tests/test_equivalence_checking.py | 4 +++- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/src/mqt/problemsolver/equivalence_checking/equivalence_checking.py b/src/mqt/problemsolver/equivalence_checking/equivalence_checking.py index 895a5a1..754c9f2 100644 --- a/src/mqt/problemsolver/equivalence_checking/equivalence_checking.py +++ b/src/mqt/problemsolver/equivalence_checking/equivalence_checking.py @@ -57,12 +57,12 @@ def create_condition_string(num_bits: int, num_counter_examples: int) -> tuple[s ::-1 ] # e.g. ['0', '0', '0', '0'] for num = 0 and num_bits = 4 counter_examples.append(str(format(num, f"0{num_bits}b"))) # appends ['0000'] for num = 0 and num_bits = 4 - for i, char in enumerate(bitstring): # the following lines add a negated letter (e.g. "~a") for - if char == "0" and i == 0: # each "0" and a letter (e.g. "a") for each "1" in the bitstring list. - bitstring[i] = ( # If the first letter is added (so i > 0), the subsequent letters are added together - "~" + alphabet[i] # with a logical AND operator (e.g. "~a & ~b & ~c & ~d"). - ) - elif char == "1" and i == 0: + # The following lines add a negated letter (e.g. "~a") for each "0" and a letter (e.g. "a") for each "1" in the bitstring list. + # If the first letter is added (so i > 0), the subsequent letters are added together with a logical AND operator (e.g. "~a & ~b & ~c & ~d"). + for i, char in enumerate(bitstring): + if char == "0" and i == 0: + bitstring[i] = "~" + alphabet[i] + elif char == "1" and i == 0: bitstring[i] = alphabet[i] elif char == "0": bitstring[i] = " & " + "~" + alphabet[i] diff --git a/tests/test_equivalence_checking.py b/tests/test_equivalence_checking.py index 2f0854f..77a0d22 100644 --- a/tests/test_equivalence_checking.py +++ b/tests/test_equivalence_checking.py @@ -3,7 +3,6 @@ from __future__ import annotations import importlib -import string from typing import TYPE_CHECKING import pytest @@ -64,3 +63,6 @@ def test_find_counter_examples() -> None: found_counter_examples.sort() counter_examples.sort() assert found_counter_examples == counter_examples + + with pytest.raises(ValueError, match="Invalid value for delta 1.2, which must be between 0 and 1."): + equivalence_checking.find_counter_examples(miter=res_string, num_bits=5, shots=shots, delta=1.2) From df60eb7642f5c3dc75e3595290ec4283aaeeed8d Mon Sep 17 00:00:00 2001 From: tobi-forster Date: Sat, 24 Aug 2024 17:03:04 +0200 Subject: [PATCH 25/27] minor fixes --- .../equivalence_checking/equivalence_checking.py | 12 ++++++------ tests/test_equivalence_checking.py | 5 +---- 2 files changed, 7 insertions(+), 10 deletions(-) diff --git a/src/mqt/problemsolver/equivalence_checking/equivalence_checking.py b/src/mqt/problemsolver/equivalence_checking/equivalence_checking.py index 754c9f2..f8e2486 100644 --- a/src/mqt/problemsolver/equivalence_checking/equivalence_checking.py +++ b/src/mqt/problemsolver/equivalence_checking/equivalence_checking.py @@ -57,11 +57,11 @@ def create_condition_string(num_bits: int, num_counter_examples: int) -> tuple[s ::-1 ] # e.g. ['0', '0', '0', '0'] for num = 0 and num_bits = 4 counter_examples.append(str(format(num, f"0{num_bits}b"))) # appends ['0000'] for num = 0 and num_bits = 4 - # The following lines add a negated letter (e.g. "~a") for each "0" and a letter (e.g. "a") for each "1" in the bitstring list. - # If the first letter is added (so i > 0), the subsequent letters are added together with a logical AND operator (e.g. "~a & ~b & ~c & ~d"). - for i, char in enumerate(bitstring): - if char == "0" and i == 0: - bitstring[i] = "~" + alphabet[i] + for i, char in enumerate(bitstring): # the following lines add a negated letter (e.g. "~a") for + if char == "0" and i == 0: # each "0" and a letter (e.g. "a") for each "1" in the bitstring list. + bitstring[i] = ( # If the first letter is added (so i > 0), the subsequent letters are added together + "~" + alphabet[i] # with a logical AND operator (e.g. "~a & ~b & ~c & ~d"). + ) elif char == "1" and i == 0: bitstring[i] = alphabet[i] elif char == "0": @@ -229,4 +229,4 @@ def try_parameter_combinations( data.loc[i] = row i += 1 - data.to_csv(path, index=False) + data.to_csv(path, index=False) \ No newline at end of file diff --git a/tests/test_equivalence_checking.py b/tests/test_equivalence_checking.py index 77a0d22..6c81453 100644 --- a/tests/test_equivalence_checking.py +++ b/tests/test_equivalence_checking.py @@ -62,7 +62,4 @@ def test_find_counter_examples() -> None: if isinstance(found_counter_examples, list): found_counter_examples.sort() counter_examples.sort() - assert found_counter_examples == counter_examples - - with pytest.raises(ValueError, match="Invalid value for delta 1.2, which must be between 0 and 1."): - equivalence_checking.find_counter_examples(miter=res_string, num_bits=5, shots=shots, delta=1.2) + assert found_counter_examples == counter_examples \ No newline at end of file From 19230befc726b191468471c17882b3f8084a4c0a Mon Sep 17 00:00:00 2001 From: tobi-forster Date: Sat, 24 Aug 2024 17:08:31 +0200 Subject: [PATCH 26/27] Updated tests and comments --- .../equivalence_checking/equivalence_checking.py | 10 ++++++---- tests/test_equivalence_checking.py | 5 ++++- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/src/mqt/problemsolver/equivalence_checking/equivalence_checking.py b/src/mqt/problemsolver/equivalence_checking/equivalence_checking.py index f8e2486..473702b 100644 --- a/src/mqt/problemsolver/equivalence_checking/equivalence_checking.py +++ b/src/mqt/problemsolver/equivalence_checking/equivalence_checking.py @@ -57,10 +57,12 @@ def create_condition_string(num_bits: int, num_counter_examples: int) -> tuple[s ::-1 ] # e.g. ['0', '0', '0', '0'] for num = 0 and num_bits = 4 counter_examples.append(str(format(num, f"0{num_bits}b"))) # appends ['0000'] for num = 0 and num_bits = 4 - for i, char in enumerate(bitstring): # the following lines add a negated letter (e.g. "~a") for - if char == "0" and i == 0: # each "0" and a letter (e.g. "a") for each "1" in the bitstring list. - bitstring[i] = ( # If the first letter is added (so i > 0), the subsequent letters are added together - "~" + alphabet[i] # with a logical AND operator (e.g. "~a & ~b & ~c & ~d"). + # The following lines add a negated letter (e.g. "~a") for each "0" and a letter (e.g. "a") for each "1" in the bitstring list. + # If the first letter is added (so i > 0), the subsequent letters are added together with a logical AND operator (e.g. "~a & ~b & ~c & ~d"). + for i, char in enumerate(bitstring): + if char == "0" and i == 0: + bitstring[i] = ( + "~" + alphabet[i] ) elif char == "1" and i == 0: bitstring[i] = alphabet[i] diff --git a/tests/test_equivalence_checking.py b/tests/test_equivalence_checking.py index 6c81453..6e0afe9 100644 --- a/tests/test_equivalence_checking.py +++ b/tests/test_equivalence_checking.py @@ -62,4 +62,7 @@ def test_find_counter_examples() -> None: if isinstance(found_counter_examples, list): found_counter_examples.sort() counter_examples.sort() - assert found_counter_examples == counter_examples \ No newline at end of file + assert found_counter_examples == counter_examples + + with pytest.raises(ValueError, match="Invalid value for delta 1.2, which must be between 0 and 1."): + equivalence_checking.find_counter_examples(miter=res_string, num_bits=5, shots=shots, delta=1.2) \ No newline at end of file From 07d35fd21d034ca0f83bd5b3b3f9a2c8a76a2ab7 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sat, 24 Aug 2024 15:08:40 +0000 Subject: [PATCH 27/27] =?UTF-8?q?=F0=9F=8E=A8=20pre-commit=20fixes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../equivalence_checking/equivalence_checking.py | 10 ++++------ tests/test_equivalence_checking.py | 2 +- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/src/mqt/problemsolver/equivalence_checking/equivalence_checking.py b/src/mqt/problemsolver/equivalence_checking/equivalence_checking.py index 473702b..754c9f2 100644 --- a/src/mqt/problemsolver/equivalence_checking/equivalence_checking.py +++ b/src/mqt/problemsolver/equivalence_checking/equivalence_checking.py @@ -59,11 +59,9 @@ def create_condition_string(num_bits: int, num_counter_examples: int) -> tuple[s counter_examples.append(str(format(num, f"0{num_bits}b"))) # appends ['0000'] for num = 0 and num_bits = 4 # The following lines add a negated letter (e.g. "~a") for each "0" and a letter (e.g. "a") for each "1" in the bitstring list. # If the first letter is added (so i > 0), the subsequent letters are added together with a logical AND operator (e.g. "~a & ~b & ~c & ~d"). - for i, char in enumerate(bitstring): - if char == "0" and i == 0: - bitstring[i] = ( - "~" + alphabet[i] - ) + for i, char in enumerate(bitstring): + if char == "0" and i == 0: + bitstring[i] = "~" + alphabet[i] elif char == "1" and i == 0: bitstring[i] = alphabet[i] elif char == "0": @@ -231,4 +229,4 @@ def try_parameter_combinations( data.loc[i] = row i += 1 - data.to_csv(path, index=False) \ No newline at end of file + data.to_csv(path, index=False) diff --git a/tests/test_equivalence_checking.py b/tests/test_equivalence_checking.py index 6e0afe9..77a0d22 100644 --- a/tests/test_equivalence_checking.py +++ b/tests/test_equivalence_checking.py @@ -65,4 +65,4 @@ def test_find_counter_examples() -> None: assert found_counter_examples == counter_examples with pytest.raises(ValueError, match="Invalid value for delta 1.2, which must be between 0 and 1."): - equivalence_checking.find_counter_examples(miter=res_string, num_bits=5, shots=shots, delta=1.2) \ No newline at end of file + equivalence_checking.find_counter_examples(miter=res_string, num_bits=5, shots=shots, delta=1.2)