Sequential Quantum Gate Decomposer  v1.9.6
Powerful decomposition of general unitarias into one- and two-qubit gates gates
noisy_runtime_fusion.py
Go to the documentation of this file.
1 from __future__ import annotations
2 
3 from typing import Mapping
4 
5 import numpy as np
6 
7 from squander.density_matrix import DensityMatrix
8 from squander.partitioning.noisy_runtime_core import (
9  PHASE3_FUSION_CLASS_DEFERRED,
10  PHASE3_FUSION_CLASS_FUSED,
11  PHASE3_FUSION_CLASS_SUPPORTED_UNFUSED,
12  PHASE3_FUSION_KIND_NOISE_BOUNDARY,
13  PHASE3_FUSION_KIND_UNITARY_ISLAND,
14  NoisyRuntimeFusedRegionRecord,
15  _execute_member_sequence,
16 )
17 from squander.partitioning.noisy_runtime_errors import (
18  NoisyRuntimeValidationError,
19  runtime_validation_error,
20 )
21 from squander.partitioning.noisy_types import (
22  NoisyPartitionDescriptor,
23  NoisyPartitionDescriptorMember,
24  NoisyPartitionDescriptorSet,
25 )
26 
27 
29  descriptor_set: NoisyPartitionDescriptorSet,
30  members: tuple[NoisyPartitionDescriptorMember, ...],
31 ) -> tuple[tuple[bool, tuple[NoisyPartitionDescriptorMember, ...]], ...]:
32  if not members:
33  return tuple()
34  segments: list[tuple[bool, tuple[NoisyPartitionDescriptorMember, ...]]] = []
35  current_is_unitary = descriptor_set.canonical_operation_for(members[0]).is_unitary
36  current_members: list[NoisyPartitionDescriptorMember] = [members[0]]
37  for member in members[1:]:
38  if (
39  descriptor_set.canonical_operation_for(member).is_unitary
40  == current_is_unitary
41  ):
42  current_members.append(member)
43  continue
44  segments.append((current_is_unitary, tuple(current_members)))
45  current_is_unitary = descriptor_set.canonical_operation_for(member).is_unitary
46  current_members = [member]
47  segments.append((current_is_unitary, tuple(current_members)))
48  return tuple(segments)
49 
50 
52  members: tuple[NoisyPartitionDescriptorMember, ...],
53 ) -> tuple[int, ...]:
54  return tuple(
55  sorted(
56  {
57  local_qbit
58  for member in members
59  for local_qbit in member.local_qubit_support
60  }
61  )
62  )
63 
64 
65 def _u3_unitary(theta_over_2: float, phi: float, lam: float) -> np.ndarray:
66  return np.asarray(
67  [
68  [np.cos(theta_over_2), -np.exp(1j * lam) * np.sin(theta_over_2)],
69  [
70  np.exp(1j * phi) * np.sin(theta_over_2),
71  np.exp(1j * (phi + lam)) * np.cos(theta_over_2),
72  ],
73  ],
74  dtype=np.complex128,
75  )
76 
77 
79  local_parameter_vector: np.ndarray, local_start: int, local_stop: int
80 ) -> np.ndarray:
81  theta, phi, lam = local_parameter_vector[local_start:local_stop]
82  return _u3_unitary(float(theta), float(phi), float(lam))
83 
84 
86  *,
87  local_qbit_to_kernel_index: Mapping[int, int],
88  local_target_qbit: int,
89  local_control_qbit: int,
90 ) -> tuple[int, int]:
91  """Map descriptor CNOT wires to _embed_cnot_gate(kernel_control, kernel_target)."""
92  return (
93  local_qbit_to_kernel_index[local_control_qbit],
94  local_qbit_to_kernel_index[local_target_qbit],
95  )
96 
97 
99  gate_matrix: np.ndarray, *, total_kernel_qbits: int, kernel_target_qbit: int
100 ) -> np.ndarray:
101  dim = 1 << total_kernel_qbits
102  embedded = np.zeros((dim, dim), dtype=np.complex128)
103  for basis in range(dim):
104  input_bit = (basis >> kernel_target_qbit) & 1
105  base_state = basis & ~(1 << kernel_target_qbit)
106  for output_bit in (0, 1):
107  output_state = base_state | (output_bit << kernel_target_qbit)
108  embedded[output_state, basis] = gate_matrix[output_bit, input_bit]
109  return embedded
110 
111 
112 def _embed_cnot_gate(
113  *, total_kernel_qbits: int, kernel_control_qbit: int, kernel_target_qbit: int
114 ) -> np.ndarray:
115  dim = 1 << total_kernel_qbits
116  embedded = np.zeros((dim, dim), dtype=np.complex128)
117  for basis in range(dim):
118  output_state = basis
119  if (basis >> kernel_control_qbit) & 1:
120  output_state ^= 1 << kernel_target_qbit
121  embedded[output_state, basis] = 1.0
122  return embedded
123 
124 
126  descriptor_set: NoisyPartitionDescriptorSet,
127  member: NoisyPartitionDescriptorMember,
128  local_parameter_vector: np.ndarray,
129  *,
130  local_qbit_to_kernel_index: Mapping[int, int],
131  total_kernel_qbits: int,
132  runtime_path: str,
133 ) -> np.ndarray:
134  op = descriptor_set.canonical_operation_for(member)
135  if op.name == "U3":
136  if member.local_target_qbit is None:
138  descriptor_set,
139  category="descriptor_to_runtime_mismatch",
140  first_unsupported_condition="local_target_qbit",
141  failure_stage="runtime_preflight",
142  runtime_path=runtime_path,
143  reason="Fused partitioned runtime requires local_target_qbit for U3 members",
144  )
145  local_start = member.local_param_start
146  local_stop = local_start + op.param_count
147  if local_stop > local_parameter_vector.size:
149  descriptor_set,
150  category="descriptor_to_runtime_mismatch",
151  first_unsupported_condition="parameter_routing",
152  failure_stage="runtime_preflight",
153  runtime_path=runtime_path,
154  reason=(
155  "Fused partitioned runtime has out-of-range parameters for workload "
156  "'{}' at canonical operation {}".format(
157  descriptor_set.workload_id, member.canonical_operation_index
158  )
159  ),
160  )
163  local_parameter_vector, local_start, local_stop
164  ),
165  total_kernel_qbits=total_kernel_qbits,
166  kernel_target_qbit=local_qbit_to_kernel_index[member.local_target_qbit],
167  )
168  if op.name == "CNOT":
169  if member.local_target_qbit is None or member.local_control_qbit is None:
171  descriptor_set,
172  category="descriptor_to_runtime_mismatch",
173  first_unsupported_condition="local_control_qbit",
174  failure_stage="runtime_preflight",
175  runtime_path=runtime_path,
176  reason="Fused partitioned runtime requires both local qubits for CNOT members",
177  )
178  k_ctl, k_tgt = _kernel_indices_for_fused_cnot(
179  local_qbit_to_kernel_index=local_qbit_to_kernel_index,
180  local_target_qbit=member.local_target_qbit,
181  local_control_qbit=member.local_control_qbit,
182  )
183  return _embed_cnot_gate(
184  total_kernel_qbits=total_kernel_qbits,
185  kernel_control_qbit=k_ctl,
186  kernel_target_qbit=k_tgt,
187  )
189  descriptor_set,
190  category="unsupported_runtime_operation",
191  first_unsupported_condition="fusion_gate_name",
192  failure_stage="runtime_preflight",
193  runtime_path=runtime_path,
194  reason="Fused partitioned runtime cannot build a fused kernel for '{}'".format(
195  op.name
196  ),
197  )
198 
199 
201  descriptor_set: NoisyPartitionDescriptorSet,
202  partition: NoisyPartitionDescriptor,
203  members: tuple[NoisyPartitionDescriptorMember, ...],
204  local_parameter_vector: np.ndarray,
205  *,
206  runtime_path: str,
207 ) -> tuple[np.ndarray, tuple[int, ...], tuple[int, ...]]:
208  active_local_qbits = _unique_local_qbits(members)
209  if not active_local_qbits:
211  descriptor_set,
212  category="descriptor_to_runtime_mismatch",
213  first_unsupported_condition="local_qubit_support",
214  failure_stage="runtime_preflight",
215  runtime_path=runtime_path,
216  reason="Fused partitioned runtime requires non-empty local_qubit_support",
217  )
218  if len(active_local_qbits) > 2:
220  descriptor_set,
221  category="unsupported_runtime_operation",
222  first_unsupported_condition="fusion_qubit_span",
223  failure_stage="runtime_preflight",
224  runtime_path=runtime_path,
225  reason=(
226  "Fused partitioned runtime supports only unitary islands on up to 2 "
227  "qubits, got span {} for workload '{}'".format(
228  len(active_local_qbits), descriptor_set.workload_id
229  )
230  ),
231  )
232  local_qbit_to_kernel_index = {
233  local_qbit: kernel_index
234  for kernel_index, local_qbit in enumerate(active_local_qbits)
235  }
236  kernel_dim = 1 << len(active_local_qbits)
237  fused_kernel = np.eye(kernel_dim, dtype=np.complex128)
238  for member in members:
239  op = descriptor_set.canonical_operation_for(member)
240  if not op.is_unitary:
242  descriptor_set,
243  category="unsupported_runtime_operation",
244  first_unsupported_condition="fusion_member_kind",
245  failure_stage="runtime_preflight",
246  runtime_path=runtime_path,
247  reason="Fused partitioned runtime only supports unitary members inside one fused island",
248  )
249  gate_matrix = _build_gate_matrix_for_member(
250  descriptor_set,
251  member,
252  local_parameter_vector,
253  local_qbit_to_kernel_index=local_qbit_to_kernel_index,
254  total_kernel_qbits=len(active_local_qbits),
255  runtime_path=runtime_path,
256  )
257  fused_kernel = gate_matrix @ fused_kernel
258  global_target_qbits = tuple(
259  partition.local_to_global_qbits[local_qbit] for local_qbit in active_local_qbits
260  )
261  return fused_kernel, active_local_qbits, global_target_qbits
262 
263 
265  descriptor_set: NoisyPartitionDescriptorSet,
266  partition: NoisyPartitionDescriptor,
267  members: tuple[NoisyPartitionDescriptorMember, ...],
268  *,
269  classification: str,
270  reason: str,
271 ) -> NoisyRuntimeFusedRegionRecord:
272  active_local_qbits = _unique_local_qbits(members)
273  global_target_qbits = tuple(
274  partition.local_to_global_qbits[local_qbit] for local_qbit in active_local_qbits
275  )
276  return NoisyRuntimeFusedRegionRecord(
277  partition_index=partition.partition_index,
278  candidate_kind=PHASE3_FUSION_KIND_UNITARY_ISLAND,
279  classification=classification,
280  reason=reason,
281  partition_member_indices=tuple(
282  member.partition_member_index for member in members
283  ),
284  canonical_operation_indices=tuple(
285  member.canonical_operation_index for member in members
286  ),
287  operation_names=tuple(
288  descriptor_set.canonical_operation_for(m).name for m in members
289  ),
290  global_target_qbits=global_target_qbits,
291  local_target_qbits=active_local_qbits,
292  member_count=len(members),
293  gate_count=sum(
294  descriptor_set.canonical_operation_for(m).is_unitary for m in members
295  ),
296  )
297 
298 
300  descriptor_set: NoisyPartitionDescriptorSet,
301  partition: NoisyPartitionDescriptor,
302  left_members: tuple[NoisyPartitionDescriptorMember, ...],
303  boundary_members: tuple[NoisyPartitionDescriptorMember, ...],
304  right_members: tuple[NoisyPartitionDescriptorMember, ...],
305 ) -> NoisyRuntimeFusedRegionRecord:
306  relevant_members = left_members + boundary_members + right_members
307  active_local_qbits = _unique_local_qbits(relevant_members)
308  global_target_qbits = tuple(
309  partition.local_to_global_qbits[local_qbit] for local_qbit in active_local_qbits
310  )
311  return NoisyRuntimeFusedRegionRecord(
312  partition_index=partition.partition_index,
313  candidate_kind=PHASE3_FUSION_KIND_NOISE_BOUNDARY,
314  classification=PHASE3_FUSION_CLASS_DEFERRED,
315  reason="explicit_noise_boundary",
316  partition_member_indices=tuple(
317  member.partition_member_index for member in relevant_members
318  ),
319  canonical_operation_indices=tuple(
320  member.canonical_operation_index for member in relevant_members
321  ),
322  operation_names=tuple(
323  descriptor_set.canonical_operation_for(m).name for m in relevant_members
324  ),
325  global_target_qbits=global_target_qbits,
326  local_target_qbits=active_local_qbits,
327  member_count=len(relevant_members),
328  gate_count=sum(
329  descriptor_set.canonical_operation_for(m).is_unitary
330  for m in relevant_members
331  ),
332  )
333 
334 
335 
339 
340 
342  descriptor_set: NoisyPartitionDescriptorSet,
343  partition: NoisyPartitionDescriptor,
344  local_parameter_vector: np.ndarray,
345  rho: DensityMatrix,
346  *,
347  runtime_path: str,
348  allow_fusion: bool,
349 ) -> tuple[NoisyRuntimeFusedRegionRecord, ...]:
350  segments = _iter_member_segments(descriptor_set, partition.members)
351  fused_regions: list[NoisyRuntimeFusedRegionRecord] = []
352  for segment_index, (is_unitary, segment_members) in enumerate(segments):
353  if is_unitary:
354  eligible_for_fusion = len(segment_members) >= 2
355  if allow_fusion and eligible_for_fusion:
356  try:
357  fused_kernel, active_local_qbits, global_target_qbits = (
359  descriptor_set,
360  partition,
361  segment_members,
362  local_parameter_vector,
363  runtime_path=runtime_path,
364  )
365  )
366  except NoisyRuntimeValidationError as exc:
367  if exc.first_unsupported_condition != "fusion_qubit_span":
368  raise
370  descriptor_set,
371  segment_members,
372  local_parameter_vector,
373  rho,
374  runtime_path=runtime_path,
375  )
376  fused_regions.append(
378  descriptor_set,
379  partition,
380  segment_members,
381  classification=PHASE3_FUSION_CLASS_DEFERRED,
382  reason="fusion_qubit_span",
383  )
384  )
385  continue
386  try:
387  rho.apply_local_unitary(
388  np.asarray(fused_kernel, dtype=np.complex128),
389  list(global_target_qbits),
390  )
391  except Exception as exc:
393  descriptor_set,
394  category="unsupported_runtime_execution",
395  first_unsupported_condition="fused_partition_execution",
396  failure_stage="runtime_execution",
397  runtime_path=runtime_path,
398  reason=(
399  "Fused partitioned runtime failed while executing partition {} "
400  "of workload '{}' on local span {}: {}".format(
401  partition.partition_index,
402  descriptor_set.workload_id,
403  list(active_local_qbits),
404  exc,
405  )
406  ),
407  ) from exc
408  fused_regions.append(
410  descriptor_set,
411  partition,
412  segment_members,
413  classification=PHASE3_FUSION_CLASS_FUSED,
414  reason="eligible_unitary_island",
415  )
416  )
417  else:
419  descriptor_set,
420  segment_members,
421  local_parameter_vector,
422  rho,
423  runtime_path=runtime_path,
424  )
425  fused_regions.append(
427  descriptor_set,
428  partition,
429  segment_members,
430  classification=PHASE3_FUSION_CLASS_SUPPORTED_UNFUSED,
431  reason=(
432  "fusion_disabled"
433  if eligible_for_fusion
434  else "singleton_unitary_region"
435  ),
436  )
437  )
438  continue
439 
441  descriptor_set,
442  segment_members,
443  local_parameter_vector,
444  rho,
445  runtime_path=runtime_path,
446  )
447  if segment_index == 0 or segment_index == len(segments) - 1:
448  continue
449  left_is_unitary, left_members = segments[segment_index - 1]
450  right_is_unitary, right_members = segments[segment_index + 1]
451  if left_is_unitary and right_is_unitary:
452  fused_regions.append(
454  descriptor_set,
455  partition,
456  left_members,
457  segment_members,
458  right_members,
459  )
460  )
461  return tuple(fused_regions)
def _execute_partition_with_optional_fusion
: execute a partition with optional fusion explain: this function is used to execute a partition with...