microsoft/qdk

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
brlackey/neutral-atom-models

Branches

Tags

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

Clone

HTTPS

Download ZIP

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

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