From 42b896da3bc79ecd7ce3708940c0259df102afc6 Mon Sep 17 00:00:00 2001 From: Hamid El Maazouz Date: Wed, 19 Jun 2024 14:14:16 +0100 Subject: [PATCH 1/4] Bug fix - Compare method calls instead of methods themselves --- src/QAT/qat/purr/compiler/hardware_models.py | 2 +- src/tests/test_qasm.py | 26 ++++++++++++++------ 2 files changed, 19 insertions(+), 9 deletions(-) diff --git a/src/QAT/qat/purr/compiler/hardware_models.py b/src/QAT/qat/purr/compiler/hardware_models.py index 357edb501..030a7be7a 100644 --- a/src/QAT/qat/purr/compiler/hardware_models.py +++ b/src/QAT/qat/purr/compiler/hardware_models.py @@ -79,7 +79,7 @@ def get_cl2qu_index_mapping(instructions: List[Instruction], model: QuantumHardw qubit = next(( qubit for qubit in model.qubits - if qubit.get_acquire_channel().full_id == instruction.channel.full_id + if qubit.get_acquire_channel().full_id() == instruction.channel.full_id() ), None) if qubit is None: diff --git a/src/tests/test_qasm.py b/src/tests/test_qasm.py index 16d46eefc..a74183ca7 100644 --- a/src/tests/test_qasm.py +++ b/src/tests/test_qasm.py @@ -806,10 +806,14 @@ def test_basic_single_measures(self): # correctly assigned, aka that measuring c[0] then c[1] results in c = [c0, c1]. assert len(results["c"]) == 2 - @pytest.mark.parametrize("use_experimental,frontend_mod", - [(True, experimental_frontends), - (False, core_frontends), - ], ids=("Experimental", "Standard")) + @pytest.mark.parametrize( + "use_experimental,frontend_mod", + [ + (True, experimental_frontends), + (False, core_frontends), + ], + ids=("Experimental", "Standard"), + ) def test_frontend_peek(self, use_experimental, frontend_mod): with pytest.raises(ValueError): fetch_frontend("", use_experimental=use_experimental) @@ -961,7 +965,6 @@ def test_ecr_intrinsic(self): def test_ecr_already_exists(self): Qasm2Parser().parse(get_builder(self.echo), get_qasm2("ecr_exists.qasm")) - @pytest.mark.parametrize( "qasm_file", [ @@ -990,7 +993,9 @@ class TestQatOptimization: def _measure_merge_timings(self, file, qubit_count, keys, expected): builder = parse_and_apply_optimiziations(file, qubit_count=qubit_count) qat_file = InstructionEmitter().emit(builder.instructions, builder.model) - timeline = EchoEngine(builder.model).create_duration_timeline(qat_file.instructions) + timeline = EchoEngine(builder.model).create_duration_timeline( + qat_file.instructions + ) def get_start_end(key, instruction, channel_type): pulse_channel = builder.model.get_pulse_channel_from_device( @@ -1303,6 +1308,7 @@ def test_simple_circuit(self): counts = result.get_counts() assert counts["1"] > 900 + mapping_setup1 = ( """ OPENQASM 2.0; @@ -1345,6 +1351,10 @@ def test_cl2qu_index_mapping(qasm_string, expected_mapping): hw = get_default_echo_hardware(3) parser = Qasm2Parser() result = parser.parse(get_builder(hw), qasm_string) - instructions = result.instructions - mapping = get_cl2qu_index_mapping(instructions, hw) + mapping = get_cl2qu_index_mapping(result.instructions, hw) + assert mapping == expected_mapping + + blob = result.serialize() + result2 = InstructionBuilder.deserialize(blob) + mapping = get_cl2qu_index_mapping(result2.instructions, hw) assert mapping == expected_mapping From 6e671503d3ea3598a5511e6a9ceff87d926e691e Mon Sep 17 00:00:00 2001 From: Hamid El Maazouz Date: Wed, 19 Jun 2024 15:10:55 +0100 Subject: [PATCH 2/4] Do not mitigate results when ErrorMitigationConfig is None or empty --- src/QAT/qat/purr/compiler/runtime.py | 29 ++++++++-- src/tests/test_readout_mitigation.py | 84 ++++++++++++++++++++++------ 2 files changed, 90 insertions(+), 23 deletions(-) diff --git a/src/QAT/qat/purr/compiler/runtime.py b/src/QAT/qat/purr/compiler/runtime.py index a801c1fa7..7788dfd81 100644 --- a/src/QAT/qat/purr/compiler/runtime.py +++ b/src/QAT/qat/purr/compiler/runtime.py @@ -11,6 +11,7 @@ CompilerConfig, MetricsType, ResultsFormatting, + ErrorMitigationConfig, ) from qat.purr.compiler.error_mitigation.readout_mitigation import get_readout_mitigation from qat.purr.compiler.execution import ( @@ -18,7 +19,10 @@ QuantumExecutionEngine, _binary, ) -from qat.purr.compiler.hardware_models import QuantumHardwareModel, get_cl2qu_index_mapping +from qat.purr.compiler.hardware_models import ( + QuantumHardwareModel, + get_cl2qu_index_mapping, +) from qat.purr.compiler.instructions import Instruction, Repeat, is_generated_name from qat.purr.compiler.interrupt import Interrupt, NullInterrupt from qat.purr.compiler.metrics import CompilationMetrics, MetricsMixin @@ -146,7 +150,7 @@ def squash_binary(value): return results def _apply_error_mitigation(self, results, instructions, error_mitigation): - if error_mitigation is None: + if error_mitigation is None or error_mitigation == ErrorMitigationConfig.Empty: return results # TODO: add support for multiple registers @@ -181,7 +185,12 @@ def run_quantum_executable( exe.run(self) def _common_execute( - self, fexecute: callable, instructions, results_format=None, repeats=None, error_mitigation=None + self, + fexecute: callable, + instructions, + results_format=None, + repeats=None, + error_mitigation=None, ): """ Executes these instructions against the current engine and returns the results. @@ -219,19 +228,27 @@ def _execute_with_interrupt( """ Executes these instructions against the current engine and returns the results. """ + def fexecute(instrs): return self.engine._execute_with_interrupt(instrs, interrupt) - return self._common_execute(fexecute, instructions, results_format, repeats, error_mitigation) + return self._common_execute( + fexecute, instructions, results_format, repeats, error_mitigation + ) - def execute(self, instructions, results_format=None, repeats=None, error_mitigation=None): + def execute( + self, instructions, results_format=None, repeats=None, error_mitigation=None + ): """ Executes these instructions against the current engine and returns the results. """ + def fexecute(instrs): return self.engine.execute(instrs) - return self._common_execute(fexecute, instructions, results_format, repeats, error_mitigation) + return self._common_execute( + fexecute, instructions, results_format, repeats, error_mitigation + ) def _binary_count(results_list, repeats): diff --git a/src/tests/test_readout_mitigation.py b/src/tests/test_readout_mitigation.py index fa43981e7..d9ef5bccc 100644 --- a/src/tests/test_readout_mitigation.py +++ b/src/tests/test_readout_mitigation.py @@ -21,6 +21,7 @@ from qat.purr.compiler.execution import SweepIterator from qat.purr.compiler.hardware_models import ErrorMitigation, ReadoutMitigation from qat.qat import execute_qasm +from .utils import get_jagged_echo_hardware def apply_error_mitigation_setup( @@ -106,13 +107,18 @@ def apply_hardware_options(self, hardware, random_cal, config_options): matrix = identity(2**qubit_count) if "linear_readout_mitigation": linear = self.generate_random_linear(qubit_count, random_cal) - hardware.error_mitigation = self.build_error_mitigation(matrix=matrix, linear=linear) + hardware.error_mitigation = self.build_error_mitigation( + matrix=matrix, linear=linear + ) - @pytest.mark.parametrize("get_hardware", [get_default_echo_hardware, - get_default_qiskit_hardware]) + @pytest.mark.parametrize( + "get_hardware", [get_default_echo_hardware, get_default_qiskit_hardware] + ) @pytest.mark.parametrize("qubit_count", [i for i in range(2, 9)]) @pytest.mark.parametrize("random_cal", [True, False]) - def test_something_changes_qasm(self, get_hardware, qubit_count, random_cal, config_options): + def test_something_changes_qasm( + self, get_hardware, qubit_count, random_cal, config_options + ): hw = get_hardware(qubit_count) self.apply_hardware_options(hw, random_cal, config_options) compiler_config = self.build_config(config_options) @@ -126,17 +132,18 @@ def test_something_changes_qasm(self, get_hardware, qubit_count, random_cal, con else: original = result["b"] zero = "0" * qubit_count - one = "11"+zero[2:] + one = "11" + zero[2:] assert sum([original.get(zero), original.get(one, 0)]) == 1000.0 mitigated = result[config] for key, value in mitigated.items(): if key in original: - assert isclose(value, original[key]/1000.0) + assert isclose(value, original[key] / 1000.0) else: assert isclose(value, 0.0) - @pytest.mark.parametrize("get_hardware", [get_default_echo_hardware, - get_default_qiskit_hardware]) + @pytest.mark.parametrize( + "get_hardware", [get_default_echo_hardware, get_default_qiskit_hardware] + ) def test_multiple_creg_fail(self, get_hardware, config_options): qasm = """ OPENQASM 2.0; @@ -155,8 +162,9 @@ def test_multiple_creg_fail(self, get_hardware, config_options): with pytest.raises(ValueError): execute_qasm(qasm, hw, compiler_config=compiler_config) - @pytest.mark.parametrize("get_hardware", [get_default_echo_hardware, - get_default_qiskit_hardware]) + @pytest.mark.parametrize( + "get_hardware", [get_default_echo_hardware, get_default_qiskit_hardware] + ) def test_non_binary_count_format_fails(self, get_hardware, config_options): hw = get_hardware(2) self.apply_hardware_options(hw, False, config_options) @@ -165,8 +173,9 @@ def test_non_binary_count_format_fails(self, get_hardware, config_options): with pytest.raises(ValueError): execute_qasm(self.get_qasm(2), hw, compiler_config=compiler_config) - @pytest.mark.parametrize("get_hardware", [get_default_echo_hardware, - get_default_qiskit_hardware]) + @pytest.mark.parametrize( + "get_hardware", [get_default_echo_hardware, get_default_qiskit_hardware] + ) def test_lack_of_hardware_calibration_fails(self, get_hardware, config_options): hw = get_hardware(2) self.apply_hardware_options(hw, False, []) @@ -176,6 +185,43 @@ def test_lack_of_hardware_calibration_fails(self, get_hardware, config_options): execute_qasm(self.get_qasm(2), hw, compiler_config=compiler_config) +class TestErrorMitigationConfig: +@pytest.mark.parametrize( + "get_hardware", + [ + get_default_echo_hardware, + get_default_qiskit_hardware, + get_jagged_echo_hardware, + ], +) +@pytest.mark.parametrize( + "error_mitigation", + [None, ErrorMitigationConfig.Empty], +) +class TestNoReadoutMitigation: + def get_qasm(self, qubit_count): + return f""" + OPENQASM 2.0; + include "qelib1.inc"; + qreg q[{qubit_count}]; + creg b[{qubit_count}]; + h q[0]; + cx q[0], q[1]; + measure q -> b; + """ + + def test_execution(self, get_hardware, error_mitigation): + hw = get_hardware(2) + config = CompilerConfig( + results_format=QuantumResultsFormat().binary_count(), + error_mitigation=error_mitigation, + repeats=10000, + ) + results = execute_qasm(self.get_qasm(2), hw, compiler_config=config) + assert results is not None + assert len(results) == 1 + + class TestOnNoisySimulator: config = CompilerConfig( results_format=QuantumResultsFormat().binary_count(), @@ -189,15 +235,17 @@ def __init__(self, model=None, auto_plot=False, sim_qubit_dt=0.25e-10): self.fidelity_r0 = {qubit.index: 1.0 for qubit in self.model.qubits} self.fidelity_r1 = {qubit.index: 1.0 for qubit in self.model.qubits} - def _execute_on_hardware(self, sweep_iterator: SweepIterator, package: QatFile, interrupt=None): + def _execute_on_hardware( + self, sweep_iterator: SweepIterator, package: QatFile, interrupt=None + ): result = super()._execute_on_hardware(sweep_iterator, package) for key in result.keys(): q = int(key.split("_", 1)[1]) for array in result[key]: for j in range(len(array)): - if (array[j] > 0 and np.random.rand() > self.fidelity_r0[q]) or ( - array[j] < 0 and np.random.rand() > self.fidelity_r1[q] - ): + if ( + array[j] > 0 and np.random.rand() > self.fidelity_r0[q] + ) or (array[j] < 0 and np.random.rand() > self.fidelity_r1[q]): array[j] *= -1 return result @@ -230,7 +278,9 @@ def test_prepare_bitstring_and_measure( eng.fidelity_r0 = [q0_ro_fidelity_0, q1_ro_fidelity_0] eng.fidelity_r1 = [q0_ro_fidelity_1, q1_ro_fidelity_1] - mitigated_result = execute_qasm(qasm, eng, self.config)["linear_readout_mitigation"] + mitigated_result = execute_qasm(qasm, eng, self.config)[ + "linear_readout_mitigation" + ] for output_bits, probability in mitigated_result.items(): if output_bits == bitstring: assert abs(probability - 1) < 0.05 From 310bb4170e734dced5f2d236322693c5ca9d44e6 Mon Sep 17 00:00:00 2001 From: Hamid El Maazouz Date: Wed, 19 Jun 2024 16:55:39 +0100 Subject: [PATCH 3/4] Add jagged hardware model tests for linear error mitigation and exclude matrix error mitigation --- src/tests/test_readout_mitigation.py | 123 +++++++++++++++++++++------ 1 file changed, 97 insertions(+), 26 deletions(-) diff --git a/src/tests/test_readout_mitigation.py b/src/tests/test_readout_mitigation.py index d9ef5bccc..2cdd303f6 100644 --- a/src/tests/test_readout_mitigation.py +++ b/src/tests/test_readout_mitigation.py @@ -44,14 +44,6 @@ def lin_mit_dict_from_fidelity(ro_fidelity_0, ro_fidelity_1): return hw -@pytest.mark.parametrize( - "config_options", - [ - ["matrix_readout_mitigation"], - ["linear_readout_mitigation"], - ["matrix_readout_mitigation", "linear_readout_mitigation"], - ], -) class TestReadoutMitigation: def get_qasm(self, qubit_count): return f""" @@ -68,12 +60,12 @@ def build_error_mitigation(self, linear=None, matrix=None, m3=None): readout = ReadoutMitigation(linear=linear, matrix=matrix, m3=m3) return ErrorMitigation(readout_mitigation=readout) - def generate_random_linear(self, qubit_count, random_data=True): + def generate_random_linear(self, qubit_indices, random_data=True): output = {} - for qubit in range(qubit_count): + for index in qubit_indices: random_0 = random() if random_data else 1 random_1 = random() if random_data else 1 - output[str(qubit)] = { + output[str(index)] = { "0|0": random_0, "1|0": 1 - random_0, "1|1": random_1, @@ -99,21 +91,110 @@ def build_config(self, configs): def apply_hardware_options(self, hardware, random_cal, config_options): matrix = None linear = None - qubit_count = len(hardware.qubits) + qubit_indices = [qubit.index for qubit in hardware.qubits] + qubit_count = len(qubit_indices) if "matrix_readout_mitigation" in config_options: if random_cal: matrix = rand(2**qubit_count, 2**qubit_count) else: matrix = identity(2**qubit_count) if "linear_readout_mitigation": - linear = self.generate_random_linear(qubit_count, random_cal) + linear = self.generate_random_linear(qubit_indices, random_cal) hardware.error_mitigation = self.build_error_mitigation( matrix=matrix, linear=linear ) - @pytest.mark.parametrize( - "get_hardware", [get_default_echo_hardware, get_default_qiskit_hardware] - ) + +@pytest.mark.parametrize( + "get_hardware", + [ + get_default_echo_hardware, + get_default_qiskit_hardware, + get_jagged_echo_hardware, + ], +) +@pytest.mark.parametrize( + "config_options", + [ + ["linear_readout_mitigation"], + ], +) +class TestLinearReadoutMitigation(TestReadoutMitigation): + @pytest.mark.parametrize("qubit_count", [i for i in range(2, 9)]) + @pytest.mark.parametrize("random_cal", [True, False]) + def test_something_changes_qasm( + self, get_hardware, qubit_count, random_cal, config_options + ): + hw = get_hardware(qubit_count) + self.apply_hardware_options(hw, random_cal, config_options) + compiler_config = self.build_config(config_options) + result = execute_qasm( + self.get_qasm(qubit_count=qubit_count), hw, compiler_config=compiler_config + ) + for config in config_options: + if random_cal: + assert result["b"] != result[config] + assert all([i > 0 for i in result[config].values()]) + else: + original = result["b"] + zero = "0" * qubit_count + one = "11" + zero[2:] + assert sum([original.get(zero), original.get(one, 0)]) == 1000.0 + mitigated = result[config] + for key, value in mitigated.items(): + if key in original: + assert isclose(value, original[key] / 1000.0) + else: + assert isclose(value, 0.0) + + def test_multiple_creg_fail(self, get_hardware, config_options): + qasm = """ + OPENQASM 2.0; + include "qelib1.inc"; + qreg q[1]; + creg b[1]; + creg c[1]; + h q[0]; + cx q[0], q[1]; + measure q[0] -> b[0]; + measure q[1] -> c[0]; + """ + hw = get_hardware(2) + self.apply_hardware_options(hw, False, config_options) + compiler_config = self.build_config(config_options) + with pytest.raises(ValueError): + execute_qasm(qasm, hw, compiler_config=compiler_config) + + def test_non_binary_count_format_fails(self, get_hardware, config_options): + hw = get_hardware(2) + self.apply_hardware_options(hw, False, config_options) + compiler_config = self.build_config(config_options) + compiler_config.results_format = QuantumResultsFormat().raw() + with pytest.raises(ValueError): + execute_qasm(self.get_qasm(2), hw, compiler_config=compiler_config) + + def test_lack_of_hardware_calibration_fails(self, get_hardware, config_options): + hw = get_hardware(2) + self.apply_hardware_options(hw, False, []) + compiler_config = self.build_config(config_options) + compiler_config.results_format = QuantumResultsFormat().raw() + with pytest.raises(ValueError): + execute_qasm(self.get_qasm(2), hw, compiler_config=compiler_config) + + +# TODO - Add jagged hardware for Matrix error mitigation tests when they're fixed +@pytest.mark.parametrize( + "get_hardware", + [get_default_echo_hardware, get_default_qiskit_hardware], +) +@pytest.mark.parametrize( + "config_options", + [ + ["matrix_readout_mitigation"], + ["matrix_readout_mitigation", "linear_readout_mitigation"], + ], +) +class TestMatrixReadoutMitigation(TestReadoutMitigation): @pytest.mark.parametrize("qubit_count", [i for i in range(2, 9)]) @pytest.mark.parametrize("random_cal", [True, False]) def test_something_changes_qasm( @@ -141,9 +222,6 @@ def test_something_changes_qasm( else: assert isclose(value, 0.0) - @pytest.mark.parametrize( - "get_hardware", [get_default_echo_hardware, get_default_qiskit_hardware] - ) def test_multiple_creg_fail(self, get_hardware, config_options): qasm = """ OPENQASM 2.0; @@ -162,9 +240,6 @@ def test_multiple_creg_fail(self, get_hardware, config_options): with pytest.raises(ValueError): execute_qasm(qasm, hw, compiler_config=compiler_config) - @pytest.mark.parametrize( - "get_hardware", [get_default_echo_hardware, get_default_qiskit_hardware] - ) def test_non_binary_count_format_fails(self, get_hardware, config_options): hw = get_hardware(2) self.apply_hardware_options(hw, False, config_options) @@ -173,9 +248,6 @@ def test_non_binary_count_format_fails(self, get_hardware, config_options): with pytest.raises(ValueError): execute_qasm(self.get_qasm(2), hw, compiler_config=compiler_config) - @pytest.mark.parametrize( - "get_hardware", [get_default_echo_hardware, get_default_qiskit_hardware] - ) def test_lack_of_hardware_calibration_fails(self, get_hardware, config_options): hw = get_hardware(2) self.apply_hardware_options(hw, False, []) @@ -185,7 +257,6 @@ def test_lack_of_hardware_calibration_fails(self, get_hardware, config_options): execute_qasm(self.get_qasm(2), hw, compiler_config=compiler_config) -class TestErrorMitigationConfig: @pytest.mark.parametrize( "get_hardware", [ From 803980d7f8b0b2ded5263fcfe08309d8d8134cd3 Mon Sep 17 00:00:00 2001 From: Hamid El Maazouz Date: Wed, 19 Jun 2024 18:04:15 +0100 Subject: [PATCH 4/4] Increase version number to 1.1.1. --- partials/minimal/pyproject.toml | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/partials/minimal/pyproject.toml b/partials/minimal/pyproject.toml index b5f4a7ccb..23d061921 100644 --- a/partials/minimal/pyproject.toml +++ b/partials/minimal/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "qat-compiler-minimal" -version = "1.1.0" +version = "1.1.1" description = "A minimal version of the `qat-compiler` package." readme = "README.rst" documentation = "https://oqc-community.github.io/qat" diff --git a/pyproject.toml b/pyproject.toml index b8c9fe385..cebcf152a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -2,7 +2,7 @@ name = "qat-compiler" # This name has the -compiler suffix in order to use the poetry and twine tools to build and publish to PyPI # witout having to manually adjust the dist file names. -version = "1.1.0" +version = "1.1.1" description = "A low-level quantum compiler and runtime which facilitates executing quantum IRs." readme = "README.rst" documentation = "https://oqc-community.github.io/qat"