-
Notifications
You must be signed in to change notification settings - Fork 360
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Support Expr
that Terra 0.24 newly introduced
#1915
Conversation
* Implemented ecr for stabilizer simulator. * Implemented ecr for stabilizer simulator. * Adapted to coding style. * fixed testcode for ecr in stabilizer.
Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
d7bdb00
to
591c907
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I haven't had and likely won't have time to review in full depth - I think we're probably going to need to move to a model where that's more the responsibility within the Aer team, but I've just run through quickly and made a couple of notes on the high-level implementation.
In C++ land, I was vaguely thinking that a more efficient simulator/interpreter would want to convert the tree representation of Qiskit's Expr
into a linear executable bytecode rather than retaining the tree structure into C++ land (better cache locality of the data, no recursion needed during execution, potential for JIT optimisations while writing out the bytecode, etc). I'm not suggesting you change to that in this PR if you don't think it's necessary, I'm just mentioning it as the path I'd thought made a little more sense for a simulator.
qiskit_aer/backends/aer_compiler.py
Outdated
def _convert_jump_conditional(self, cond_tuple, bit_map): | ||
"""Convert a condition tuple according to the wire map.""" | ||
if isinstance(cond_tuple[0], Clbit): | ||
if isinstance(cond_tuple, Expr): | ||
return cond_tuple | ||
elif isinstance(cond_tuple[0], Clbit): | ||
return (bit_map[cond_tuple[0]], cond_tuple[1]) | ||
# ClassicalRegister conditions should already be in the outer circuit. | ||
return cond_tuple | ||
elif isinstance(cond_tuple[0], ClassicalRegister): | ||
# ClassicalRegister conditions should already be in the outer circuit. | ||
return cond_tuple | ||
|
||
# Classical Expression | ||
def _convert_clbit_index(expr): | ||
if isinstance(expr, Unary): | ||
_convert_clbit_index(expr.operand) | ||
elif isinstance(expr, Binary): | ||
_convert_clbit_index(expr.left) | ||
_convert_clbit_index(expr.right) | ||
elif isinstance(expr, Var): | ||
if isinstance(expr.var, Clbit): | ||
expr.var = bit_map[expr.var] | ||
else: | ||
expr.var = [bit_map[clbit] for clbit in expr.var] | ||
return expr | ||
|
||
return (_convert_clbit_index(deepcopy(cond_tuple[0])), cond_tuple[1]) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not clear what this function is doing - it appears to me that the if/elif/elif
covers all valid .condition
fields already, so I don't know what the _convert_clbit_index
function is for.
For writing that type of function, you might want to look at the tools in Qiskit that are designed to work more efficiently / easily with the Expr
tree, rather than sequences of if/elif
statements. The most general form in ExprVisitor
, but there are a few pre-defined helpers that do more of the work as well. The full docs are here: https://qiskit.org/documentation/apidoc/circuit_classical.html#working-with-the-expression-tree.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I created _convert_clbit_index()
to convert cbit indices with bit_map
in the given Expr
. Do you mean that this conversion is not necessary?
Definitely I should use ExprVisitor
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's more that I can't immediately see how control could pass to the definition of that function - I think the if/elif/elif
above it is exhaustive.
qiskit_aer/backends/aer_compiler.py
Outdated
def _assemble_expr(circ, expr): | ||
aer_expr = None | ||
if expr is None: | ||
aer_expr = None | ||
elif isinstance(expr, Value): | ||
if isinstance(expr.type, Uint): | ||
aer_expr = AerUintValue(expr.type.width, expr.value) | ||
elif isinstance(expr.type, Bool): | ||
aer_expr = AerBoolValue(expr.value) | ||
else: | ||
raise AerError(f"invalid value type is specified: {expr.type.__class__}") | ||
elif isinstance(expr, Var): | ||
aer_expr = AerVar(_assemble_type(expr.type), _assemble_clbit_indices(circ, expr.var)) | ||
elif isinstance(expr, Cast): | ||
aer_expr = AerCast(_assemble_type(expr.type), _assemble_expr(circ, expr.operand)) | ||
elif isinstance(expr, Unary): | ||
aer_expr = AerUnaryExpr( | ||
_assemble_unary_operator(expr.op), _assemble_expr(circ, expr.operand) | ||
) | ||
elif isinstance(expr, Binary): | ||
aer_expr = AerBinaryExpr( | ||
_assemble_binary_operator(expr.op), | ||
_assemble_expr(circ, expr.left), | ||
_assemble_expr(circ, expr.right), | ||
) | ||
else: | ||
raise AerError(f"unknown expression: {expr.__class__}") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For example, this is another case where using the ExprVisitor
pattern might be a bit more reliable. It looks something like:
class _AssembleExprImpl(ExprVisitor):
def __init__(self, circuit):
self.circuit = circuit
def visit_value(self, node):
return AerUintValue(node.type.width, node.value) if isinstance(expr.type, Uint) else AerBoolValue(node.value)
def visit_var(self, node):
return AerVar(_assemble_type(node.type), _assemble_clbit_indices(self.circuit, node.var))
def visit_cast(self, node):
return AerCast(_assemble_type(node.type), self.visit(node.operand))
# ... and so on
def _assemble_expr(circ, expr):
# You could also just create one `_AssembleExprImpl` per `circuit` in the root assmebler
return _AssembleExprImpl(circ).visit(expr)
It's not a huge difference, but it does effectively turn the if/elif/elif
chain with many isinstance
calls into simple double-dispatch which is exactly two vtable lookups.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes. ExprVisitor
looks much better than the current my impl.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@jakelishman I updated this PR to use ExprVisitor
. I think this class does not have visit()
so I created it. Maybe we can use accept()
instead of creating this new method.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah ExprVisitor
doesn't have a visit
method because you call it as expr.accept(visitor)
; the Expr
subclasses handle choosing which method to call, rather than the ExprVisitor
doing magic inference.
qiskit_aer/backends/aer_compiler.py
Outdated
target_clbits = [] | ||
if isinstance(instruction.operation.target, Clbit): | ||
target_clbits = [bit_map[instruction.operation.target]] | ||
elif isinstance(instruction.operation.target, Expr): | ||
self._list_clbit_from_expr(bit_map, target_clbits, instruction.operation.target) | ||
else: | ||
target_clbits = [bit_map[c] for c in instruction.operation.target] | ||
mark_cargs = set(cargs + target_clbits) - set(instruction.clbits) | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just as a warning: the next version of Qiskit will (very likely) introduce Var
fields that are not backed by Clbit
instances. This PR is fine for now, but I'll send you the design doc just as soon as I've finished writing it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Here is the promised design document: Qiskit/RFCs#50
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I believe that backward compatibility needs to be maintained regarding Var
instantiation that takes Clbit
or ClassicalRegister
. So, in the new design, there are three types of Var
:
- based on
Clbit
- based on
ClassicalRegister
- based on specific type
In simulation, Var
needs to be assigned a specific memory area. I will use the existing memory management in Aer and implicitly create ClassicalRegister
for the third type of Var
s before simulation.
@@ -287,7 +610,8 @@ struct Op { | |||
bool conditional = false; // is gate conditional gate | |||
uint_t conditional_reg; // (opt) the (single) register location to look up for | |||
// conditional | |||
RegComparison bfunc; // (opt) boolean function relation | |||
BinaryOp binary_op; // (opt) boolean function relation |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Op.bfunc
is replaced with Op.binary_op
but OpTyp::bfunc
and ClassicalRegister::apply_bfunc
are still using name bfunc
. Is it OK?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks. It should be binary_op
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmmm, we still use Operations::OpType::bfunc
and keeping apply_bfunc
is reasonable.
a99d63b
to
245b4fa
Compare
6c07c1b
to
7362754
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've looked over the Python-space components again, and they look generally fine to me. I can't do the C++ components, sorry - I don't have the time to be getting that deep into Aer any more, but given that the tests pass, I'm guessing that they work ok.
I left a couple of small suggestions for tidying things up using some additional Terra helper methods that already exist / potentially re-using code, but it's not critical; it could be fixed later if necessary.
qiskit_aer/backends/aer_compiler.py
Outdated
class _ClbitConverter(ExprVisitor): | ||
"""Apply cbit index conversion in Expr tree""" | ||
|
||
def __init__(self, bit_map): | ||
self.bit_map = bit_map | ||
|
||
def visit_value(self, node, /): | ||
# pylint: disable=unused-variable | ||
return node | ||
|
||
def visit_var(self, node, /): | ||
if isinstance(node.var, Clbit): | ||
node.var = self.bit_map[node.var] | ||
else: | ||
node.var = [self.bit_map[clbit] for clbit in node.var] | ||
return node | ||
|
||
def visit_cast(self, node, /): | ||
node.operand.accept(self) | ||
return node | ||
|
||
def visit_unary(self, node, /): | ||
node.operand.accept(self) | ||
return node | ||
|
||
def visit_binary(self, node, /): | ||
node.left.accept(self) | ||
node.right.accept(self) | ||
return node | ||
|
||
def visit_generic(self, node, /): | ||
raise AerError(f"unsupported expression is used: {node.__class__}") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It feels like this visitor should construct the corresponding AerExpr
tree, rather than deepcopying the Terra one and mutating it? In particular, mutating Var.var
is super unsafe, especially because it's being changed to a value of an incorrect type here. (I also want to be a bit sneaky in Terra 1.0 and make expr.Var
and expr.Value
immutable; they logically always should have been.)
Can you re-use _AssembleExprImpl
here? That does the clbit remapping already, I think?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This class is no longer used in the latest code. Now, Only Var
instance is copied.
qiskit_aer/backends/aer_compiler.py
Outdated
if isinstance(cond_tuple, Expr): | ||
return cond_tuple | ||
elif isinstance(cond_tuple[0], Clbit): | ||
return (bit_map[cond_tuple[0]], cond_tuple[1]) | ||
# ClassicalRegister conditions should already be in the outer circuit. | ||
return cond_tuple | ||
elif isinstance(cond_tuple[0], ClassicalRegister): | ||
# ClassicalRegister conditions should already be in the outer circuit. | ||
return cond_tuple | ||
|
||
# left and right expression | ||
def _convert_clbit_index(expr): | ||
if isinstance(expr, Unary): | ||
_convert_clbit_index(expr.operand) | ||
elif isinstance(expr, Binary): | ||
_convert_clbit_index(expr.left) | ||
_convert_clbit_index(expr.right) | ||
elif isinstance(expr, Var): | ||
if isinstance(expr.var, Clbit): | ||
expr.var = bit_map[expr.var] | ||
else: | ||
expr.var = [bit_map[clbit] for clbit in expr.var] | ||
return expr | ||
|
||
left = deepcopy(cond_tuple[0]).accept(AerCompiler._ClbitConverter(bit_map)) | ||
right = cond_tuple[1] | ||
return (left, right) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have a previous review comment about this up top, but in what situations do we reach line 260? As far as I know, a condition can only be an Expr
or a tuple[Clbit | ClassicalRegister, int]
, so the if statements on lines 253 to 239 should be exhaustive, right? In what situations might we get through them?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks. Var
is possible to come here. I rewrite this part.
qiskit_aer/backends/aer_compiler.py
Outdated
def _list_clbit_from_expr(self, bit_map, clbits, expr): | ||
if isinstance(expr, Unary): | ||
self._list_clbit_from_expr(bit_map, clbits, expr.operand) | ||
elif isinstance(expr, Binary): | ||
self._list_clbit_from_expr(bit_map, clbits, expr.left) | ||
self._list_clbit_from_expr(bit_map, clbits, expr.right) | ||
elif isinstance(expr, Var): | ||
if isinstance(expr.var, Clbit): | ||
clbits += bit_map[expr.var] | ||
else: | ||
clbits += [bit_map[clbit] for clbit in expr.var] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There's a helper in Terra for iterating only over the Var
instances: qiskit.circuit.classical.expr.iter_vars
. You could implement this like
from qiskit.circuit.classical import expr
def _clbits_from_expr(self, bit_map, node):
out = set()
for var in expr.iter_vars(node):
if isinstance(var.var, Clbit):
out.add(bit_map[var.var])
elif isinstance(var.var, ClassicalRegister):
out.update(bit_map[bit] for bit in var.var)
else:
# not handled
return out
and you don't need to manually handle all the possible entries in the tree (for example, your current implementation forgets Cast
), and Terra will automatically handle any new Expr
subclasses that get added for you.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks. I changed the code as you suggested.
qiskit_aer/backends/aer_compiler.py
Outdated
aer_circ.multiplexer(qubits, params, conditional_reg, label if label else name) | ||
aer_circ.multiplexer(qubits, params, conditional_reg, aer_cond_expr, | ||
label if label else name) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm surprised that this passes the lint CI, because this line splitting isn't compatible with black
, which I thought Aer was using.
releasenotes/notes/support_classical_expr-dd621e5c0fd23a15.yaml
Outdated
Show resolved
Hide resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think Expr implementation looks fine and I confirm there is no side effect by this change
* initial commit for classical expressions * add classical expression in C++ and its binding * Implemented ecr for stabilizer simulator. (Qiskit#1892) * Implemented ecr for stabilizer simulator. * Implemented ecr for stabilizer simulator. * Adapted to coding style. * fixed testcode for ecr in stabilizer. * remove deploy documentation to /documentation/aer (Qiskit#1891) Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com> * support classical expression evaluation for branches * fix lint errors * change Expr to CExpr * resolve lint errors * suppprt expr for batched shot GPU * use ExprVisitor in AerCompiler to traverse nodes in Expr * fix lint issues * use accept method instead of custom visit method for ExprVisitor in aer_compiler * fix lint errors * refactor aer_compiler * Update releasenotes/notes/support_classical_expr-dd621e5c0fd23a15.yaml Co-authored-by: Jake Lishman <[email protected]> * replace expr rather than modiyfing expr.var * fix lint error --------- Co-authored-by: MarcMaussner <[email protected]> Co-authored-by: Luciano Bello <[email protected]> Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com> Co-authored-by: Jun Doi <[email protected]> Co-authored-by: Jake Lishman <[email protected]>
Summary
Add support of simulation of classical expressions in Terra 0.24
Details and comments
This PR adds experimental feature to support classical expression introduced in Terra 0.24.
The supported expression is for test expression in
switch
,while
andif else
.Expr
class and its sub-classes are converted newly defined C++ classes inoperations.hpp
, which are bound to python.AerCompiler
supports conversion from branch conditions inswitch
while
, andif else
intoExpr
and then assembles the circuit toAerCircuit
with newly introduced C++ classes forExpr
.