Skip to content

Commit 0c44fba

Browse files
XYSheTakishima
andauthored
Implement a UnitarySimulator to save the unitary of a quantum circuit (#409)
* Add matout() to check for matrices applied. Add check for self-control. * Move unitary logic to its own compiler engine class * Improve unitary simulator example * Added support for measurement. Added history for unitaries. Added tests * Clean up directory * Fix simulator error, add changelog * Undo unneeded space changes * Refactor example a little * Reformat some of the docstrings * Update CHANGELOG * Improve code to enable all pylint checks for the UnitarySimulator * Improve test coverage and simply parts of the code * Tweak UnitarySimulator some more - Make it so that multiple calls to flush() do not unnecessarily increase the history list of unitary matrices Co-authored-by: Damien Nguyen <[email protected]>
1 parent 10b4077 commit 0c44fba

File tree

5 files changed

+668
-0
lines changed

5 files changed

+668
-0
lines changed

CHANGELOG.md

+3
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
88
## [Unreleased]
99

1010
### Added
11+
12+
- UnitarySimulator backend for computing the unitary transformation corresponding to a quantum circuit.
13+
1114
### Changed
1215
### Deprecated
1316
### Fixed

examples/unitary_simulator.py

+82
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
# -*- coding: utf-8 -*-
2+
# Copyright 2021 ProjectQ-Framework (www.projectq.ch)
3+
#
4+
# Licensed under the Apache License, Version 2.0 (the "License");
5+
# you may not use this file except in compliance with the License.
6+
# You may obtain a copy of the License at
7+
#
8+
# http://www.apache.org/licenses/LICENSE-2.0
9+
#
10+
# Unless required by applicable law or agreed to in writing, software
11+
# distributed under the License is distributed on an "AS IS" BASIS,
12+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
# See the License for the specific language governing permissions and
14+
# limitations under the License.
15+
# pylint: skip-file
16+
17+
"""Example of using the UnitarySimulator."""
18+
19+
20+
import numpy as np
21+
22+
from projectq.backends import UnitarySimulator
23+
from projectq.cengines import MainEngine
24+
from projectq.meta import Control
25+
from projectq.ops import All, X, QFT, Measure, CtrlAll
26+
27+
28+
def run_circuit(eng, n_qubits, circuit_num, gate_after_measure=False):
29+
"""Run a quantum circuit demonstrating the capabilities of the UnitarySimulator."""
30+
qureg = eng.allocate_qureg(n_qubits)
31+
32+
if circuit_num == 1:
33+
All(X) | qureg
34+
elif circuit_num == 2:
35+
X | qureg[0]
36+
with Control(eng, qureg[:2]):
37+
All(X) | qureg[2:]
38+
elif circuit_num == 3:
39+
with Control(eng, qureg[:2], ctrl_state=CtrlAll.Zero):
40+
All(X) | qureg[2:]
41+
elif circuit_num == 4:
42+
QFT | qureg
43+
44+
eng.flush()
45+
All(Measure) | qureg
46+
47+
if gate_after_measure:
48+
QFT | qureg
49+
eng.flush()
50+
All(Measure) | qureg
51+
52+
53+
def main():
54+
"""Definition of the main function of this example."""
55+
# Create a MainEngine with a unitary simulator backend
56+
eng = MainEngine(backend=UnitarySimulator())
57+
58+
n_qubits = 3
59+
60+
# Run out quantum circuit
61+
# 1 - circuit applying X on all qubits
62+
# 2 - circuit applying an X gate followed by a controlled-X gate
63+
# 3 - circuit applying a off-controlled-X gate
64+
# 4 - circuit applying a QFT on all qubits (QFT will get decomposed)
65+
run_circuit(eng, n_qubits, 3, gate_after_measure=True)
66+
67+
# Output the unitary transformation of the circuit
68+
print('The unitary of the circuit is:')
69+
print(eng.backend.unitary)
70+
71+
# Output the final state of the qubits (assuming they all start in state |0>)
72+
print('The final state of the qubits is:')
73+
print(eng.backend.unitary @ np.array([1] + ([0] * (2 ** n_qubits - 1))))
74+
print('\n')
75+
76+
# Show the unitaries separated by measurement:
77+
for history in eng.backend.history:
78+
print('Previous unitary is: \n', history, '\n')
79+
80+
81+
if __name__ == '__main__':
82+
main()

projectq/backends/__init__.py

+1
Original file line numberDiff line numberDiff line change
@@ -36,3 +36,4 @@
3636
from ._aqt import AQTBackend
3737
from ._awsbraket import AWSBraketBackend
3838
from ._ionq import IonQBackend
39+
from ._unitary import UnitarySimulator

projectq/backends/_unitary.py

+290
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,290 @@
1+
# -*- coding: utf-8 -*-
2+
# Copyright 2021 ProjectQ-Framework (www.projectq.ch)
3+
#
4+
# Licensed under the Apache License, Version 2.0 (the "License");
5+
# you may not use this file except in compliance with the License.
6+
# You may obtain a copy of the License at
7+
#
8+
# http://www.apache.org/licenses/LICENSE-2.0
9+
#
10+
# Unless required by applicable law or agreed to in writing, software
11+
# distributed under the License is distributed on an "AS IS" BASIS,
12+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
# See the License for the specific language governing permissions and
14+
# limitations under the License.
15+
16+
"""Contain a backend that saves the unitary of a quantum circuit."""
17+
18+
from copy import deepcopy
19+
import itertools
20+
import math
21+
import warnings
22+
import random
23+
import numpy as np
24+
25+
from projectq.cengines import BasicEngine
26+
from projectq.types import WeakQubitRef
27+
from projectq.meta import has_negative_control, get_control_count, LogicalQubitIDTag
28+
from projectq.ops import (
29+
AllocateQubitGate,
30+
DeallocateQubitGate,
31+
MeasureGate,
32+
FlushGate,
33+
)
34+
35+
36+
def _qidmask(target_ids, control_ids, n_qubits):
37+
"""
38+
Calculate index masks.
39+
40+
Args:
41+
target_ids (list): list of target qubit indices
42+
control_ids (list): list of control qubit indices
43+
control_state (list): list of states for the control qubits (0 or 1)
44+
n_qubits (int): number of qubits
45+
"""
46+
mask_list = []
47+
perms = np.array([x[::-1] for x in itertools.product("01", repeat=n_qubits)]).astype(int)
48+
all_ids = np.array(range(n_qubits))
49+
irel_ids = np.delete(all_ids, control_ids + target_ids)
50+
51+
if len(control_ids) > 0:
52+
cmask = np.where(np.all(perms[:, control_ids] == [1] * len(control_ids), axis=1))
53+
else:
54+
cmask = np.array(range(perms.shape[0]))
55+
56+
if len(irel_ids) > 0:
57+
irel_perms = np.array([x[::-1] for x in itertools.product("01", repeat=len(irel_ids))]).astype(int)
58+
for i in range(2 ** len(irel_ids)):
59+
irel_mask = np.where(np.all(perms[:, irel_ids] == irel_perms[i], axis=1))
60+
common = np.intersect1d(irel_mask, cmask)
61+
if len(common) > 0:
62+
mask_list.append(common)
63+
else:
64+
irel_mask = np.array(range(perms.shape[0]))
65+
mask_list.append(np.intersect1d(irel_mask, cmask))
66+
return mask_list
67+
68+
69+
class UnitarySimulator(BasicEngine):
70+
"""
71+
Simulator engine aimed at calculating the unitary transformation that represents the current quantum circuit.
72+
73+
Attributes:
74+
unitary (np.ndarray): Current unitary representing the quantum circuit being processed so far.
75+
history (list<np.ndarray>): List of previous quantum circuit unitaries.
76+
77+
Note:
78+
The current implementation of this backend resets the unitary after the first gate that is neither a qubit
79+
deallocation nor a measurement occurs after one of those two aforementioned gates.
80+
81+
The old unitary call be accessed at anytime after such a situation occurs via the `history` property.
82+
83+
.. code-block:: python
84+
85+
eng = MainEngine(backend=UnitarySimulator(), engine_list=[])
86+
qureg = eng.allocate_qureg(3)
87+
All(X) | qureg
88+
89+
eng.flush()
90+
All(Measure) | qureg
91+
eng.deallocate_qubit(qureg[1])
92+
93+
X | qureg[0] # WARNING: appending gate after measurements or deallocations resets the unitary
94+
"""
95+
96+
def __init__(self):
97+
"""Initialize a UnitarySimulator object."""
98+
super().__init__()
99+
self._qubit_map = dict()
100+
self._unitary = [1]
101+
self._num_qubits = 0
102+
self._is_valid = True
103+
self._is_flushed = False
104+
self._state = [1]
105+
self._history = []
106+
107+
@property
108+
def unitary(self):
109+
"""
110+
Access the last unitary matrix directly.
111+
112+
Returns:
113+
A numpy array which is the unitary matrix of the circuit.
114+
"""
115+
return deepcopy(self._unitary)
116+
117+
@property
118+
def history(self):
119+
"""
120+
Access all previous unitary matrices.
121+
122+
The current unitary matrix is appended to this list once a gate is received after either a measurement or a
123+
qubit deallocation has occurred.
124+
125+
Returns:
126+
A list where the elements are all previous unitary matrices representing the circuit, separated by
127+
measurement/deallocate gates.
128+
"""
129+
return deepcopy(self._history)
130+
131+
def is_available(self, cmd):
132+
"""
133+
Test whether a Command is supported by a compiler engine.
134+
135+
Specialized implementation of is_available: The unitary simulator can deal with all arbitrarily-controlled gates
136+
which provide a gate-matrix (via gate.matrix).
137+
138+
Args:
139+
cmd (Command): Command for which to check availability (single- qubit gate, arbitrary controls)
140+
141+
Returns:
142+
True if it can be simulated and False otherwise.
143+
"""
144+
if has_negative_control(cmd):
145+
return False
146+
147+
if isinstance(cmd.gate, (AllocateQubitGate, DeallocateQubitGate, MeasureGate)):
148+
return True
149+
150+
try:
151+
gate_mat = cmd.gate.matrix
152+
if len(gate_mat) > 2 ** 6:
153+
warnings.warn("Potentially large matrix gate encountered! ({} qubits)".format(math.log2(len(gate_mat))))
154+
return True
155+
except AttributeError:
156+
return False
157+
158+
def receive(self, command_list):
159+
"""
160+
Receive a list of commands.
161+
162+
Receive a list of commands from the previous engine and handle them:
163+
* update the unitary of the quantum circuit
164+
* update the internal quantum state if a measurement or a qubit deallocation occurs
165+
166+
prior to sending them on to the next engine.
167+
168+
Args:
169+
command_list (list<Command>): List of commands to execute on the simulator.
170+
"""
171+
for cmd in command_list:
172+
self._handle(cmd)
173+
174+
if not self.is_last_engine:
175+
self.send(command_list)
176+
177+
def _flush(self):
178+
"""Flush the simulator state."""
179+
if not self._is_flushed:
180+
self._is_flushed = True
181+
self._state = self._unitary @ self._state
182+
183+
def _handle(self, cmd):
184+
"""
185+
Handle all commands.
186+
187+
Args:
188+
cmd (Command): Command to handle.
189+
190+
Raises:
191+
RuntimeError: If a measurement is performed before flush gate.
192+
"""
193+
if isinstance(cmd.gate, AllocateQubitGate):
194+
self._qubit_map[cmd.qubits[0][0].id] = self._num_qubits
195+
self._num_qubits += 1
196+
self._unitary = np.kron(np.identity(2), self._unitary)
197+
self._state.extend([0] * len(self._state))
198+
199+
elif isinstance(cmd.gate, DeallocateQubitGate):
200+
pos = self._qubit_map[cmd.qubits[0][0].id]
201+
self._qubit_map = {key: value - 1 if value > pos else value for key, value in self._qubit_map.items()}
202+
self._num_qubits -= 1
203+
self._is_valid = False
204+
205+
elif isinstance(cmd.gate, MeasureGate):
206+
self._is_valid = False
207+
208+
if not self._is_flushed:
209+
raise RuntimeError(
210+
'Please make sure all previous gates are flushed before measurement so the state gets updated'
211+
)
212+
213+
if get_control_count(cmd) != 0:
214+
raise ValueError('Cannot have control qubits with a measurement gate!')
215+
216+
all_qubits = [qb for qr in cmd.qubits for qb in qr]
217+
measurements = self.measure_qubits([qb.id for qb in all_qubits])
218+
219+
for qb, res in zip(all_qubits, measurements):
220+
# Check if a mapper assigned a different logical id
221+
for tag in cmd.tags:
222+
if isinstance(tag, LogicalQubitIDTag):
223+
qb = WeakQubitRef(qb.engine, tag.logical_qubit_id)
224+
break
225+
self.main_engine.set_measurement_result(qb, res)
226+
227+
elif isinstance(cmd.gate, FlushGate):
228+
self._flush()
229+
else:
230+
if not self._is_valid:
231+
self._flush()
232+
233+
warnings.warn(
234+
"Processing of other gates after a qubit deallocation or measurement will reset the unitary,"
235+
"previous unitary can be accessed in history"
236+
)
237+
self._history.append(self._unitary)
238+
self._unitary = np.identity(2 ** self._num_qubits, dtype=complex)
239+
self._state = np.array([1] + ([0] * (2 ** self._num_qubits - 1)), dtype=complex)
240+
self._is_valid = True
241+
242+
self._is_flushed = False
243+
mask_list = _qidmask(
244+
[self._qubit_map[qb.id] for qr in cmd.qubits for qb in qr],
245+
[self._qubit_map[qb.id] for qb in cmd.control_qubits],
246+
self._num_qubits,
247+
)
248+
for mask in mask_list:
249+
cache = np.identity(2 ** self._num_qubits, dtype=complex)
250+
cache[np.ix_(mask, mask)] = cmd.gate.matrix
251+
self._unitary = cache @ self._unitary
252+
253+
def measure_qubits(self, ids):
254+
"""
255+
Measure the qubits with IDs ids and return a list of measurement outcomes (True/False).
256+
257+
Args:
258+
ids (list<int>): List of qubit IDs to measure.
259+
260+
Returns:
261+
List of measurement results (containing either True or False).
262+
"""
263+
random_outcome = random.random()
264+
val = 0.0
265+
i_picked = 0
266+
while val < random_outcome and i_picked < len(self._state):
267+
val += np.abs(self._state[i_picked]) ** 2
268+
i_picked += 1
269+
270+
i_picked -= 1
271+
272+
pos = [self._qubit_map[ID] for ID in ids]
273+
res = [False] * len(pos)
274+
275+
mask = 0
276+
val = 0
277+
for i, _pos in enumerate(pos):
278+
res[i] = ((i_picked >> _pos) & 1) == 1
279+
mask |= 1 << _pos
280+
val |= (res[i] & 1) << _pos
281+
282+
nrm = 0.0
283+
for i, _state in enumerate(self._state):
284+
if (mask & i) != val:
285+
self._state[i] = 0.0
286+
else:
287+
nrm += np.abs(_state) ** 2
288+
289+
self._state *= 1.0 / np.sqrt(nrm)
290+
return res

0 commit comments

Comments
 (0)