microsoft/qdk

Public

mirrored from https://github.com/microsoft/qdkAvailable

CodeCommitsIssuesPull requestsActionsInsightsSecurity
v1.27.0

Branches

Tags

  • No tags available.
0Branches0Tags
Go to file
Add file
Code

Clone

HTTPS

Download ZIP

source/pip/qsharp/_device/_atom/_scheduler.py

949lines · modecode

1# Copyright (c) Microsoft Corporation.
2# Licensed under the MIT License.
3
4from ._utils import as_qis_gate, get_used_values, uses_any_value
5from pyqir import (
6 Call,
7 Instruction,
8 Function,
9 QirModuleVisitor,
10 FunctionType,
11 Type,
12 Linkage,
13 qubit_type,
14 qubit_id,
15 IntType,
16 Value,
17)
18from .._device import Device, Zone, ZoneType
19from collections import defaultdict
20from dataclasses import dataclass
21from itertools import chain
22from typing import Iterable, TypeAlias, Optional
23from fractions import Fraction
24from functools import lru_cache
25
26QubitId: TypeAlias = Value
27Location: TypeAlias = tuple[int, int]
28MoveGroupScaleFactor: TypeAlias = tuple[bool | Fraction, bool | Fraction]
29MOVE_GROUPS_PER_PARALLEL_SECTION = 1
30
31
32@dataclass
33class Move:
34 __slots__ = ("qubit_id_ptr", "src_loc", "dst_loc")
35
36 qubit_id_ptr: Value
37 src_loc: Location
38 dst_loc: Location
39
40 def __hash__(self):
41 return hash(self.qubit_id_ptr)
42
43 def __str__(self):
44 return f"Move Qubit({self.qubit_id}): {self.src_loc} -> {self.dst_loc}"
45
46 def __repr__(self):
47 return self.__str__()
48
49 @property
50 def qubit_id(self) -> int:
51 q_id = qubit_id(self.qubit_id_ptr)
52 assert q_id is not None, "Qubit id should be known"
53 return q_id
54
55 def parity(self):
56 return move_parity(self.src_loc, self.dst_loc)
57
58 def direction(self):
59 return move_direction(self.src_loc, self.dst_loc)
60
61
62@dataclass
63class PartialMove:
64 """A move missing its destination location."""
65
66 __slots__ = ("qubit_id_ptr", "src_loc")
67
68 qubit_id_ptr: Value
69 src_loc: Location
70
71 @property
72 def qubit_id(self) -> int:
73 q_id = qubit_id(self.qubit_id_ptr)
74 assert q_id is not None, "Qubit id should be known"
75 return q_id
76
77 def into_move(self, dst_loc: Location) -> Move:
78 return Move(self.qubit_id_ptr, self.src_loc, dst_loc)
79
80
81PartialMovePair: TypeAlias = tuple[PartialMove, PartialMove]
82
83
84def move_parity(source: Location, destination: Location) -> tuple[int, int]:
85 """Returns a tuple representing the parities of the source and destination columns of a move."""
86 return (source[1] % 2, destination[1] % 2)
87
88
89def move_direction(source: Location, destination: Location) -> tuple[int, int]:
90 """Returns a tuple representing if the move is up or down, and left or right."""
91 return (int(source[0] < destination[0]), int(source[1] < destination[1]))
92
93
94def index_from_parity_and_direction(ud: int, lr: int) -> int:
95 return 2 * ud + lr
96
97
98def is_invalid_move_pair(move1: Move, move2: Move) -> bool:
99 """
100 Returns true if the two moves are incompatible, i.e., if they have the same
101 source row then they must have the same destination row, and if they have the
102 same source column then they must have the same destination column.
103 """
104
105 source_row_diff = move1.src_loc[0] - move2.src_loc[0]
106 destination_row_diff = move1.dst_loc[0] - move2.dst_loc[0]
107 source_col_diff = move1.src_loc[1] - move2.src_loc[1]
108 destination_col_diff = move1.dst_loc[1] - move2.dst_loc[1]
109
110 return (
111 (source_row_diff == 0 and destination_row_diff != 0)
112 or (source_row_diff != 0 and destination_row_diff == 0)
113 or (source_col_diff == 0 and destination_col_diff != 0)
114 or (source_col_diff != 0 and destination_col_diff == 0)
115 )
116
117
118@lru_cache(maxsize=1 << 14)
119def scale_factor_helper(source_diff, destination_diff):
120 if destination_diff == 0:
121 return True
122 if (s := Fraction(source_diff, destination_diff)) >= 0:
123 return s
124
125
126def scale_factor(move1: Move, move2: Move) -> Optional[MoveGroupScaleFactor]:
127 """
128 Returns a tuple of two elements, representing the row displacement ratio and column
129 displacement ratio between the moves.
130 """
131
132 if is_invalid_move_pair(move1, move2):
133 return None
134
135 source_row_diff = move1.src_loc[0] - move2.src_loc[0]
136 destination_row_diff = move1.dst_loc[0] - move2.dst_loc[0]
137 source_col_diff = move1.src_loc[1] - move2.src_loc[1]
138 destination_col_diff = move1.dst_loc[1] - move2.dst_loc[1]
139 row_scale_factor = scale_factor_helper(source_row_diff, destination_row_diff)
140 col_scale_factor = scale_factor_helper(source_col_diff, destination_col_diff)
141
142 if row_scale_factor is not None and col_scale_factor is not None:
143 return row_scale_factor, col_scale_factor
144
145
146class MoveGroup:
147 """
148 Represents a group of moves that can be done at the same time.
149
150 Attributes:
151 moves (set): A set of moves that can be performed in parallel.
152 scale_factor (Optional[tuple[Fraction, Fraction]]): A tuple of fractions
153 representing the scale factors in the row and col axes between
154 moves. `None`, if there is a single element in the move set.
155 ref_move (Move): A move used as a representative of the group, used
156 to test compatibility of other moves with the group.
157 """
158
159 __slots__ = ("moves", "scale_factor", "ref_move")
160
161 def __init__(self, moves: Iterable[Move]):
162 self.moves = set(moves)
163 self.scale_factor = scale_factor(*moves) if len(self.moves) > 1 else None
164 self.ref_move = next(iter(moves))
165
166 def __len__(self) -> int:
167 return len(self.moves)
168
169 def add(self, move: Move):
170 """
171 Adds a move to this move group.
172
173 Args:
174 move (Move): The move to add.
175 """
176
177 # A move group with a single move doesn't have an associated scale factor.
178 # Therefore, we cannot test if a move is compatible with it, which means
179 # we cannot add moves to it.
180 assert (
181 self.scale_factor
182 ), "cannot add to move group candidate with a single move"
183 self.moves.add(move)
184
185 def remove(self, move: Move):
186 self.moves.remove(move)
187
188 def discard(self, move: Move):
189 self.moves.discard(move)
190
191
192class MoveGroupPool:
193 """A data structure that takes individual moves as input and organizes them
194 into groups of moves that can be executed in parallel.
195
196 Attributes:
197 moves: A set containing all the moves in the move-group pool.
198 move_group_candidates: A dict organizing the move-group candidates
199 by scale factor.
200 parity: The parity of source and destination columns of all the moves
201 in this pool.
202 direction: The up/down and left/right direction of all the moves
203 in this pool.
204 """
205
206 def __init__(self):
207 """Initializes a move-group pool for moves of the given `parity` and `direction`.
208 Args:
209 parity: The parity of source and destination columns of all the moves
210 in this pool.
211 direction: The up/down and left/right direction of all the moves
212 in this pool.
213 """
214 self.moves: Optional[list[Move]] = []
215 self.move_group_candidates: dict[MoveGroupScaleFactor, list[MoveGroup]] = (
216 defaultdict(list)
217 )
218 self.single_moves: set[Move] | list[Move] = set()
219
220 def move_group_candidates_iter(self) -> Iterable[MoveGroup]:
221 return chain(*self.move_group_candidates.values())
222
223 def is_empty(self) -> bool:
224 """Returns `True` if there are no moves left, `False` otherwise."""
225 return (
226 not any(s.moves for s in self.move_group_candidates_iter())
227 and not self.single_moves
228 )
229
230 def largest_move_group_candidate(self) -> Optional[MoveGroup]:
231 try:
232 return max(self.move_group_candidates_iter(), key=len)
233 except ValueError:
234 return None
235
236 def add(self, move: Move):
237 """Adds a move to the move-group pool.
238 Args:
239 move: The move to add. It must be of the same parity and direction as
240 the rest of the moves in this pool.
241 """
242 assert self.moves is not None
243
244 move_added = False
245
246 # Add the move to all the groups it is compatible with
247 for group_scale_factor, groups in self.move_group_candidates.items():
248 for group in groups:
249 if scale_factor(move, group.ref_move) == group_scale_factor:
250 group.add(move)
251 move_added = True
252
253 # Build a table organizing the moves by scale factor with respect to `move`.
254 moves_by_scale: dict[MoveGroupScaleFactor, list[Move]] = defaultdict(list)
255 for move2 in self.moves:
256 s = scale_factor(move, move2)
257 if s is None:
258 continue
259 moves_by_scale[s].append(move2)
260
261 # Try to create new candidates having the new move as the ref_move.
262 for s, moves in moves_by_scale.items():
263 candidates_with_same_scale_factor = self.move_group_candidates[s]
264 for move2 in moves:
265 for group in candidates_with_same_scale_factor:
266 if move2 in group.moves:
267 # This pair already belongs to an existing move group candidate,
268 # so we don't need to create a new one.
269 break
270 else:
271 # Create a new move group candidate.
272 new_candidate = MoveGroup((move, move2))
273
274 # Add previous moves to the new candidate.
275 new_candidate.moves.update(moves_by_scale[s])
276
277 candidates_with_same_scale_factor.append(new_candidate)
278 move_added = True
279
280 # This case triggers if `move` is not compatible with any move in `self.moves`.
281 if not move_added:
282 assert isinstance(self.single_moves, set)
283 self.single_moves.add(move)
284
285 self.moves.append(move)
286
287 def try_take(self, number_of_moves: int) -> list[Move]:
288 """Take up to `number_of_moves` from the largest move group candidate.
289 Args:
290 number_of_moves: The number of moves to take from this pool.
291 """
292 # Once we start taking moves from the MoveGroupPool, we don't need to add
293 # new moves. So we set `self.moves` to `None` as a safety measure.
294 if self.moves is not None:
295 self.moves = None
296
297 if largest_move_group_candidate := self.largest_move_group_candidate():
298 # Ensure moves are sorted by qubit ID to have a deterministic order.
299 moves = sorted(
300 largest_move_group_candidate.moves, key=lambda m: m.qubit_id
301 )[:number_of_moves]
302 moves_set = set(moves)
303 # Remove the taken moves from all candidates.
304 for group in self.move_group_candidates_iter():
305 group.moves -= moves_set
306 assert isinstance(self.single_moves, set)
307 self.single_moves -= moves_set
308 return moves
309 else:
310 if isinstance(self.single_moves, set):
311 self.single_moves = sorted(
312 self.single_moves, key=lambda m: m.qubit_id, reverse=True
313 )
314 if m := self.single_moves.pop():
315 return [m]
316 else:
317 return []
318
319 def take_largest_candidate(self) -> list[Move]:
320 """Take all the moves from the largest move group candidate."""
321 # Once we start taking moves from the MoveGroupPool, we don't need to add
322 # new moves. So we set `self.moves` to `None` as a safety measure.
323 if self.moves is not None:
324 self.moves = None
325
326 if largest_move_group_candidate := self.largest_move_group_candidate():
327 # Ensure moves are sorted by qubit ID to have a deterministic order.
328 moves = sorted(largest_move_group_candidate.moves, key=lambda m: m.qubit_id)
329 moves_set = largest_move_group_candidate.moves
330 # Remove the taken moves from all candidates.
331 for group in self.move_group_candidates_iter():
332 if group is not largest_move_group_candidate:
333 group.moves -= moves_set
334 assert isinstance(self.single_moves, set)
335 self.single_moves -= moves_set
336 moves_set.clear()
337 return moves
338 else:
339 if isinstance(self.single_moves, set):
340 self.single_moves = sorted(
341 self.single_moves, key=lambda m: m.qubit_id, reverse=True
342 )
343 if m := self.single_moves.pop():
344 return [m]
345 else:
346 return []
347
348
349class MoveScheduler:
350 """
351 Takes a device, a target zone, and a list of qubits to move to that
352 target zone and builds an iterator that returns groups of moves
353 that can be executed in parallel.
354
355 Attributes:
356 device: An object containing information about the device.
357 zone: The zone the moves will be scheduled to.
358 available_dst_locations: The available destinations in the `zone`.
359 partial_moves: The moves that haven't been assigned a destination location.
360 disjoint_pools: A list containing one pool of move-groups for each parity and direction.
361 """
362
363 def __init__(
364 self,
365 device: Device,
366 zone: Zone,
367 qubits_to_move: list[QubitId | tuple[QubitId, QubitId]],
368 ):
369 """Initializes the move scheduler from a device, a target zone,
370 and a list of qubits to move to that target zone.
371 Args:
372 device: An object containing information about the device.
373 zone: The zone the moves will be scheduled to.
374 qubits_to_move: A list of qubits to move.
375 """
376 self.device = device
377 self.zone = zone
378 self.available_dst_locations = self.build_zone_locations(zone)
379 self.move_group_pool = MoveGroupPool()
380
381 # Step through the partial moves and push them to the largest
382 # candidate they are compatible with.
383 partial_moves = self.qubits_to_partial_moves(qubits_to_move)
384 for partial_move in partial_moves:
385 if isinstance(partial_move, PartialMove):
386 self.add_to_largest_compatible_move_group(partial_move)
387 else:
388 self.add_pair_to_largest_compatible_move_group(partial_move)
389
390 def build_zone_locations(self, zone: Zone) -> dict[Location, None]:
391 zone_row_offset = zone.offset // self.device.column_count
392 # We use a dict with None values instead of a set to preserve order.
393 return {
394 (row, col): None
395 for row in range(
396 zone_row_offset,
397 zone_row_offset + zone.row_count,
398 )
399 for col in range(self.device.column_count)
400 }
401
402 def qubits_to_partial_moves(
403 self, qubits_to_move: list[QubitId | tuple[QubitId, QubitId]]
404 ) -> list[PartialMove | PartialMovePair]:
405 partial_moves = []
406 for elt in qubits_to_move:
407 if isinstance(elt, tuple):
408 q_id1 = qubit_id(elt[0])
409 q_id2 = qubit_id(elt[1])
410 assert q_id1 is not None
411 assert q_id2 is not None
412 mov1 = PartialMove(elt[0], self.device.get_home_loc(q_id1))
413 mov2 = PartialMove(elt[1], self.device.get_home_loc(q_id2))
414 partial_moves.append((mov1, mov2))
415 else:
416 q_id = qubit_id(elt)
417 assert q_id is not None
418 mov = PartialMove(elt, self.device.get_home_loc(q_id))
419 partial_moves.append(mov)
420
421 def sort_key(partial_move: PartialMove | PartialMovePair):
422 if isinstance(partial_move, PartialMove):
423 return self.device.get_ordering(partial_move.qubit_id)
424 else:
425 return self.device.get_ordering(partial_move[0].qubit_id)
426
427 return sorted(partial_moves, key=sort_key)
428
429 def is_empty(self):
430 """
431 Returns `True` if all moves were scheduled.
432 That is, there are no partial moves and all disjoint pools are empty.
433 """
434 return self.move_group_pool.is_empty()
435
436 def largest_move_group_pool(self) -> MoveGroupPool:
437 return self.move_group_pool
438
439 def add_to_largest_compatible_move_group(
440 self, partial_move: PartialMove
441 ) -> MoveGroupPool:
442 zone_row_offset = self.zone.offset // self.device.column_count
443
444 # Heuristic: Prefer moves that are straight up or down.
445 for row in range(zone_row_offset, zone_row_offset + self.zone.row_count):
446 dst_loc = (row, partial_move.src_loc[1])
447 if dst_loc in self.available_dst_locations:
448 move = partial_move.into_move(dst_loc)
449 pool = self.move_group_pool
450 pool.add(move)
451 del self.available_dst_locations[move.dst_loc]
452 return pool
453
454 if move := self.get_compatible_move(self.move_group_pool, partial_move):
455 self.move_group_pool.add(move)
456 del self.available_dst_locations[move.dst_loc]
457 return self.move_group_pool
458
459 raise Exception("not enough IZ space to schedule all moves")
460
461 def add_pair_to_largest_compatible_move_group(
462 self, partial_move_pair: PartialMovePair
463 ) -> MoveGroupPool:
464 zone_row_offset = self.zone.offset // self.device.column_count
465 partial_move = partial_move_pair[0]
466
467 # Heuristic: Prefer moves that are straight up or down.
468 if partial_move.src_loc[1] % 2 == 0:
469 for row in range(zone_row_offset, zone_row_offset + self.zone.row_count):
470 dst_loc1 = (row, partial_move.src_loc[1])
471 dst_loc2 = (row, partial_move.src_loc[1] + 1)
472 if (
473 dst_loc1 in self.available_dst_locations
474 and dst_loc2 in self.available_dst_locations
475 ):
476 move1 = partial_move.into_move(dst_loc1)
477 move2 = partial_move_pair[1].into_move(dst_loc2)
478 pool1 = self.move_group_pool
479 pool2 = self.move_group_pool
480 pool1.add(move1)
481 pool2.add(move2)
482 del self.available_dst_locations[dst_loc1]
483 del self.available_dst_locations[dst_loc2]
484 return pool1
485
486 if move1 := self.get_compatible_move(
487 self.move_group_pool, partial_move, is_pair=True
488 ):
489 # Push the move corresponding to the first qubit of the CZ pair.
490 self.move_group_pool.add(move1)
491
492 # Build the move corresponding to the second qubit of the CZ pair.
493 dest2 = (move1.dst_loc[0], move1.dst_loc[1] + 1)
494 move2 = partial_move_pair[1].into_move(dest2)
495 self.move_group_pool.add(move2)
496 del self.available_dst_locations[move1.dst_loc]
497 del self.available_dst_locations[move2.dst_loc]
498 return self.move_group_pool
499 raise Exception("not enough IZ space to schedule all moves")
500
501 def get_destination(
502 self,
503 partial_move: PartialMove,
504 scale_factor: MoveGroupScaleFactor,
505 group: MoveGroup,
506 ) -> Optional[Location]:
507 """
508 Returns an available destination location that would make `partial_move`
509 fit in the given group, or `None` if no such location exists.
510 """
511 row_scale_factor, col_scale_factor = scale_factor
512
513 if row_scale_factor is True:
514 dst_row = group.ref_move.dst_loc[0]
515 else:
516 # We compute the destination row by solving this equation for `dst_row`:
517 # src_row_diff / (group.ref_move.dst_loc[0] - dst_row) == row_scale_factor
518 src_row_diff = group.ref_move.src_loc[0] - partial_move.src_loc[0]
519 dst_row = group.ref_move.dst_loc[0] - src_row_diff / row_scale_factor
520 assert isinstance(dst_row, Fraction)
521 if dst_row.denominator == 1:
522 dst_row = dst_row.numerator
523 else:
524 return None
525
526 if col_scale_factor is True:
527 dst_col = group.ref_move.dst_loc[1]
528 else:
529 # We compute the destination col by solving this equation for `dst_col`:
530 # src_col_diff / (group.ref_move.dst_loc[1] - dst_col) == col_scale_factor
531 src_col_diff = group.ref_move.src_loc[1] - partial_move.src_loc[1]
532 dst_col = group.ref_move.dst_loc[1] - src_col_diff / col_scale_factor
533 assert isinstance(dst_col, Fraction)
534 if dst_col.denominator == 1:
535 dst_col = dst_col.numerator
536 else:
537 return None
538
539 loc = (dst_row, dst_col)
540 if loc in self.available_dst_locations:
541 return loc
542
543 def get_compatible_move(
544 self,
545 pool: MoveGroupPool,
546 partial_move: PartialMove,
547 is_pair=False,
548 ) -> Optional[Move]:
549 # First, try finding a large enough group to place the partial move in.
550 if self.zone.type != ZoneType.MEAS:
551 GROUP_SIZE_THRESHOLD = self.device.column_count // 4
552 best_destination: Optional[Location] = None
553 best_destination_group_len = 0
554 for scale, groups in pool.move_group_candidates.items():
555 for group in sorted(groups, key=len, reverse=True):
556 if (
557 len(group) < GROUP_SIZE_THRESHOLD
558 or len(group) < best_destination_group_len
559 ):
560 break
561 if destination := self.get_destination(partial_move, scale, group):
562 if (not is_pair) or destination[1] % 2 == 0:
563 best_destination = destination
564 best_destination_group_len = len(group)
565 break
566 if best_destination:
567 return partial_move.into_move(best_destination)
568
569 # If we didn't find a group to place the partial_move in,
570 # just pick the next available IZ location.
571 for destination in self.available_dst_locations:
572 if (not is_pair) or destination[1] % 2 == 0:
573 return partial_move.into_move(destination)
574
575 def __iter__(self):
576 return self
577
578 def __next__(self) -> list[Move]:
579 # If there are no moves left to schedule, stop the iteration.
580 if self.is_empty():
581 raise StopIteration
582
583 # Try_get from the largest candidate.
584 return self.largest_move_group_pool().take_largest_candidate()
585
586
587class Schedule(QirModuleVisitor):
588 """
589 Schedule instructions within a block, adding appropriate moves to the interaction zone to perform operations
590 """
591
592 begin_func: Function
593 end_func: Function
594 move_funcs: list[Function]
595
596 def __init__(self, device: Device):
597 super().__init__()
598 self.device = device
599 self.num_qubits = len(self.device.home_locs)
600 self.pending_moves: list[list[Move]] = []
601
602 def _on_module(self, module):
603 i64_ty = IntType(module.context, 64)
604 # Find or create the necessary runtime functions.
605 for func in module.functions:
606 if func.name == "__quantum__rt__begin_parallel":
607 self.begin_func = func
608 elif func.name == "__quantum__rt__end_parallel":
609 self.end_func = func
610 if not hasattr(self, "begin_func"):
611 self.begin_func = Function(
612 FunctionType(
613 Type.void(module.context),
614 [],
615 ),
616 Linkage.EXTERNAL,
617 "__quantum__rt__begin_parallel",
618 module,
619 )
620 if not hasattr(self, "end_func"):
621 self.end_func = Function(
622 FunctionType(
623 Type.void(module.context),
624 [],
625 ),
626 Linkage.EXTERNAL,
627 "__quantum__rt__end_parallel",
628 module,
629 )
630 self.move_func = Function(
631 FunctionType(
632 Type.void(module.context),
633 [qubit_type(module.context), i64_ty, i64_ty],
634 ),
635 Linkage.EXTERNAL,
636 "__quantum__qis__move__body",
637 module,
638 )
639
640 super()._on_module(module)
641
642 def _on_block(self, block):
643 # Use only the first interaction and measurement zone; more could be supported in future.
644 interaction_zone = self.device.get_interaction_zones()[0]
645 measurement_zone = self.device.get_measurement_zones()[0]
646 max_iz_pairs = (self.device.column_count // 2) * interaction_zone.row_count
647 max_measurements = self.device.column_count * measurement_zone.row_count
648
649 # Track pending/queued single qubit operations by qubit id.
650 self.single_qubit_ops = [[] for _ in range(self.num_qubits)]
651
652 # Track pending CZ operations.
653 self.curr_cz_ops = []
654
655 # Track pending measurements.
656 self.measurements = []
657
658 # Track pending qubits to move to an interaction or measurement zone.
659 self.pending_qubits_to_move: list[QubitId | tuple[QubitId, QubitId]] = []
660
661 # Track values used in CZ ops and measurements to avoid putting operations on the
662 # same qubit in the same batch.
663 self.vals_used_in_cz_ops = set()
664 self.vals_used_in_measurements = set()
665
666 instructions = [instr for instr in block.instructions]
667 for instr in instructions:
668 gate = as_qis_gate(instr)
669 if (
670 gate != {}
671 and len(gate["qubit_args"]) == 1
672 and len(gate["result_args"]) == 0
673 ):
674 # This is a single qubit gate; queue it up for later execution when this qubit is needed for CZ or measurement.
675
676 # If this qubit is involved in pending moves, that implies a CZ or measurement is pending, so flush now.
677 if any(
678 (
679 gate["qubit_args"][0] == qubit_id(q)
680 if isinstance(q, QubitId)
681 else (
682 gate["qubit_args"][0] == qubit_id(q[0])
683 or gate["qubit_args"][0] == qubit_id(q[1])
684 )
685 )
686 for q in self.pending_qubits_to_move
687 ):
688 self.flush_pending(instr)
689
690 # Remove the instruction from the block and queue by the qubit id.
691 instr.remove()
692 self.single_qubit_ops[gate["qubit_args"][0]].append((instr, gate))
693
694 elif gate != {} and len(gate["qubit_args"]) == 2:
695 # This is a CZ gate; queue it up to be executed in the next available interaction zone row.
696
697 # Pick next available interaction zone pair for these qubits. If none, flush the current set and start a fresh set.
698 # Create move instructions to move qubits to interaction zone and save them in pending moves for later insertion.
699 assert isinstance(instr, Call)
700 (vals_used, _) = get_used_values(instr)
701 if (
702 self.measurements
703 or uses_any_value(vals_used, self.vals_used_in_cz_ops)
704 or len(self.curr_cz_ops) >= max_iz_pairs
705 ):
706 self.flush_pending(instr)
707 instr.remove()
708 self.curr_cz_ops.append(instr)
709 self.vals_used_in_cz_ops.update(vals_used)
710
711 # Prefer using matching relative column ordering to home locations to reduce move crossings.
712 if (
713 self.device.get_home_loc(gate["qubit_args"][0])[1]
714 > self.device.get_home_loc(gate["qubit_args"][1])[1]
715 ):
716 self.pending_qubits_to_move.append((instr.args[1], instr.args[0]))
717 else:
718 self.pending_qubits_to_move.append((instr.args[0], instr.args[1]))
719
720 elif gate != {} and len(gate["result_args"]) == 1:
721 # This is a measurement; queue it up to be executed in the measurement zone.
722
723 # Pick next available measurement zone location for this qubit. If none, flush the current set and start a fresh set.
724 # Create move instructions to move qubit to measurement zone and save them in pending moves for later insertion.
725 assert isinstance(instr, Call)
726 (vals_used, _) = get_used_values(instr)
727 if (
728 not self.measurements
729 or len(self.measurements) >= max_measurements
730 or uses_any_value(vals_used, self.vals_used_in_measurements)
731 ):
732 self.flush_pending(instr)
733 if len(self.single_qubit_ops[gate["qubit_args"][0]]) > 0:
734 # There are still pending single qubits ops for the qubit we want to measure,
735 # so trigger another flush.
736 # We need to cache and restore the measurements and pending moves that have already
737 # been queued so that this flush affects the single qubit ops but not the measurements.
738 temp_meas = self.measurements
739 self.measurements = []
740 temp_moves = self.pending_qubits_to_move
741 self.pending_qubits_to_move = []
742 self.flush_pending(instr)
743 self.measurements = temp_meas
744 self.pending_qubits_to_move = temp_moves
745
746 # Remove the measurement from the block and queue it.
747 instr.remove()
748 self.measurements.append((instr, gate))
749 self.vals_used_in_measurements.update(vals_used)
750 self.pending_qubits_to_move.append(instr.args[0])
751 else:
752 # This is not a gate or measurement; flush any pending operations and leave the instruction in place.
753 # This uses a while loop to ensure all pending operations are flushed before the instruction.
754 while self.any_pending_ops():
755 self.flush_pending(instr)
756
757 def any_pending_single_qubit_ops(self):
758 return any(ops for ops in self.single_qubit_ops)
759
760 def any_pending_czs(self):
761 return bool(self.curr_cz_ops)
762
763 def any_pending_measurements(self):
764 return bool(self.measurements)
765
766 def any_pending_ops(self):
767 return (
768 self.any_pending_czs()
769 or self.any_pending_single_qubit_ops()
770 or self.any_pending_measurements()
771 )
772
773 def flush_pending(self, insert_before: Instruction):
774 interaction_zone = self.device.get_interaction_zones()[0]
775 self.builder.insert_before(insert_before)
776 # If cz ops pending, insert accumulated moves, single qubits ops matching cz rows, then the cz ops, then move back.
777 if self.curr_cz_ops:
778 self.schedule_pending_moves(interaction_zone)
779 self.insert_moves()
780 qubits_by_row = self.target_qubits_by_row(interaction_zone)
781 for qubits_in_row in qubits_by_row:
782 self.flush_single_qubit_ops(qubits_in_row)
783 self.builder.call(self.begin_func, [])
784 for cz_op in self.curr_cz_ops:
785 self.builder.instr(cz_op)
786 self.builder.call(self.end_func, [])
787 self.curr_cz_ops = []
788 self.insert_moves_back()
789 self.vals_used_in_cz_ops = set()
790 return
791 # If measurements pending, insert accumulated moves, then measurements, then move back.
792 elif len(self.measurements) > 0:
793 self.schedule_pending_moves(self.device.get_measurement_zones()[0])
794 self.insert_moves()
795 self.builder.call(self.begin_func, [])
796 for meas_op, meas_gate in self.measurements:
797 self.builder.instr(meas_op)
798 self.builder.call(self.end_func, [])
799 self.measurements = []
800 self.vals_used_in_measurements = set()
801 self.insert_moves_back()
802 return
803 # Else, create movements for remaining single qubit ops to the first interaction zone,
804 # insert those moves, then the ops, then move back.
805 else:
806 while self.any_pending_single_qubit_ops():
807 target_qubits_by_row = [[] for _ in range(interaction_zone.row_count)]
808 curr_row = 0
809 for q in range(self.num_qubits):
810 if len(self.single_qubit_ops[q]) > 0:
811 target_qubits_by_row[curr_row].append(q)
812 if (
813 len(target_qubits_by_row[curr_row])
814 >= self.device.column_count
815 ):
816 curr_row += 1
817 if curr_row >= interaction_zone.row_count:
818 break
819 for target_qubits in target_qubits_by_row:
820 for q in target_qubits:
821 qubit = self.single_qubit_ops[q][0][0].args[0]
822 if self.single_qubit_ops[q][0][1]["gate"] == "rz":
823 qubit = self.single_qubit_ops[q][0][0].args[1]
824 self.pending_qubits_to_move.append(qubit)
825 self.schedule_pending_moves(interaction_zone)
826 self.insert_moves()
827 qubits_by_row = self.target_qubits_by_row(interaction_zone)
828 for qubits_in_row in qubits_by_row:
829 self.flush_single_qubit_ops(qubits_in_row)
830 self.insert_moves_back()
831 return
832
833 def target_qubits_by_row(self, zone: Zone) -> list[list[int]]:
834 zone_row_offset = zone.offset // self.device.column_count
835 qubits_by_row: list[list[int]] = [[] for _ in range(zone.row_count)]
836 for group in self.pending_moves:
837 for move in group:
838 row_idx = move.dst_loc[0] - zone_row_offset
839 qubits_by_row[row_idx].append(move.qubit_id)
840 # Organize qubits in each row by qubit_id, so that parallel sections
841 # of single-qubit ops in the generated QIR are easier to read.
842 for row in qubits_by_row:
843 row.sort()
844 return qubits_by_row
845
846 def schedule_pending_moves(self, zone: Zone):
847 move_scheduler = MoveScheduler(self.device, zone, self.pending_qubits_to_move)
848 for move_group in move_scheduler:
849 self.pending_moves.append(move_group)
850 # self.verify_that_all_moves_were_scheduled()
851 self.pending_qubits_to_move = []
852
853 def verify_that_all_moves_were_scheduled(self):
854 moves_to_schedule = sum(
855 len(x) if isinstance(x, tuple) else 1 for x in self.pending_qubits_to_move
856 )
857 scheduled_moves = sum(len(group) for group in self.pending_moves)
858 assert (
859 moves_to_schedule == scheduled_moves
860 ), f"{moves_to_schedule} != {scheduled_moves}"
861
862 def insert_moves(self):
863 """
864 For each pending move, insert a call to the move function that moves the
865 given qubit to the given (row, col) location.
866 """
867 move_group_id = 0
868 for move_group in self.pending_moves:
869 # We can execute `MOVE_GROUPS_PER_PARALLEL_SECTION`, if
870 # this is the first one, start a parallel section.
871 if move_group_id == 0:
872 self.builder.call(self.begin_func, [])
873
874 # Insert all the moves in a group using the same move function.
875 for move in move_group:
876 self.builder.call(self.move_func, (move.qubit_id_ptr, *move.dst_loc))
877
878 # There `MOVE_GROUPS_PER_PARALLEL_SECTION` move groups,
879 # so we increment the id modulo `MOVE_GROUPS_PER_PARALLEL_SECTION`.
880 move_group_id = (move_group_id + 1) % MOVE_GROUPS_PER_PARALLEL_SECTION
881
882 # We can execute `MOVE_GROUPS_PER_PARALLEL_SECTION`, if
883 # this is the last one, end the parallel section.
884 if move_group_id == 0:
885 self.builder.call(self.end_func, [])
886
887 # End the parallel section if it hasn't been ended.
888 if move_group_id != 0:
889 self.builder.call(self.end_func, [])
890
891 def insert_moves_back(self):
892 move_group_id = 0
893 for move_group in self.pending_moves:
894 # We can execute `MOVE_GROUPS_PER_PARALLEL_SECTION`, if
895 # this is the first one, start a parallel section.
896 if move_group_id == 0:
897 self.builder.call(self.begin_func, [])
898
899 # Insert all the moves in a group using the same move function.
900 for move in move_group:
901 self.builder.call(self.move_func, (move.qubit_id_ptr, *move.src_loc))
902
903 # There `MOVE_GROUPS_PER_PARALLEL_SECTION` move groups,
904 # so we increment the id modulo `MOVE_GROUPS_PER_PARALLEL_SECTION`.
905 move_group_id = (move_group_id + 1) % MOVE_GROUPS_PER_PARALLEL_SECTION
906
907 # We can execute `MOVE_GROUPS_PER_PARALLEL_SECTION`, if
908 # this is the last one, end the parallel section.
909 if move_group_id == 0:
910 self.builder.call(self.end_func, [])
911
912 # End the parallel section if it hasn't been ended.
913 if move_group_id != 0:
914 self.builder.call(self.end_func, [])
915
916 # Clear pending moves.
917 self.pending_moves = []
918
919 def flush_single_qubit_ops(self, target_qubits):
920 # Flush all pending single qubit ops for the given target qubits, combining
921 # consecutive ops of the same type into a single parallel region by row in
922 # the interaction zone.
923 ops_to_flush = []
924 for q in target_qubits:
925 ops_to_flush.append(list(reversed(self.single_qubit_ops[q])))
926 self.single_qubit_ops[q] = []
927 while any(len(q_ops) > 0 for q_ops in ops_to_flush):
928 rz_ops = []
929 for q_ops in ops_to_flush:
930 if len(q_ops) == 0:
931 continue
932 if q_ops[-1][1]["gate"] == "rz":
933 rz_ops.append(q_ops.pop()[0])
934 if len(rz_ops) > 0:
935 self.builder.call(self.begin_func, [])
936 for rz_op in rz_ops:
937 self.builder.instr(rz_op)
938 self.builder.call(self.end_func, [])
939 sx_ops = []
940 for q_ops in ops_to_flush:
941 if len(q_ops) == 0:
942 continue
943 if q_ops[-1][1]["gate"] == "sx":
944 sx_ops.append(q_ops.pop()[0])
945 if len(sx_ops) > 0:
946 self.builder.call(self.begin_func, [])
947 for sx_op in sx_ops:
948 self.builder.instr(sx_op)
949 self.builder.call(self.end_func, [])
950