Is developing. https://github.com/Blueqat/Blueqat
In Making a Blueqat backend ~ Part 1, I explained how to make a simulator backend that can accept OpenQASM input. This time, I will explain how to make a backend in the general case where it is not.
At a minimum, you can create a backend by inheriting Backend
and implementing the run
method.
It also reimplements the copy
method if the copy of the backend object itself is not convenient for copy.deepcopy
.
The run
method is in the formrun (self, gates, n_qubits, * args, ** kwargs)
.
gates
is the list of quantum gate objects, n_qubits
is the number of qubits, * args
, ** kwargs
is when the user calls Circuit (). Run ()
The argument of run
is passed.
If you implement this, you can do a backend, but since it is thrown too much to the developer, we have prepared the following flow.
It sounds confusing, but if you follow the flow, it can be a little easier to implement.
Also, when reimplementing the run
method, you may use the one you have prepared.
By default, calling the run
method calls the following code.
def _run(self, gates, n_qubits, args, kwargs):
gates, ctx = self._preprocess_run(gates, n_qubits, args, kwargs)
self._run_gates(gates, n_qubits, ctx)
return self._postprocess_run(ctx)
def _run_gates(self, gates, n_qubits, ctx):
"""Iterate gates and call backend's action for each gates"""
for gate in gates:
action = self._get_action(gate)
if action is not None:
ctx = action(gate, ctx)
else:
ctx = self._run_gates(gate.fallback(n_qubits), n_qubits, ctx)
return ctx
What backend developers should implement
--_preprocess_run
method
--Action for each gate
--_postprocess_run
method
is.
Notice the variable labeled ctx
here.
This variable is a variable to hold the state from the beginning to the end of run
. (If you don't need it, you can use None
, but I think there are few cases where you don't need it at all.)
Since the backend itself is also an ordinary object, you can give it a state by setting self.foo = ...
etc., but it will cause bugs etc., so please use ctx
as much as possible.
As an example of what to save in ctx
--Number of qubits
--State vector in the middle of calculation
--The input option of the run
method and its parsing
――Other things that take time to make or have a state
And so on.
(Be careful about not knowing during execution unless you leave the number of qubits in ctx
)
Also, if you look at the code above, paying attention to ctx
--Create a ctx
object with _preprocess_run
--When calling the action for each gate, a ctx
object is passed. Action is also expected to be implemented to return a ctx
object
--_postprocess_run
receives ctx
and returns the result of run
It has become a flow.
Quantum circuits are made by arranging gates. A typical backend implementation method is to apply the gates in a row in sequence.
The operation of applying a gate is called action here. To implement action, just add a method,
def gate_{Gate name}(self, gate, ctx):
#Something implemented
return ctx
To do.
As a feature of Blueqat, there was a slice notation such as Circuit (). H [:]
, but there is also a method to separate the slice notation, and for idx in gate.target_iter (qubit number) If you do like)
, you can get the index of 1 qubit gate. Also, the index of a 2-qubit gate such as CNOT should be for c, t in gate.control_target_iter (number of qubits)
.
The number of qubits is required here in order to know how many qubits to apply when doing something like Circuit (). H [:]
.
Not all gates need to be implemented. For example, T gates and S gates can be made if there is a rotating Z gate (RZ), so on the Blueqat side, if it is not implemented, take care to use another gate instead. I have.
Also, if there is no alternative gate, it will be an error if it is used, but it will not be an error if it is not used, so you can create a backend that supports only some gates. (For example, if you implement only X gate, CX gate, and CCX gate, you can create a backend specialized for classical logic circuits. You can implement it with just bit operations, so you can create something that operates at high speed.)
Gates that need to be implemented may be sorted out in the future, but at present Measurements are X, Y, Z, H, CZ, CX, RX, RY, RZ, CCZ, U3. (Measurement defines action in the same way as gate)
A slightly unusual backend is QasmOutputBackend
.
This is not a simulator, but a backend for converting Blueqat's quantum circuits to OpenQ ASM.
ctx
holds a list of OpenQASM rows and the number of qubits.
Also, each action adds a row to the list.
Click here for the entire code.
class QasmOutputBackend(Backend):
"""Backend for OpenQASM output."""
def _preprocess_run(self, gates, n_qubits, args, kwargs):
def _parse_run_args(output_prologue=True, **_kwargs):
return { 'output_prologue': output_prologue }
args = _parse_run_args(*args, **kwargs)
if args['output_prologue']:
qasmlist = [
"OPENQASM 2.0;",
'include "qelib1.inc";',
f"qreg q[{n_qubits}];",
f"creg c[{n_qubits}];",
]
else:
qasmlist = []
return gates, (qasmlist, n_qubits)
def _postprocess_run(self, ctx):
return "\n".join(ctx[0])
def _one_qubit_gate_noargs(self, gate, ctx):
for idx in gate.target_iter(ctx[1]):
ctx[0].append(f"{gate.lowername} q[{idx}];")
return ctx
gate_x = _one_qubit_gate_noargs
gate_y = _one_qubit_gate_noargs
gate_z = _one_qubit_gate_noargs
gate_h = _one_qubit_gate_noargs
gate_t = _one_qubit_gate_noargs
gate_s = _one_qubit_gate_noargs
def _two_qubit_gate_noargs(self, gate, ctx):
for control, target in gate.control_target_iter(ctx[1]):
ctx[0].append(f"{gate.lowername} q[{control}],q[{target}];")
return ctx
gate_cz = _two_qubit_gate_noargs
gate_cx = _two_qubit_gate_noargs
gate_cy = _two_qubit_gate_noargs
gate_ch = _two_qubit_gate_noargs
gate_swap = _two_qubit_gate_noargs
def _one_qubit_gate_args_theta(self, gate, ctx):
for idx in gate.target_iter(ctx[1]):
ctx[0].append(f"{gate.lowername}({gate.theta}) q[{idx}];")
return ctx
gate_rx = _one_qubit_gate_args_theta
gate_ry = _one_qubit_gate_args_theta
gate_rz = _one_qubit_gate_args_theta
def gate_i(self, gate, ctx):
for idx in gate.target_iter(ctx[1]):
ctx[0].append(f"id q[{idx}];")
return ctx
def gate_u1(self, gate, ctx):
for idx in gate.target_iter(ctx[1]):
ctx[0].append(f"{gate.lowername}({gate.lambd}) q[{idx}];")
return ctx
def gate_u2(self, gate, ctx):
for idx in gate.target_iter(ctx[1]):
ctx[0].append(f"{gate.lowername}({gate.phi},{gate.lambd}) q[{idx}];")
return ctx
def gate_u3(self, gate, ctx):
for idx in gate.target_iter(ctx[1]):
ctx[0].append(f"{gate.lowername}({gate.theta},{gate.phi},{gate.lambd}) q[{idx}];")
return ctx
def gate_cu1(self, gate, ctx):
for c, t in gate.control_target_iter(ctx[1]):
ctx[0].append(f"{gate.lowername}({gate.lambd}) q[{c}],q[{t}];")
return ctx
def gate_cu2(self, gate, ctx):
for c, t in gate.control_target_iter(ctx[1]):
ctx[0].append(f"{gate.lowername}({gate.phi},{gate.lambd}) q[{c}],q[{t}];")
return ctx
def gate_cu3(self, gate, ctx):
for c, t in gate.control_target_iter(ctx[1]):
ctx[0].append(f"{gate.lowername}({gate.theta},{gate.phi},{gate.lambd}) q[{c}],q[{t}];")
return ctx
def _three_qubit_gate_noargs(self, gate, ctx):
c0, c1, t = gate.targets
ctx[0].append(f"{gate.lowername} q[{c0}],q[{c1}],q[{t}];")
return ctx
gate_ccx = _three_qubit_gate_noargs
gate_cswap = _three_qubit_gate_noargs
def gate_measure(self, gate, ctx):
for idx in gate.target_iter(ctx[1]):
ctx[0].append(f"measure q[{idx}] -> c[{idx}];")
return ctx
_preprocess_run
def _preprocess_run(self, gates, n_qubits, args, kwargs):
def _parse_run_args(output_prologue=True, **_kwargs):
return { 'output_prologue': output_prologue }
args = _parse_run_args(*args, **kwargs)
if args['output_prologue']:
qasmlist = [
"OPENQASM 2.0;",
'include "qelib1.inc";',
f"qreg q[{n_qubits}];",
f"creg c[{n_qubits}];",
]
else:
qasmlist = []
return gates, (qasmlist, n_qubits)
The first thing we are doing is analyzing options.
Since it may be executed with the ʻoutput_prologue option like
Circuit (). Run (backend ='qasm_output', output_prologue = False) `, the option is analyzed.
This option is True by default, but if False is specified, it will be added to the beginning of OpenQ ASM.
OPENQASM 2.0;
include "qelib1.inc";
qreg q[Bit number];
creg c[Bit number];
Is omitted.
Next, ctx is a list of OpenQASM rows and a tuple of qubits.
It returns gates and ctx, but gates just returns what was passed as an argument.
I decided to return gates with _preprocess_run
because I wanted to operate the gate column for optimization etc., but if there is no particular need to do it, the received argument is returned as it is.
def _one_qubit_gate_noargs(self, gate, ctx):
for idx in gate.target_iter(ctx[1]):
ctx[0].append(f"{gate.lowername} q[{idx}];")
return ctx
gate_x = _one_qubit_gate_noargs
gate_y = _one_qubit_gate_noargs
gate_z = _one_qubit_gate_noargs
#Omitted because there are many
def gate_cu3(self, gate, ctx):
for c, t in gate.control_target_iter(ctx[1]):
ctx[0].append(f"{gate.lowername}({gate.theta},{gate.phi},{gate.lambd}) q[{c}],q[{t}];")
return ctx
I will do it like this. Since it is troublesome to make x, y, z gates one by one, I wear them sideways.
If you can't wear it sideways, implement it carefully like gate_cu3
.
What we are doing is that ctx
was([list of rows], number of quantum bits)
, so add a new row to the list of rows withctx [0] .append (...)
I'm just doing it.
_postprocess_run
def _postprocess_run(self, ctx):
return "\n".join(ctx[0])
It simply returns a list of lines as a line break delimited string.
This result is the result of run
.
Last time, we looked at the backend implementation method for processing systems that can read OpenQASM, but this time, we looked at the more general-purpose backend implementation method.
At Blueqat, we aim to take care of as much as possible on the library side and allow developers and users to do what they want, and it is also possible to easily create a backend by riding on the mechanism provided by Blueqat. You can, and you can even implement the full run
method without getting on board.
Anyone can create the backend itself, and you can even use your own simulator with the Blueqat interface. Everyone, please try to implement the backend.
Recommended Posts