def visit_Assign(self, node): """ Flatten an assignment. The only assignments we should see are Qubits, measurements, and runtime computations. If we see a measurement, we need to schedule the pulse (the RHS). A runtime computation is passed through as an opaque STORE command. """ if not isinstance(node.value, ast.Call): NodeError.error_msg(node, "Unexpected assignment [%s]" % ast2str(node)) if hasattr(node, 'qgl2_type'): if node.qgl2_type == 'qbit': return node elif node.qgl2_type == 'measurement': new_node = ast.Expr(value=node.value) new_node.qgl2_type = 'measurement' pyqgl2.ast_util.copy_all_loc(new_node, node) return new_node elif node.qgl2_type == 'runtime_call': # put the runtime_call in a STORE command new_node = expr2ast('Store()') # first argument is the STORE destination # TODO we want to re-write the target as an address target_str = ast2str(node.targets[0]).strip() new_node.value.args.append(ast.Str(s=target_str)) # second argument is the str() representation # of the runtime call call_str = ast2str(node.value).strip() new_node.value.args.append(ast.Str(s=call_str)) new_node.qgl2_type = 'runtime_call' pyqgl2.ast_util.copy_all_loc(new_node, node, recurse=True) return new_node return node
def emit_function(self, func_name='qgl1_main', setup=None): """ Create a function that, when run, creates the context in which the sequence is evaluated, and evaluate it. Assumes find_imports and find_sequences have already been called. func_name is the name for the function, if provided. I'm not certain that the name has any significance or whether this function will be, for all practical purposes, a lambda. """ # assumes a standard 4-space indent; if the surrounding # context uses some other indentation scheme, the interpreter # may gripe, and pep8 certainly will # indent = ' ' # FIXME: Ugliness # In the proper namespace we need to import all the QGL1 functions # that this method is using / might use. # Here we include the imports matching stuff in qgl2.qgl1.py # as required by create_imports_list(), plus # extras as required. # FIXME: Since QubitFactory is in qgl2.qgl1, why is this needed? # - find_imports didn't find QubitFactory? base_imports = indent + 'from QGL import QubitFactory\n' found_imports = ('\n' + indent).join(self.create_imports_list()) # define the method preamble preamble = 'def %s():\n' % func_name preamble += base_imports preamble += indent + found_imports preamble += '\n\n' # FIXME: In below block, calls to ast2str are the slowest part # (78%) of calling get_sequence_function. Fixable? for node in self.qbit_creates: preamble += indent + ast2str(node).strip() + '\n' if setup: for setup_stmnt in setup: preamble += indent + ('%s\n' % ast2str(setup_stmnt).strip()) sequence = [ast2str(item).strip() for item in self.sequence] # TODO there must be a more elegant way to indent this properly seq_str = indent + 'seq = [\n' + 2 * indent seq_str += (',\n' + 2 * indent).join(sequence) seq_str += '\n' + indent + ']\n' postamble = indent + 'return seq\n' res = preamble + seq_str + postamble return res
def with_qiter_eq(stmnt1, stmnt2): """ Given two "with-qiter" statements, determine whether they are identical and therefore the effect of the two statements can be implemented by repeating one of the statements twice. Returns True if the statements are identical, False otherwise. Note that the statements are assumed to be consecutive, NOT separated by a third statement. If the statements are separated, then the comparison is still legitimate, but does not imply that the two statements can be replaced with iteration. Also note that the comparison is assumed to be done AFTER the evaluator has finished transforming the statements, including rebinding any variables and/or variable names. At this point, only a small subset of valid Python statements and expressions may appear in the given statements. We use this assumption liberally, and therefore this method may need to change if/when the evaluator changes. """ try: stmnt_txt1 = ast2str(stmnt1).strip() except BaseException as exc: print('with_qiter_eq ast2str failed stmnt1 %s' % str(exc)) return False try: stmnt_txt2 = ast2str(stmnt2).strip() except BaseException as exc: print('with_qiter_eq ast2str failed stmnt2 %s' % str(exc)) return False # Just to be doubly certain, try converting the text back # to AST again, and then check those results. try: stmnt1_ast_again = expr2ast(stmnt_txt1) stmnt1_txt_again = ast2str(stmnt1_ast_again).strip() except BaseException as exc: print('with_qiter_eq expr2ast failed stmnt1 %s' % str(exc)) return False try: stmnt2_ast_again = expr2ast(stmnt_txt1) stmnt2_txt_again = ast2str(stmnt2_ast_again).strip() except BaseException as exc: print('with_qiter_eq expr2ast failed stmnt2 %s' % str(exc)) return False if stmnt_txt1 != stmnt_txt2: return False elif stmnt1_txt_again != stmnt2_txt_again: return False else: return True
def visit_Expr(self, node): # we only really understand stubs right now if isinstance(node.value, ast.Call): print('RTC CALL: %s' % ast2str(node.value).strip()) return node
def do_seq(self, qbits, body): chan_name = '/'.join(qbits) if chan_name not in self.qbit2sequence: self.qbit2sequence[chan_name] = list() for stmnt in body: txt = ast2str(stmnt).strip() self.qbit2sequence[chan_name].append(txt)
def factory(node, local_vars): ''' Evaluates a ast.Call node of a QRegister and returns its value. local_vars is a dictionary of symbol -> value bindings ''' if not is_qbit_create(node): NodeError.error_msg( node, "Attempted to create a QRegister from an invalid AST node [%s]." % ast2str(node)) # convert args into values arg_values = [] for arg in node.value.args: if isinstance(arg, ast.Num): arg_values.append(arg.n) elif isinstance(arg, ast.Str): arg_values.append(arg.s) elif isinstance(arg, ast.Name) and arg.id in local_vars: arg_values.append(local_vars[arg.id]) elif is_qbit_subscript(arg, local_vars): # evaluate the subscript to extract the referenced qubits parent_qreg = local_vars[arg.value.id] try: arg_val = eval(ast2str(arg), None, local_vars) except: NodeError.error_msg( node, "Unhandled qreg subscript [%s]" % ast2str(arg)) sub_qubits = parent_qreg.qubits[arg_val.idx] if hasattr(sub_qubits, '__iter__'): arg_values.extend("q" + str(n) for n in sub_qubits) else: arg_values.append("q" + str(sub_qubits)) else: NodeError.error_msg( node, "Unhandled argument to QRegister [%s]" % ast2str(arg)) return QRegister(*arg_values)
def expand_arg(self, arg): ''' Expands a single argument to a stub call. QRegisters are expanded to a list of constituent Qubits. QRegister subscripts are similar, except that the slice selects which Qubits to return. So, given a = QRegister(2) b = QRegister(1) we do (in AST shorthand): expand_arg("a") -> ["QBIT_1", "QBIT_2"] expand_arg("a[1]") -> ["QBIT_2"] ''' expanded_args = [] if isinstance(arg, ast.Name) and arg.id in self.allocated_qregs: qreg = self.allocated_qregs[arg.id] # add an argument for each constituent qubit in the QRegister for n in range(len(qreg)): new_arg = ast.Name(id=qreg.use_name(n), ctx=ast.Load()) expanded_args.append(new_arg) elif (isinstance(arg, ast.Subscript) and arg.value.id in self.allocated_qregs): # add an argument for the subset of qubits referred to # by the QReference # eval the subscript to extract the slice qreg = self.allocated_qregs[arg.value.id] try: qref = eval(ast2str(arg), None, self.allocated_qregs) except: NodeError.error_msg( arg, "Error evaluating QReference [%s]" % ast2str(arg)) # convert the slice into a list of indices idx = range(len(qreg))[qref.idx] if not hasattr(idx, '__iter__'): idx = (idx, ) for n in idx: new_arg = ast.Name(id=qreg.use_name(n), ctx=ast.Load()) expanded_args.append(new_arg) else: # don't expand it expanded_args.append(arg) return expanded_args
def do_concur(self, body): for stmnt in body: if not is_seq(stmnt): # TODO: this error message is not helpful NodeError.fatal_msg(stmnt, 'expected a "with seq" block') return else: # TODO: The grouper should annotate the seq statement # so we don't have to find the qbits again. # qbits = find_all_channels(stmnt) if not qbits: print('XXN body\n%s' % ast2str(stmnt)) self.do_seq(qbits, stmnt.body)
def make_cgoto_call(self, label, node, cmp_operator, cmp_addr, value): """ Create a conditional goto call """ if isinstance(cmp_operator, ast.Eq): cmp_ast = expr2ast('CmpEq("%s", %s)' % (cmp_addr, str(value))) elif isinstance(cmp_operator, ast.NotEq): cmp_ast = expr2ast('CmpNeq("%s", %s)' % (cmp_addr, str(value))) elif isinstance(cmp_operator, ast.Gt): cmp_ast = expr2ast('CmpGt("%s", %s)' % (cmp_addr, str(value))) elif isinstance(cmp_operator, ast.Lt): cmp_ast = expr2ast('CmpLt("%s", %s)' % (cmp_addr, str(value))) else: NodeError.error_msg( node, 'Unallowed comparison operator [%s]' % ast2str(node)) return None label_ast = expr2ast('Goto(BlockLabel(\'%s\'))' % label) pyqgl2.ast_util.copy_all_loc(cmp_ast, node, recurse=True) pyqgl2.ast_util.copy_all_loc(label_ast, node, recurse=True) return list([cmp_ast, label_ast])
def test_code(code_text): tree = ast.parse(code_text, mode='exec') sync = SynchronizeBlocks(tree) new = sync.visit(deepcopy(tree)) print('ORIG\n%s\n=>\n%s' % (ast2str(tree), ast2str(new)))
self.preamble.append(stmnt) else: self.apply_name_mappings(stmnt) new_body.append(stmnt) node.body = new_body return node if __name__ == '__main__': text = """ a = foo(1, 2) X(a) b = bar(a) b = bar(b) a = bar(a) X90(a) """ ptree = ast.parse(text, mode='exec') pre = Preamble() post = pre.separate(ptree) print(' %s' % ast2str(pre.preamble_module)) print('POST\n%s' % ast2str(post))
def test_code(code_text): tree = ast.parse(code_text, mode='exec') flat = Flattener() new = flat.visit(deepcopy(tree)) print('ORIG\n%s\n=>\n%s' % (ast2str(tree), ast2str(new)))
def visit_If(self, node): """ flatten an "if" statement, returning a new list of expressions that represent the flattened sequence """ # make sure that the test involves runtime values. # This is the only kind of test that should survive # to this point; classical test would have already # been executed. # FIXME add this check # Also, if the test contains a call, we should # move the evaluation of that call to a expression before # the comparison if (isinstance(node.test, ast.Name) or isinstance(node.test, ast.Call)): mask = 0 cmp_addr = node.test.id cmp_operator = ast.NotEq() elif (isinstance(node.test, ast.UnaryOp) and isinstance(node.test.op, ast.Not)): mask = 0 cmp_addr = node.test.operand.id cmp_operator = ast.Eq() elif isinstance(node.test, ast.Compare): # FIXME the value can be on either side of the comparison # this assumes that it is on the right mask = node.test.comparators[0].n cmp_addr = node.test.left.id cmp_operator = node.test.ops[0] else: NodeError.error_msg( node.test, 'unhandled test expression [%s]' % ast2str(node.test)) return node if_label, end_label = LabelManager.allocate_labels('if', 'if_end') cond_ast = self.make_cgoto_call(if_label, node.test, cmp_operator, cmp_addr, mask) # cond_ast is actually a list of AST nodes new_body = cond_ast end_goto_ast = self.make_ugoto_call(end_label) if_ast = self.make_label_call(if_label) end_label_ast = self.make_label_call(end_label) pyqgl2.ast_util.copy_all_loc(end_goto_ast, node, recurse=True) pyqgl2.ast_util.copy_all_loc(if_ast, node, recurse=True) pyqgl2.ast_util.copy_all_loc(end_label_ast, node, recurse=True) if node.orelse: new_body += self.flatten_body(node.orelse) new_body.append(end_goto_ast) new_body.append(if_ast) new_body += self.flatten_body(node.body) new_body.append(end_label_ast) return new_body
def emit_function(self, func_name='qgl1_main', setup=None): """ Create a function that, when run, creates the context in which the sequence is evaluated, and evaluate it. func_name is the name for the function, if provided. I'm not certain that the name has any significance or whether this function will be, for all practical purposes, a lambda. """ # assumes a standard 4-space indent; if the surrounding # context uses some other indentation scheme, the interpreter # may gripe, and pep8 certainly will # indent = ' ' # FIXME: Ugliness # In the proper namespace we need to import all the QGL1 functions # that this method is using / might use # Here we include the imports matching stuff in qgl2.qgl1.py # Can we perhaps annotate all the stubs with the proper # import statement and use that to figure out what to include here? base_imports = """ from QGL.PulseSequencePlotter import plot_pulse_files """ found_imports = ('\n' + indent).join(self.create_imports_list()) # allow QBIT parameters to be overridden # preamble = 'def %s(**kwargs):\n' % func_name preamble += base_imports preamble += indent + found_imports preamble += '\n\n' for (sym_name, _use_name, node) in self.qbit_creates: preamble += indent + 'if \'' + sym_name + '\' in kwargs:\n' preamble += (2 * indent) + sym_name preamble += ' = kwargs[\'%s\']\n' % sym_name preamble += indent + 'else:\n' preamble += (2 * indent) + ast2str(node).strip() + '\n' for (sym_name, use_name, _node) in self.qbit_creates: preamble += indent + '%s = %s\n' % (use_name, sym_name) if setup: for stmnt in setup: preamble += indent + ('%s\n' % ast2str(stmnt).strip()) seqs_def = indent + 'seqs = list()\n' seqs_str = '' seq_strs = list() for seq in self.sequences.values(): #print("Looking at seq %s" % seq) sequence = [ast2str(item).strip() for item in seq] #print ("It is %s" % sequence) # TODO: check that this is always OK. # # HACK ALERT: this might not always be the right thing to do # but right now extraneous calls to Sync at the start of # program appear to cause a problem, and they don't serve # any known purpose, so skip them. # while sequence[0] == 'Sync()': sequence = sequence[1:] # TODO there must be a more elegant way to indent this properly seq_str = indent + 'seq = [\n' + 2 * indent seq_str += (',\n' + 2 * indent).join(sequence) seq_str += '\n' + indent + ']\n' seq_strs.append(seq_str) for seq_str in seq_strs: seqs_str += seq_str # That was a single sequence. We want a list of sequences # FIXME: Really, we want a new sequence every time the source code used Init() seqs_str += indent + 'seqs += [seq]\n' postamble = indent + 'return seqs\n' res = preamble + seqs_def + seqs_str + postamble return res
def group(node, local_vars=None): # We want to make a copy of params of the root node, # but we DO NOT need to recurse through the body of # the node (and this is potentially a time-sink). # So we grab a reference to the node body, reassign # the node body to the empty list, make a copy of # the node recursively, and then put the original # body back. # orig_body = node.body node.body = list() new_node = quickcopy(node) node.body = orig_body all_qbits = MarkReferencedQbits.marker(node, local_vars=local_vars, force_recursion=True) # TODO: need to check that it's a FunctionDef alloc_stmnts = list() body_stmnts = list() # Divide between qbit allocation and ordinary # statements. This is ugly: it would be better # to move qbit allocation outside qgl2main. TODO # for stmnt in node.body: # if (isinstance(stmnt, ast.Assign) and # stmnt.targets[0].qgl_is_qbit): if isinstance(stmnt, ast.Assign): alloc_stmnts.append(stmnt) else: body_stmnts.append(stmnt) new_groups = list() qbits = sorted(all_qbits) for i in range(len(qbits)): qbit = qbits[i] # An optimization: we need to make a copy of body_stmnts # so we can modify it, but for the last iteration, we can # cannibalize it and use the input list as our scratch_body. # In the (common) case that there's only one qbit, this # has a large effect on performance. # if i == (len(qbits) - 1): scratch_body = body_stmnts else: scratch_body = quickcopy(body_stmnts) pruned_body = QbitPruner(set([qbit])).prune_body(scratch_body) if not pruned_body: continue with_group = expr2ast('with group(%s): pass' % qbit) copy_all_loc(with_group, node, recurse=True) # Insert a special Barrier named "group_marker..." # at the start of each per-qbit group. # The compiler later looks for this to know # which sequence is for which Qubit. bid = BarrierIdentifier.next_bid() beg_barrier = AddSequential.make_barrier_ast([qbit], with_group, name='group_marker', bid=bid) if DebugMsg.ACTIVE_LEVEL < 3: print("For qbit %s, inserting group_marker: %s" % (qbit, ast2str(beg_barrier))) with_group.body = [beg_barrier] + pruned_body with_group.qbit = qbit MarkReferencedQbits.marker(with_group, local_vars=local_vars) new_groups.append(with_group) with_grouped = expr2ast('with grouped: pass') copy_all_loc(with_grouped, node, recurse=True) MarkReferencedQbits.marker(with_grouped, local_vars=local_vars) with_grouped.body = new_groups new_node.body = alloc_stmnts + list([with_grouped]) return new_node
def visit(self, node): if not hasattr(node, 'qgl2_referenced_qbits'): return node # Special case: if we're called on a function def, # then use add_barriers instead, because function # definitions are different than ordinary statements # if isinstance(node, ast.FunctionDef): return self.add_barriers(node) # Save a copy of the current state, so we can # restore it later (even if we don't actually # modify the state) # prev_state = self.in_concur prev_qbits = self.referenced_qbits # Figure out if we might be entering a new execution mode # (with-concur or with-infunc). If we are, then we need # to change the state variables before recursing. # entering_concur = is_concur(node) entering_infunc = is_infunc(node) if entering_concur or entering_infunc: self.referenced_qbits = node.qgl2_referenced_qbits if entering_concur: self.in_concur = True elif entering_infunc: self.in_concur = False if hasattr(node, 'body'): node.body, new_refs = self.expand_body(node.body) if new_refs: node.qgl2_referenced_qbits = new_refs for stmnt in node.body: if not stmnt.qgl2_referenced_qbits: stmnt.qgl2_referenced_qbits = new_refs # if we're entering a concur block and have # more than one qbit to protect, then add a # begin and end barrier # if entering_concur and len(self.referenced_qbits) > 0: bid = BarrierIdentifier.next_bid() beg_barrier = self.make_barrier_ast(self.referenced_qbits, node, name='concur_beg', bid=bid) end_barrier = self.make_barrier_ast(self.referenced_qbits, node, name='concur_end', bid=bid) node.body = [beg_barrier] + node.body + [end_barrier] if hasattr(node, 'orelse') and node.orelse: node.orelse, new_refs = self.expand_body(node.orelse) if new_refs: node.qgl2_referenced_qbits = new_refs if DebugMsg.ACTIVE_LEVEL < 3: print('NODE %s => %s %s' % (ast2str(node).strip(), str( node.qgl2_referenced_qbits), str(self.referenced_qbits))) # put things back the way they were (even if we didn't # change anything) # self.in_concur = prev_state self.referenced_qbits = prev_qbits return node