|
20 | 20 | Callable,
|
21 | 21 | Dict,
|
22 | 22 | Hashable,
|
| 23 | + List, |
23 | 24 | Optional,
|
24 | 25 | Sequence,
|
25 | 26 | TYPE_CHECKING,
|
@@ -129,6 +130,122 @@ def map_operations_and_unroll(
|
129 | 130 | return unroll_circuit_op(map_operations(circuit, map_func))
|
130 | 131 |
|
131 | 132 |
|
| 133 | +def merge_operations( |
| 134 | + circuit: CIRCUIT_TYPE, |
| 135 | + merge_func: Callable[[ops.Operation, ops.Operation], Optional[ops.Operation]], |
| 136 | +) -> CIRCUIT_TYPE: |
| 137 | + """Merges operations in a circuit by calling `merge_func` iteratively on operations. |
| 138 | +
|
| 139 | + Two operations op1 and op2 are merge-able if |
| 140 | + - There is no other operations between op1 and op2 in the circuit |
| 141 | + - is_subset(op1.qubits, op2.qubits) or is_subset(op2.qubits, op1.qubits) |
| 142 | +
|
| 143 | + The `merge_func` is a callable which, given two merge-able operations |
| 144 | + op1 and op2, decides whether they should be merged into a single operation |
| 145 | + or not. If not, it should return None, else it should return the single merged |
| 146 | + operations `op`. |
| 147 | +
|
| 148 | + The method iterates on the input circuit moment-by-moment from left to right and attempts |
| 149 | + to repeatedly merge each operation in the latest moment with all the corresponding merge-able |
| 150 | + operations to it's left. |
| 151 | +
|
| 152 | + If op1 and op2 are merged, both op1 and op2 are deleted from the circuit and |
| 153 | + the resulting `merged_op` is inserted at the index corresponding to the larger |
| 154 | + of op1/op2. If both op1 and op2 act on the same number of qubits, `merged_op` is |
| 155 | + inserted in the smaller moment index to minimize circuit depth. |
| 156 | +
|
| 157 | + The number of calls to `merge_func` is O(N), where N = Total no. of operations, because: |
| 158 | + - Every time the `merge_func` returns a new operation, the number of operations in the |
| 159 | + circuit reduce by 1 and hence this can happen at most O(N) times |
| 160 | + - Every time the `merge_func` returns None, the current operation is inserted into the |
| 161 | + frontier and we go on to process the next operation, which can also happen at-most |
| 162 | + O(N) times. |
| 163 | +
|
| 164 | + Args: |
| 165 | + circuit: Input circuit to apply the transformations on. The input circuit is not mutated. |
| 166 | + merge_func: Callable to determine whether two merge-able operations in the circuit should |
| 167 | + be merged. If the operations can be merged, the callable should return the merged |
| 168 | + operation, else None. |
| 169 | +
|
| 170 | + Returns: |
| 171 | + Copy of input circuit with merged operations. |
| 172 | +
|
| 173 | + Raises: |
| 174 | + ValueError if the merged operation acts on new qubits outside the set of qubits |
| 175 | + corresponding to the original operations to be merged. |
| 176 | + """ |
| 177 | + |
| 178 | + def apply_merge_func(op1: ops.Operation, op2: ops.Operation) -> Optional[ops.Operation]: |
| 179 | + new_op = merge_func(op1, op2) |
| 180 | + qubit_set = frozenset(op1.qubits + op2.qubits) |
| 181 | + if new_op is not None and not qubit_set.issuperset(new_op.qubits): |
| 182 | + raise ValueError( |
| 183 | + f"Merged operation {new_op} must act on a subset of qubits of " |
| 184 | + f"original operations {op1} and {op2}" |
| 185 | + ) |
| 186 | + return new_op |
| 187 | + |
| 188 | + ret_circuit = circuits.Circuit() |
| 189 | + for current_moment in circuit: |
| 190 | + new_moment = ops.Moment() |
| 191 | + for op in current_moment: |
| 192 | + op_qs = set(op.qubits) |
| 193 | + idx = ret_circuit.prev_moment_operating_on(tuple(op_qs)) |
| 194 | + if idx is not None and op_qs.issubset(ret_circuit[idx][op_qs].operations[0].qubits): |
| 195 | + # Case-1: Try to merge op with the larger operation on the left. |
| 196 | + left_op = ret_circuit[idx][op_qs].operations[0] |
| 197 | + new_op = apply_merge_func(left_op, op) |
| 198 | + if new_op is not None: |
| 199 | + ret_circuit.batch_replace([(idx, left_op, new_op)]) |
| 200 | + else: |
| 201 | + new_moment = new_moment.with_operation(op) |
| 202 | + continue |
| 203 | + |
| 204 | + while idx is not None and len(op_qs) > 0: |
| 205 | + # Case-2: left_ops will merge right into `op` whenever possible. |
| 206 | + for left_op in ret_circuit[idx][op_qs].operations: |
| 207 | + is_merged = False |
| 208 | + if op_qs.issuperset(left_op.qubits): |
| 209 | + # Try to merge left_op into op |
| 210 | + new_op = apply_merge_func(left_op, op) |
| 211 | + if new_op is not None: |
| 212 | + ret_circuit.batch_remove([(idx, left_op)]) |
| 213 | + op, is_merged = new_op, True |
| 214 | + if not is_merged: |
| 215 | + op_qs -= frozenset(left_op.qubits) |
| 216 | + idx = ret_circuit.prev_moment_operating_on(tuple(op_qs)) |
| 217 | + new_moment = new_moment.with_operation(op) |
| 218 | + ret_circuit += new_moment |
| 219 | + return _to_target_circuit_type(ret_circuit, circuit) |
| 220 | + |
| 221 | + |
| 222 | +def merge_moments( |
| 223 | + circuit: CIRCUIT_TYPE, |
| 224 | + merge_func: Callable[[ops.Moment, ops.Moment], Optional[ops.Moment]], |
| 225 | +) -> CIRCUIT_TYPE: |
| 226 | + """Merges adjacent moments, one by one from left to right, by calling `merge_func(m1, m2)`. |
| 227 | +
|
| 228 | + Args: |
| 229 | + circuit: Input circuit to apply the transformations on. The input circuit is not mutated. |
| 230 | + merge_func: Callable to determine whether two adjacent moments in the circuit should be |
| 231 | + merged. If the moments can be merged, the callable should return the merged moment, |
| 232 | + else None. |
| 233 | +
|
| 234 | + Returns: |
| 235 | + Copy of input circuit with merged moments. |
| 236 | + """ |
| 237 | + if not circuit: |
| 238 | + return circuit |
| 239 | + merged_moments: List[ops.Moment] = [circuit[0]] |
| 240 | + for current_moment in circuit[1:]: |
| 241 | + merged_moment = merge_func(merged_moments[-1], current_moment) |
| 242 | + if not merged_moment: |
| 243 | + merged_moments.append(current_moment) |
| 244 | + else: |
| 245 | + merged_moments[-1] = merged_moment |
| 246 | + return _create_target_circuit_type(merged_moments, circuit) |
| 247 | + |
| 248 | + |
132 | 249 | def _check_circuit_op(op, tags_to_check: Optional[Sequence[Hashable]]):
|
133 | 250 | return isinstance(op.untagged, circuits.CircuitOperation) and (
|
134 | 251 | tags_to_check is None or any(tag in op.tags for tag in tags_to_check)
|
|
0 commit comments