From 646f4a17aed5592b0e503bc56a4dce29193e953f Mon Sep 17 00:00:00 2001 From: Julien Dumazert Date: Fri, 14 Apr 2023 11:39:57 +0200 Subject: [PATCH 1/6] Add support for delay instruction * The backend-dependent time unit dt is left untouched * All other time units (s, ms, us, ns, ps) are converted to us when generating QIR --- src/qiskit_qir/visitor.py | 28 ++++++++++++++++++++++++---- tests/test_circuits/basic_gates.py | 25 +++++++++++++++++++++++++ tests/test_qiskit_qir.py | 18 ++++++++++++++++++ 3 files changed, 67 insertions(+), 4 deletions(-) diff --git a/src/qiskit_qir/visitor.py b/src/qiskit_qir/visitor.py index c82c6ce..05ae588 100644 --- a/src/qiskit_qir/visitor.py +++ b/src/qiskit_qir/visitor.py @@ -52,6 +52,7 @@ "cz", "h", "reset", + "delay", "rx", "ry", "rz", @@ -75,6 +76,7 @@ "m", "measure", "reset", + "delay", "rx", "ry", "rz", @@ -88,9 +90,7 @@ "z", ] -_NOOP_INSTRUCTIONS = ["delay"] - -_SUPPORTED_INSTRUCTIONS = _QUANTUM_INSTRUCTIONS + _NOOP_INSTRUCTIONS +_SUPPORTED_INSTRUCTIONS = _QUANTUM_INSTRUCTIONS class QuantumCircuitElementVisitor(metaclass=ABCMeta): @@ -311,7 +311,13 @@ def __branch(): if self._emit_barrier_calls: qis.barrier(self._builder) elif "delay" == instruction.name: - pass + # us is chosen as the default time unit in QIR since it is well + # suited to current performance of qubit implementations. + # When using dt, the backend-dependent time unit, the duration + # value is left untouched. + multipliers = {"s": 1e6, "ms": 1e3, "us": 1, "ns": 1e-3, "ps": 1e-6, "dt": 1.0} + duration = instruction.duration * multipliers[instruction.unit] + self._call_delay_instruction(duration, *qubits) elif "swap" == instruction.name: qis.swap(self._builder, *qubits) elif "ccx" == instruction.name: @@ -375,3 +381,17 @@ def _map_profile_to_capabilities(self, profile: str): raise UnsupportedOperation( f"The supplied profile is not supported: {profile}." ) + + def _declare_delay_instruction(self) -> None: + mod = self._module + assert mod is not None + void = pyqir.Type.void(mod.context) + double = pyqir.Type.double(mod.context) + function_type = FunctionType(void, [double, pyqir.qubit_type(mod.context)]) + return Function(function_type, Linkage.EXTERNAL, "__quantum__qis__delay__body", mod) + + def _call_delay_instruction(self, duration: float, qubit: Constant) -> None: + assert self._module is not None + declaration = self._declare_delay_instruction() + double = pyqir.Type.double(self._module.context) + self._builder.call(declaration, [const(double, duration), qubit]) diff --git a/tests/test_circuits/basic_gates.py b/tests/test_circuits/basic_gates.py index 78600da..8d901a3 100644 --- a/tests/test_circuits/basic_gates.py +++ b/tests/test_circuits/basic_gates.py @@ -25,6 +25,8 @@ _measurements = {"measure": "mz"} +_delays = {"delay": "delay"} + _rotations = {"rx": "rx", "ry": "ry", "rz": "rz"} _two_qubit_gates = {"cx": "cnot", "cz": "cz", "swap": "swap"} @@ -43,6 +45,8 @@ def _map_gate_name(gate: str) -> str: return _adj_gates[gate] elif gate in _measurements: return _measurements[gate] + elif gate in _delays: + return _delays[gate] elif gate in _rotations: return _rotations[gate] elif gate in _two_qubit_gates: @@ -76,6 +80,27 @@ def test_fixture(): locals()[name] = _generate_one_qubit_fixture(gate) +def _generate_delay_gate_fixture(unit: str): + @pytest.fixture() + def test_fixture(): + circuit = QuantumCircuit(1) + if unit == 'dt': + circuit.delay(1, 0, unit=unit) + else: + circuit.delay(0.5, 0, unit=unit) + return _map_gate_name('delay'), unit, circuit + + return test_fixture + + +delay_tests = [] +# Generate time param operation fixtures +for unit in {'s', 'ms', 'us', 'ns', 'ps', 'dt'}: + name = _fixture_name('delay_' + unit) + delay_tests.append(name) + locals()[name] = _generate_delay_gate_fixture(unit) + + def _generate_rotation_fixture(gate: str): @pytest.fixture() def test_fixture(): diff --git a/tests/test_qiskit_qir.py b/tests/test_qiskit_qir.py index 62c66eb..3cc973e 100644 --- a/tests/test_qiskit_qir.py +++ b/tests/test_qiskit_qir.py @@ -16,6 +16,7 @@ from test_circuits.basic_gates import ( single_op_tests, adj_op_tests, + delay_tests, rotation_tests, double_op_tests, triple_op_tests, @@ -120,6 +121,23 @@ def test_rotation_gates(circuit_name, request): assert len(func) == 3 +@pytest.mark.parametrize("circuit_name", delay_tests) +def test_delay_gate(circuit_name, request): + qir_op, unit, circuit = request.getfixturevalue(circuit_name) + generated_qir = str(to_qir_module(circuit)[0]).splitlines() + test_utils.check_attributes(generated_qir, 1, 0) + func = test_utils.get_entry_point_body(generated_qir) + assert func[0] == test_utils.initialize_call_string() + if unit == 'dt': + assert func[1] == test_utils.rotation_call_string(qir_op, 1, 0) + else: + multipliers = {"s": 1e6, "ms": 1e3, "us": 1, "ns": 1e-3, "ps": 1e-6} + duration = 0.5 * multipliers[unit] + assert func[1] == test_utils.rotation_call_string(qir_op, duration, 0) + assert func[2] == test_utils.return_string() + assert len(func) == 3 + + @pytest.mark.parametrize("circuit_name", double_op_tests) def test_double_qubit_gates(circuit_name, request): qir_op, circuit = request.getfixturevalue(circuit_name) From c6dfd306dd1c46400daf6c1bf8a858f0d34ecff8 Mon Sep 17 00:00:00 2001 From: Julien Dumazert Date: Mon, 2 Oct 2023 18:23:43 +0200 Subject: [PATCH 2/6] Fix: two delay gates share a single declaration Before this commit, adding two delay gates in a circuit would create two delay declarations: * __quantum__qis__delay__body * __quantum__qis__delay__body.1 --- src/qiskit_qir/visitor.py | 8 ++++++-- tests/test_qiskit_qir.py | 10 ++++++++++ 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/src/qiskit_qir/visitor.py b/src/qiskit_qir/visitor.py index 05ae588..990157a 100644 --- a/src/qiskit_qir/visitor.py +++ b/src/qiskit_qir/visitor.py @@ -115,6 +115,7 @@ def __init__(self, profile: str = "AdaptiveExecution", **kwargs): self._measured_qubits = {} self._emit_barrier_calls = kwargs.get("emit_barrier_calls", False) self._record_output = kwargs.get("record_output", True) + self._delay_declaration = None def visit_qiskit_module(self, module: QiskitModule): _log.debug( @@ -392,6 +393,9 @@ def _declare_delay_instruction(self) -> None: def _call_delay_instruction(self, duration: float, qubit: Constant) -> None: assert self._module is not None - declaration = self._declare_delay_instruction() + # Ensure we are using the same delay instruction once we declared it, + # if we call it multiple times. + if self._delay_declaration is None: + self._delay_declaration = self._declare_delay_instruction() double = pyqir.Type.double(self._module.context) - self._builder.call(declaration, [const(double, duration), qubit]) + self._builder.call(self._delay_declaration, [const(double, duration), qubit]) diff --git a/tests/test_qiskit_qir.py b/tests/test_qiskit_qir.py index 3cc973e..6d1b91c 100644 --- a/tests/test_qiskit_qir.py +++ b/tests/test_qiskit_qir.py @@ -7,6 +7,7 @@ import pytest import logging +from qiskit import QuantumCircuit from qiskit_qir.elements import QiskitModule from qiskit_qir.visitor import BasicQisVisitor from qiskit_qir.translate import to_qir_module @@ -137,6 +138,15 @@ def test_delay_gate(circuit_name, request): assert func[2] == test_utils.return_string() assert len(func) == 3 +def test_two_delay_gates_single_declaration(): + circuit = QuantumCircuit(1) + circuit.delay(1, unit='dt') + circuit.delay(2, unit='dt') + generated_qir = str(to_qir_module(circuit)[0]).splitlines() + func = test_utils.get_entry_point_body(generated_qir) + assert func[0] == test_utils.initialize_call_string() + assert func[1] == test_utils.rotation_call_string('delay', 1, 0) + assert func[2] == test_utils.rotation_call_string('delay', 2, 0) @pytest.mark.parametrize("circuit_name", double_op_tests) def test_double_qubit_gates(circuit_name, request): From 68a3a90ab7359d063f9eb5b69899c19ea2248d8c Mon Sep 17 00:00:00 2001 From: Julien Dumazert Date: Thu, 25 May 2023 19:10:49 +0200 Subject: [PATCH 3/6] Modifications in setup.cfg for publication --- README.md | 3 +++ setup.cfg | 10 +++++----- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 4d1e02f..06bcf55 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,9 @@ Qiskit to QIR translator. +This is a temporary fork until the upstream qiskit-qir supports user-defined +instructions. + ## Example ```python diff --git a/setup.cfg b/setup.cfg index d91de58..657bac5 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,14 +1,14 @@ [metadata] -name = qiskit-qir +name = qiskit-qir-alice-bob-fork version = 0.3.2 -author = Microsoft -author_email = que-contacts@microsoft.com +author = Alice & Bob +author_email = contact@alice-bob.com description = Qiskit to QIR translator long_description = file: README.md long_description_content_type = text/markdown -url = https://github.com/microsoft/qiskit-qir +url = https://github.com/Alice-Bob-SW/qiskit-qir project_urls = - Bug Tracker = https://github.com/microsoft/qiskit-qir + Bug Tracker = https://github.com/Alice-Bob-SW/qiskit-qir classifiers = Development Status :: 3 - Alpha Intended Audience :: Developers From 5260e03ebcef0b9badc924c758b153a5a9f96c39 Mon Sep 17 00:00:00 2001 From: Julien Dumazert Date: Mon, 2 Oct 2023 18:37:39 +0200 Subject: [PATCH 4/6] Bump to 0.3.2b --- setup.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index 657bac5..35f20c3 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = qiskit-qir-alice-bob-fork -version = 0.3.2 +version = 0.3.2b author = Alice & Bob author_email = contact@alice-bob.com description = Qiskit to QIR translator From 647d15c466a2f8a4e0a81ada52fb57478453bcdb Mon Sep 17 00:00:00 2001 From: Julien Dumazert Date: Fri, 3 Nov 2023 16:20:27 +0100 Subject: [PATCH 5/6] Add support for mx, prepare_x, prepare_z --- src/qiskit_qir/visitor.py | 55 ++++++++++++++++++++++++-- tests/test_circuits/basic_gates.py | 32 ++++++++++++++- tests/test_circuits/custom_mx.py | 62 ++++++++++++++++++++++++++++++ tests/test_qiskit_qir.py | 15 ++++++++ tests/test_utils.py | 4 ++ 5 files changed, 163 insertions(+), 5 deletions(-) create mode 100644 tests/test_circuits/custom_mx.py diff --git a/src/qiskit_qir/visitor.py b/src/qiskit_qir/visitor.py index 990157a..7ce0646 100644 --- a/src/qiskit_qir/visitor.py +++ b/src/qiskit_qir/visitor.py @@ -47,6 +47,8 @@ "barrier", "delay", "measure", + "measure_x", + "initialize", "m", "cx", "cz", @@ -75,6 +77,8 @@ "id", "m", "measure", + "measure_x", + "initialize", "reset", "delay", "rx", @@ -115,7 +119,7 @@ def __init__(self, profile: str = "AdaptiveExecution", **kwargs): self._measured_qubits = {} self._emit_barrier_calls = kwargs.get("emit_barrier_calls", False) self._record_output = kwargs.get("record_output", True) - self._delay_declaration = None + self._declarations = {} def visit_qiskit_module(self, module: QiskitModule): _log.debug( @@ -297,6 +301,10 @@ def __branch(): for qubit, result in zip(qubits, results): self._measured_qubits[qubit_id(qubit)] = True qis.mz(self._builder, qubit, result) + elif "measure_x" == instruction.name: + for qubit, result in zip(qubits, results): + self._measured_qubits[qubit_id(qubit)] = True + self._call_mx_instruction(qubit, result) else: if not self._capabilities & Capability.QUBIT_USE_AFTER_MEASUREMENT: # If we have a supported instruction, apply the capability @@ -319,6 +327,9 @@ def __branch(): multipliers = {"s": 1e6, "ms": 1e3, "us": 1, "ns": 1e-3, "ps": 1e-6, "dt": 1.0} duration = instruction.duration * multipliers[instruction.unit] self._call_delay_instruction(duration, *qubits) + elif "initialize" == instruction.name: + state = str(instruction.params[0]) + self._call_prepare_basis_instruction(state, *qubits) elif "swap" == instruction.name: qis.swap(self._builder, *qubits) elif "ccx" == instruction.name: @@ -391,11 +402,47 @@ def _declare_delay_instruction(self) -> None: function_type = FunctionType(void, [double, pyqir.qubit_type(mod.context)]) return Function(function_type, Linkage.EXTERNAL, "__quantum__qis__delay__body", mod) + def _declare_prepare_basis_instruction(self, basis: str) -> None: + mod = self._module + assert mod is not None + void = pyqir.Type.void(mod.context) + boolean = pyqir.IntType(mod.context, width=1) + function_type = FunctionType(void, [boolean, pyqir.qubit_type(mod.context)]) + return Function(function_type, Linkage.EXTERNAL, f"__quantum__qis__prepare_{basis}__body", mod) + + def _declare_mx_instruction(self) -> None: + mod = self._module + assert mod is not None + void = pyqir.Type.void(mod.context) + function_type = FunctionType(void, [pyqir.qubit_type(mod.context), pyqir.result_type(mod.context)]) + return Function(function_type, Linkage.EXTERNAL, f"__quantum__qis__mx__body", mod) + def _call_delay_instruction(self, duration: float, qubit: Constant) -> None: assert self._module is not None # Ensure we are using the same delay instruction once we declared it, # if we call it multiple times. - if self._delay_declaration is None: - self._delay_declaration = self._declare_delay_instruction() + if 'delay' not in self._declarations: + self._declarations['delay'] = self._declare_delay_instruction() double = pyqir.Type.double(self._module.context) - self._builder.call(self._delay_declaration, [const(double, duration), qubit]) + self._builder.call(self._declarations['delay'], [const(double, duration), qubit]) + + def _call_mx_instruction(self, qubit: Constant, bit: Constant) -> None: + assert self._module is not None + if 'mx' not in self._declarations: + self._declarations['mx'] = self._declare_mx_instruction() + self._builder.call(self._declarations['mx'], [qubit, bit]) + + def _call_prepare_basis_instruction(self, state: str, qubit: Constant) -> None: + assert self._module is not None + known_states = { + '0': ('z', False), + '1': ('z', True), + '+': ('x', False), + '-': ('x', True), + } + basis, arg = known_states[state] + prep_name = f'p{basis}' + if prep_name not in self._declarations: + self._declarations[prep_name] = self._declare_prepare_basis_instruction(basis) + boolean = pyqir.IntType(self._module.context, width=1) + self._builder.call(self._declarations[prep_name], [const(boolean, arg), qubit]) diff --git a/tests/test_circuits/basic_gates.py b/tests/test_circuits/basic_gates.py index 8d901a3..45033eb 100644 --- a/tests/test_circuits/basic_gates.py +++ b/tests/test_circuits/basic_gates.py @@ -5,6 +5,7 @@ import pytest from qiskit import QuantumCircuit +from . import custom_mx # All of the following dictionaries map from the names of methods on Qiskit QuantumCircuit objects # to the name of the equivalent pyqir BasicQisBuilder method @@ -23,12 +24,17 @@ _adj_gates = {"sdg": "s", "tdg": "t"} -_measurements = {"measure": "mz"} +_measurements = {"measure": "mz", "measure_x": "mx"} _delays = {"delay": "delay"} _rotations = {"rx": "rx", "ry": "ry", "rz": "rz"} +_prepares = { + "0": "prepare_z", "1": "prepare_z", + "+": "prepare_x", "-": "prepare_x", +} + _two_qubit_gates = {"cx": "cnot", "cz": "cz", "swap": "swap"} _three_qubit_gates = {"ccx": "ccx"} @@ -55,10 +61,16 @@ def _map_gate_name(gate: str) -> str: return _three_qubit_gates[gate] elif gate in _zero_qubit_operations: return _zero_qubit_operations[gate] + elif gate in _prepares: + return _prepares[gate] else: raise ValueError(f"Unknown Qiskit gate {gate}") +def _map_prepare_name(state: str) -> str: + return _prepares[state] + + def _generate_one_qubit_fixture(gate: str): @pytest.fixture() def test_fixture(): @@ -101,6 +113,24 @@ def test_fixture(): locals()[name] = _generate_delay_gate_fixture(unit) +def _generate_prepare_fixture(state: str): + @pytest.fixture() + def test_fixture(): + circuit = QuantumCircuit(1) + circuit.initialize(state, 0) + return _map_prepare_name(state), state, circuit + + return test_fixture + + +prepare_tests = [] +# Generate time param operation fixtures +for state in {'0', '1', '+', '-'}: + name = _fixture_name('initialize_' + unit) + prepare_tests.append(name) + locals()[name] = _generate_prepare_fixture(state) + + def _generate_rotation_fixture(gate: str): @pytest.fixture() def test_fixture(): diff --git a/tests/test_circuits/custom_mx.py b/tests/test_circuits/custom_mx.py new file mode 100644 index 0000000..3174d49 --- /dev/null +++ b/tests/test_circuits/custom_mx.py @@ -0,0 +1,62 @@ +############################################################################## +# Copyright 2023 Alice & Bob +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +############################################################################## + +from typing import Optional + +from qiskit.circuit import Instruction, InstructionSet, QuantumCircuit, Reset +from qiskit.circuit.equivalence_library import SessionEquivalenceLibrary +from qiskit.circuit.quantumcircuit import ClbitSpecifier, QubitSpecifier + + +class MeasureX(Instruction): + """Quantum measurement in the X basis.""" + + def __init__(self, label: Optional[str] = None): + """Create a new X-measurement instruction.""" + super().__init__('measure_x', 1, 1, [], label=label) + self._definition = QuantumCircuit(1, 1, name='measure_x') + self._definition.h(0) + self._definition.measure(0, 0) + self._definition.h(0) + + +def _measure_x( + self: QuantumCircuit, qubit: QubitSpecifier, cbit: ClbitSpecifier +) -> InstructionSet: + return self.append(MeasureX(), [qubit], [cbit]) + + +# Patching the QuantumCircuit class to add a `measure_x` method. +QuantumCircuit.measure_x = _measure_x + +# Add MeasureX to the session equivalence library +_measure_x_inst = MeasureX() +SessionEquivalenceLibrary.add_equivalence( + _measure_x_inst, _measure_x_inst.definition +) + + +# Add an equivalent from Reset to Initialize('0'). This is useful in the case +# of the local provider. +# Although we may want to preserve the Reset instruction for a future behavior +# not yet implemented (e.g., resetting to the void state of the cavity in +# case of cat qubits, which is not how the logical |0> state is encoded), users +# are used to use Reset to actually say Initialize('0'). +# That's what this equivalence rule does. If it causes trouble in the future, +# it may be removed. +_c = QuantumCircuit(1, 0) +_c.initialize('0', 0) # pylint: disable=no-member +SessionEquivalenceLibrary.add_equivalence(Reset(), _c) diff --git a/tests/test_qiskit_qir.py b/tests/test_qiskit_qir.py index 6d1b91c..4ec8b8e 100644 --- a/tests/test_qiskit_qir.py +++ b/tests/test_qiskit_qir.py @@ -22,6 +22,7 @@ double_op_tests, triple_op_tests, measurement_tests, + prepare_tests ) import test_utils @@ -138,6 +139,20 @@ def test_delay_gate(circuit_name, request): assert func[2] == test_utils.return_string() assert len(func) == 3 + +@pytest.mark.parametrize("circuit_name", prepare_tests) +def test_prepares(circuit_name, request): + qir_op, state, circuit = request.getfixturevalue(circuit_name) + generated_qir = str(to_qir_module(circuit)[0]).splitlines() + test_utils.check_attributes(generated_qir, 1, 0) + func = test_utils.get_entry_point_body(generated_qir) + assert func[0] == test_utils.initialize_call_string() + args = {'0': False, '1': True, '+': False, '-': True} + assert func[1] == test_utils.prepare_call_string(qir_op, args[state], 0) + assert func[2] == test_utils.return_string() + assert len(func) == 3 + + def test_two_delay_gates_single_declaration(): circuit = QuantumCircuit(1) circuit.delay(1, unit='dt') diff --git a/tests/test_utils.py b/tests/test_utils.py index 907781a..6b56b9b 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -41,6 +41,10 @@ def rotation_call_string(name: str, theta: float, qb: int) -> str: return f"call void @__quantum__qis__{name}__body(double {theta:#e}, {_qubit_string(qb)})" +def prepare_call_string(name: str, arg: bool, qb: int) -> str: + return f"call void @__quantum__qis__{name}__body(i1 {str(arg).lower()}, {_qubit_string(qb)})" + + def measure_call_string(name: str, res: str, qb: int) -> str: return f"call void @__quantum__qis__{name}__body({_qubit_string(qb)}, {_result_string(res)})" From 4c436ee0a2fe077f53a7d13bbe71669e4fe5b8cd Mon Sep 17 00:00:00 2001 From: Julien Dumazert Date: Fri, 3 Nov 2023 16:38:57 +0100 Subject: [PATCH 6/6] Bump to 0.3.2c --- setup.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index 35f20c3..ef13008 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = qiskit-qir-alice-bob-fork -version = 0.3.2b +version = 0.3.2c author = Alice & Bob author_email = contact@alice-bob.com description = Qiskit to QIR translator