Sequential Quantum Gate Decomposer  v1.9.6
Powerful decomposition of general unitarias into one- and two-qubit gates gates
test_circuit.py
Go to the documentation of this file.
1 '''
2 Copyright 2020 Peter Rakyta, Ph.D.
3 
4 Licensed under the Apache License, Version 2.0 (the "License");
5 you may not use this file except in compliance with the License.
6 You may obtain a copy of the License at
7 
8  http://www.apache.org/licenses/LICENSE-2.0
9 
10 Unless required by applicable law or agreed to in writing, software
11 distributed under the License is distributed on an "AS IS" BASIS,
12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 See the License for the specific language governing permissions and
14 limitations under the License.
15 
16 Tests for qgd_Circuit: covers float32/float64 dispatch, state vector vs unitary,
17 apply_from_right, gate fusion (set_min_fusion), nested circuits, and boundary qubit
18 counts that stress AVX kernels (small kernel: qbit_num<14, TBB large kernel: qbit_num>=14).
19 '''
20 
21 import numpy as np
22 import pytest
23 
24 from squander.gates.qgd_Circuit import qgd_Circuit as Circuit
25 
26 # ──────────────────────────────────────────────────────────────────────────────
27 # helpers
28 # ──────────────────────────────────────────────────────────────────────────────
29 
30 def _identity(n, dtype=np.complex128):
31  return np.eye(n, dtype=dtype, order='C')
32 
33 
34 def _random_state(n, dtype=np.complex128, seed=42):
35  """Return a normalised random state vector of length n."""
36  rng = np.random.default_rng(seed)
37  v = rng.standard_normal(n) + 1j * rng.standard_normal(n)
38  v = v.astype(dtype)
39  return np.ascontiguousarray(v / np.linalg.norm(v))
40 
41 
42 def _params64(n):
43  """Float64 parameter array of length n."""
44  if n == 0:
45  return np.array([], dtype=np.float64)
46  return np.linspace(0.1, 0.1 * n, n, dtype=np.float64)
47 
48 
49 def _params32(n):
50  """Float32 parameter array of length n."""
51  if n == 0:
52  return np.array([], dtype=np.float32)
53  return np.linspace(0.1, 0.1 * n, n, dtype=np.float32)
54 
55 
57  """Build RX(0) → RZ(0) → CNOT(0,1) circuit if qbit_num >= 2, else RX → RZ."""
58  c = Circuit(qbit_num)
59  c.add_RX(0)
60  c.add_RZ(0)
61  if qbit_num >= 2:
62  c.add_CNOT(0, 1)
63  c.add_RY(1)
64  return c
65 
66 
67 def _ref_matrix64(circuit):
68  """Compute reference unitary via get_Matrix (float64)."""
69  params = _params64(circuit.get_Parameter_Num())
70  return np.array(circuit.get_Matrix(params))
71 
72 
73 def _assert_unitary_close(a, b, rtol=1e-6):
74  """Assert two unitaries are the same up to a global phase."""
75  assert a.shape == b.shape, f"shape mismatch {a.shape} vs {b.shape}"
76  overlap = np.vdot(a.reshape(-1), b.reshape(-1))
77  if abs(overlap) > 0:
78  b = b * np.exp(-1j * np.angle(overlap))
79  err = np.linalg.norm(a - b)
80  assert err < rtol, f"unitary mismatch: error={err:.3e}"
81 
82 
83 def _assert_state_close(a, b, rtol=1e-6):
84  """Assert two state vectors are the same up to a global phase."""
85  assert a.shape == b.shape
86  overlap = np.vdot(a, b)
87  if abs(overlap) > 0:
88  b = b * np.exp(-1j * np.angle(overlap))
89  err = np.linalg.norm(a - b)
90  assert err < rtol, f"state mismatch: error={err:.3e}"
91 
92 
93 def _random_unitary(dim, seed=123):
94  """Create a deterministic random unitary matrix using QR decomposition."""
95  rng = np.random.default_rng(seed)
96  a = rng.standard_normal((dim, dim)) + 1j * rng.standard_normal((dim, dim))
97  q, r = np.linalg.qr(a)
98  d = np.diag(r)
99  phase = np.where(np.abs(d) > 0, d / np.abs(d), 1.0)
100  q = q * phase
101  return np.ascontiguousarray(q, dtype=np.complex128)
102 
103 
104 # ──────────────────────────────────────────────────────────────────────────────
105 # Qubit counts chosen to stress different AVX dispatch paths.
106 # qbit_num = 1,2,3 → small gate kernel (direct index arithmetic)
107 # qbit_num = 4,5,6 → medium sizes, apply_large_kernel_to_input_AVX
108 # qbit_num = 14 → AVX TBB path (qbit_num >= 14, for fusion)
109 # ──────────────────────────────────────────────────────────────────────────────
110 QBIT_NUMS_SMALL = [1, 2, 3]
111 QBIT_NUMS_MEDIUM = [4, 5, 6]
112 QBIT_NUMS_ALL = QBIT_NUMS_SMALL + QBIT_NUMS_MEDIUM
113 RECT_COLS_MAX = 32
114 
115 
117  """Apply_to float64: unitary and state-vector correctness."""
118 
119  @pytest.mark.parametrize("qbit_num", QBIT_NUMS_ALL)
121  """apply_to on identity should reproduce get_Matrix output."""
122  circ = _build_rx_rz_cnot_circuit(qbit_num)
123  params = _params64(circ.get_Parameter_Num())
124  ref = np.array(circ.get_Matrix(params))
125 
126  dim = 2 ** qbit_num
127  identity = _identity(dim)
128  circ.apply_to(params, identity)
129 
130  _assert_unitary_close(ref, identity)
131 
132  @pytest.mark.parametrize("qbit_num", QBIT_NUMS_ALL)
133  def test_apply_to_state_vector(self, qbit_num):
134  """apply_to on a state vector should give U @ |state>."""
135  circ = _build_rx_rz_cnot_circuit(qbit_num)
136  params = _params64(circ.get_Parameter_Num())
137  U = np.array(circ.get_Matrix(params))
138 
139  dim = 2 ** qbit_num
140  state = _random_state(dim, dtype=np.complex128)
141  expected = U @ state
142 
143  sv = state.copy()
144  circ.apply_to(params, sv.reshape(dim, 1))
145  result = sv.reshape(-1)
146 
147  _assert_state_close(expected, result)
148 
149  @pytest.mark.parametrize("qbit_num", QBIT_NUMS_ALL)
151  """Applying circuit twice equals U^2 applied to identity."""
152  circ = _build_rx_rz_cnot_circuit(qbit_num)
153  params = _params64(circ.get_Parameter_Num())
154  U = np.array(circ.get_Matrix(params))
155 
156  dim = 2 ** qbit_num
157  mtx = _identity(dim)
158  circ.apply_to(params, mtx)
159  circ.apply_to(params, mtx)
160 
161  expected = U @ U
162  _assert_unitary_close(expected, mtx)
163 
164 
166  """Apply_to float32: complex64 precision correctness against float64 reference."""
167 
168  @pytest.mark.parametrize("qbit_num", QBIT_NUMS_ALL)
170  """float32 apply_to result should be close to float64 result."""
171  circ = _build_rx_rz_cnot_circuit(qbit_num)
172  p64 = _params64(circ.get_Parameter_Num())
173  p32 = p64.astype(np.float32)
174 
175  dim = 2 ** qbit_num
176 
177  # float64 reference
178  ref64 = _identity(dim, dtype=np.complex128)
179  circ.apply_to(p64, ref64)
180 
181  # float32 result
182  mat32 = _identity(dim, dtype=np.complex64)
183  circ.apply_to(p32, mat32, is_f32=True)
184 
185  _assert_unitary_close(ref64, mat32.astype(np.complex128), rtol=1e-4)
186 
187  @pytest.mark.parametrize("qbit_num", QBIT_NUMS_ALL)
188  def test_apply_to_f32_state_vector(self, qbit_num):
189  """float32 state vector apply matches float64 reference."""
190  circ = _build_rx_rz_cnot_circuit(qbit_num)
191  p64 = _params64(circ.get_Parameter_Num())
192  p32 = p64.astype(np.float32)
193 
194  dim = 2 ** qbit_num
195  state64 = _random_state(dim, dtype=np.complex128)
196  state32 = state64.astype(np.complex64)
197 
198  sv64 = state64.reshape(dim, 1).copy()
199  circ.apply_to(p64, sv64)
200  result64 = sv64.reshape(-1)
201 
202  sv32 = state32.reshape(dim, 1).copy()
203  circ.apply_to(p32, sv32, is_f32=True)
204  result32 = sv32.reshape(-1)
205 
206  _assert_state_close(result64, result32.astype(np.complex128), rtol=1e-4)
207 
208  @pytest.mark.parametrize("qbit_num", QBIT_NUMS_ALL)
210  """Output dtype should remain complex64 after float32 apply_to."""
211  circ = _build_rx_rz_cnot_circuit(qbit_num)
212  p32 = _params32(circ.get_Parameter_Num())
213 
214  dim = 2 ** qbit_num
215  mat = _identity(dim, dtype=np.complex64)
216  circ.apply_to(p32, mat, is_f32=True)
217 
218  assert mat.dtype == np.complex64, f"expected complex64, got {mat.dtype}"
219 
221  """Passing complex128 with is_f32=True should raise."""
222  circ = Circuit(2)
223  circ.add_RX(0)
224  params = _params32(1)
225  mat = _identity(4, dtype=np.complex128) # wrong dtype for f32 path
226  with pytest.raises(Exception):
227  circ.apply_to(params, mat, is_f32=True)
228 
230  """Passing complex64 with is_f32=False (default) should raise."""
231  circ = Circuit(2)
232  circ.add_RX(0)
233  params = _params64(1)
234  mat = _identity(4, dtype=np.complex64) # wrong dtype for f64 path
235  with pytest.raises(Exception):
236  circ.apply_to(params, mat, is_f32=False)
237 
238 
240  """apply_from_right: both f64 and f32.
241 
242  Gates_block::apply_from_right iterates gates in forward order but reads
243  the parameter array from the END backward (by design for gradient
244  computations). For a SINGLE-GATE circuit there is no ordering ambiguity,
245  so we can verify the result against the matrix; for multi-gate circuits we
246  verify f32/f64 consistency and that the matrix is actually mutated.
247  """
248 
249  @pytest.mark.parametrize("qbit_num", QBIT_NUMS_SMALL + [4])
251  """For a single-gate circuit apply_from_right(params, I) == U."""
252  circ = Circuit(qbit_num)
253  circ.add_RX(0) # single parametric gate
254 
255  params = _params64(circ.get_Parameter_Num())
256  U = np.array(circ.get_Matrix(params))
257 
258  dim = 2 ** qbit_num
259  mat = _identity(dim)
260  circ.apply_from_right(params, mat) # I @ U = U
261 
262  _assert_unitary_close(U, mat)
263 
264  @pytest.mark.parametrize("qbit_num", QBIT_NUMS_SMALL + [4])
266  """Single parameter-free gate (H): apply_from_right([], A) == A @ H."""
267  circ = Circuit(qbit_num)
268  circ.add_H(0)
269 
270  params = np.array([], dtype=np.float64)
271  U = np.array(circ.get_Matrix(params)) # should be H ⊗ I...
272 
273  dim = 2 ** qbit_num
274  mat = _identity(dim)
275  circ.apply_from_right(params, mat)
276 
277  _assert_unitary_close(U, mat)
278 
279  @pytest.mark.parametrize("qbit_num", QBIT_NUMS_SMALL + [4])
281  """apply_from_right must modify the input matrix in-place."""
282  circ = _build_rx_rz_cnot_circuit(qbit_num)
283  params = _params64(circ.get_Parameter_Num())
284 
285  dim = 2 ** qbit_num
286  mat = _identity(dim)
287  circ.apply_from_right(params, mat)
288 
289  # As long as the circuit is non-trivial the matrix should differ from I
290  assert not np.allclose(mat, np.eye(dim, dtype=np.complex128), atol=1e-6), (
291  "apply_from_right did not modify the input matrix"
292  )
293 
294  @pytest.mark.parametrize("qbit_num", QBIT_NUMS_SMALL + [4])
296  """float32 apply_from_right result should match float64 reference."""
297  circ = _build_rx_rz_cnot_circuit(qbit_num)
298  p64 = _params64(circ.get_Parameter_Num())
299  p32 = p64.astype(np.float32)
300 
301  dim = 2 ** qbit_num
302  rng = np.random.default_rng(9)
303  A64 = np.ascontiguousarray(
304  rng.standard_normal((dim, dim)) + 1j * rng.standard_normal((dim, dim)),
305  dtype=np.complex128
306  )
307  A32 = A64.astype(np.complex64).copy(order='C')
308 
309  ref = A64.copy()
310  circ.apply_from_right(p64, ref)
311 
312  circ.apply_from_right(p32, A32, is_f32=True)
313 
314  _assert_unitary_close(ref, A32.astype(np.complex128), rtol=1e-4)
315 
316 
318  """4-qubit rectangular matrix coverage for all apply* methods.
319 
320  For apply_to routes, the matrix shape is 16xN (N=1..32).
321  For apply_from_right routes, the matrix shape is Nx16 (N=1..32), since
322  the right-multiply path requires the number of columns to be 16.
323  """
324 
325  def _build_circuit_q4(self):
326  circ = Circuit(4)
327  circ.add_RX(0)
328  circ.add_RZ(0)
329  circ.add_CNOT(0, 1)
330  circ.add_RY(1)
331  return circ
332 
333  @staticmethod
334  def _max_subset_err_apply_to(circ, params, full_input, is_f32=False):
335  out_full = np.ascontiguousarray(full_input.copy())
336  if is_f32:
337  circ.apply_to(params, out_full, is_f32=True)
338  else:
339  circ.apply_to(params, out_full)
340 
341  errs = []
342  for ncols in range(1, RECT_COLS_MAX + 1):
343  part = np.ascontiguousarray(full_input[:, :ncols].copy())
344  if is_f32:
345  circ.apply_to(params, part, is_f32=True)
346  else:
347  circ.apply_to(params, part)
348  errs.append(np.linalg.norm(part - out_full[:, :ncols]))
349 
350  return float(max(errs))
351 
352  @staticmethod
353  def _max_subset_err_apply_from_right(circ, params, full_input, is_f32=False):
354  out_full = np.ascontiguousarray(full_input.copy())
355  if is_f32:
356  circ.apply_from_right(params, out_full, is_f32=True)
357  else:
358  circ.apply_from_right(params, out_full)
359 
360  errs = []
361  for nrows in range(1, RECT_COLS_MAX + 1):
362  part = np.ascontiguousarray(full_input[:nrows, :].copy())
363  if is_f32:
364  circ.apply_from_right(params, part, is_f32=True)
365  else:
366  circ.apply_from_right(params, part)
367  errs.append(np.linalg.norm(part - out_full[:nrows, :]))
368 
369  return float(max(errs))
370 
372  circ = self._build_circuit_q4()
373  params = _params64(circ.get_Parameter_Num())
374 
375  rng = np.random.default_rng(123)
376  full = np.ascontiguousarray(
377  rng.standard_normal((16, RECT_COLS_MAX))
378  + 1j * rng.standard_normal((16, RECT_COLS_MAX)),
379  dtype=np.complex128,
380  )
381 
382  max_err = self._max_subset_err_apply_to(circ, params, full, is_f32=False)
383  assert max_err < 1e-10, f"apply_to f64 16xN subset mismatch: {max_err:.3e}"
384 
386  circ = self._build_circuit_q4()
387  params = _params32(circ.get_Parameter_Num())
388 
389  rng = np.random.default_rng(123)
390  full = np.ascontiguousarray(
391  rng.standard_normal((16, RECT_COLS_MAX))
392  + 1j * rng.standard_normal((16, RECT_COLS_MAX)),
393  dtype=np.complex64,
394  )
395 
396  max_err = self._max_subset_err_apply_to(circ, params, full, is_f32=True)
397  assert max_err < 5e-5, f"apply_to f32 16xN subset mismatch: {max_err:.3e}"
398 
400  circ = self._build_circuit_q4()
401  params = _params64(circ.get_Parameter_Num())
402 
403  rng = np.random.default_rng(124)
404  full = np.ascontiguousarray(
405  rng.standard_normal((RECT_COLS_MAX, 16))
406  + 1j * rng.standard_normal((RECT_COLS_MAX, 16)),
407  dtype=np.complex128,
408  )
409 
410  max_err = self._max_subset_err_apply_from_right(circ, params, full, is_f32=False)
411  assert max_err < 1e-10, f"apply_from_right f64 Nx16 subset mismatch: {max_err:.3e}"
412 
414  circ = self._build_circuit_q4()
415  params = _params32(circ.get_Parameter_Num())
416 
417  rng = np.random.default_rng(124)
418  full = np.ascontiguousarray(
419  rng.standard_normal((RECT_COLS_MAX, 16))
420  + 1j * rng.standard_normal((RECT_COLS_MAX, 16)),
421  dtype=np.complex64,
422  )
423 
424  max_err = self._max_subset_err_apply_from_right(circ, params, full, is_f32=True)
425  assert max_err < 5e-5, f"apply_from_right f32 Nx16 subset mismatch: {max_err:.3e}"
426 
427 
429  """Coverage for explicit GENERAL_OPERATION matrix insertion."""
430 
431  def _build_circuit_q4(self):
432  circ = Circuit(4)
433  circ.add_RX(0)
434  circ.add_RZ(0)
435  circ.add_CNOT(0, 1)
436  circ.add_RY(1)
437  return circ
438 
440  qbit_num = 2
441  dim = 1 << qbit_num
442  U = _random_unitary(dim, seed=91)
443 
444  circ = Circuit(qbit_num)
445  circ.add_GENERAL(U)
446 
447  params = np.array([], dtype=np.float64)
448 
449  # get_Matrix
450  got = np.asarray(circ.get_Matrix(params), dtype=np.complex128)
451  _assert_unitary_close(U, got, rtol=1e-10)
452 
453  # apply_to
454  rng = np.random.default_rng(92)
455  A = np.ascontiguousarray(rng.standard_normal((dim, dim)) + 1j * rng.standard_normal((dim, dim)), dtype=np.complex128)
456  expected_left = U @ A
457  got_left = A.copy(order="C")
458  circ.apply_to(params, got_left)
459  assert np.allclose(expected_left, got_left, atol=1e-10)
460 
461  # apply_from_right
462  B = np.ascontiguousarray(rng.standard_normal((dim, dim)) + 1j * rng.standard_normal((dim, dim)), dtype=np.complex128)
463  expected_right = B @ U
464  got_right = B.copy(order="C")
465  circ.apply_from_right(params, got_right)
466  assert np.allclose(expected_right, got_right, atol=1e-10)
467 
468  # apply_to_list
469  M0 = np.ascontiguousarray(rng.standard_normal((dim, dim)) + 1j * rng.standard_normal((dim, dim)), dtype=np.complex128)
470  M1 = np.ascontiguousarray(rng.standard_normal((dim, 1)) + 1j * rng.standard_normal((dim, 1)), dtype=np.complex128)
471  inputs = [M0.copy(order="C"), M1.copy(order="C")]
472  circ.apply_to_list(inputs, params)
473  assert np.allclose(inputs[0], U @ M0, atol=1e-10)
474  assert np.allclose(inputs[1], U @ M1, atol=1e-10)
475 
476  # derivative is empty for zero-parameter GENERAL_OPERATION
477  deriv = circ.apply_derivate_to(params, _identity(dim))
478  assert isinstance(deriv, list)
479  assert len(deriv) == 0
480 
482  qbit_num = 2
483  dim = 1 << qbit_num
484  U64 = _random_unitary(dim, seed=193)
485  U32 = np.ascontiguousarray(U64.astype(np.complex64))
486 
487  circ = Circuit(qbit_num)
488  circ.add_GENERAL(U32, is_f32=True)
489 
490  params32 = np.array([], dtype=np.float32)
491 
492  # get_Matrix
493  got32 = np.asarray(circ.get_Matrix(params32, is_f32=True), dtype=np.complex64)
494  assert np.allclose(got32, U32, atol=5e-5)
495 
496  rng = np.random.default_rng(194)
497 
498  # apply_to
499  A32 = np.ascontiguousarray(
500  rng.standard_normal((dim, dim)) + 1j * rng.standard_normal((dim, dim)),
501  dtype=np.complex64,
502  )
503  expected_left = U32 @ A32
504  got_left = A32.copy(order="C")
505  circ.apply_to(params32, got_left, is_f32=True)
506  assert np.allclose(expected_left, got_left, atol=5e-5)
507 
508  # apply_from_right
509  B32 = np.ascontiguousarray(
510  rng.standard_normal((dim, dim)) + 1j * rng.standard_normal((dim, dim)),
511  dtype=np.complex64,
512  )
513  expected_right = B32 @ U32
514  got_right = B32.copy(order="C")
515  circ.apply_from_right(params32, got_right, is_f32=True)
516  assert np.allclose(expected_right, got_right, atol=5e-5)
517 
518  # apply_to_list
519  M0 = np.ascontiguousarray(
520  rng.standard_normal((dim, dim)) + 1j * rng.standard_normal((dim, dim)),
521  dtype=np.complex64,
522  )
523  M1 = np.ascontiguousarray(
524  rng.standard_normal((dim, 1)) + 1j * rng.standard_normal((dim, 1)),
525  dtype=np.complex64,
526  )
527  inputs = [M0.copy(order="C"), M1.copy(order="C")]
528  circ.apply_to_list(inputs, params32, is_f32=True)
529  assert np.allclose(inputs[0], U32 @ M0, atol=5e-5)
530  assert np.allclose(inputs[1], U32 @ M1, atol=5e-5)
531 
532  # derivative is empty for zero-parameter GENERAL_OPERATION
533  deriv = circ.apply_derivate_to(params32, _identity(dim, dtype=np.complex64), is_f32=True)
534  assert isinstance(deriv, list)
535  assert len(deriv) == 0
536 
538  circ = self._build_circuit_q4()
539  params = _params64(circ.get_Parameter_Num())
540 
541  rng = np.random.default_rng(125)
542  mats = [
543  np.ascontiguousarray(
544  rng.standard_normal((16, ncols)) + 1j * rng.standard_normal((16, ncols)),
545  dtype=np.complex128,
546  )
547  for ncols in range(1, RECT_COLS_MAX + 1)
548  ]
549  refs = [m.copy() for m in mats]
550 
551  circ.apply_to_list(mats, params)
552  for ref in refs:
553  circ.apply_to(params, ref)
554 
555  errs = [np.linalg.norm(got - expected) for got, expected in zip(mats, refs)]
556  assert max(errs) < 1e-10, f"apply_to_list f64 16xN mismatch: {max(errs):.3e}"
557 
559  circ = self._build_circuit_q4()
560  params = _params32(circ.get_Parameter_Num())
561 
562  rng = np.random.default_rng(125)
563  mats = [
564  np.ascontiguousarray(
565  rng.standard_normal((16, ncols)) + 1j * rng.standard_normal((16, ncols)),
566  dtype=np.complex64,
567  )
568  for ncols in range(1, RECT_COLS_MAX + 1)
569  ]
570  refs = [m.copy() for m in mats]
571 
572  circ.apply_to_list(mats, params, is_f32=True)
573  for ref in refs:
574  circ.apply_to(params, ref, is_f32=True)
575 
576  errs = [np.linalg.norm(got - expected) for got, expected in zip(mats, refs)]
577  assert max(errs) < 5e-5, f"apply_to_list f32 16xN mismatch: {max(errs):.3e}"
578 
579 
581  """Gate fusion via set_min_fusion: fused results must match unfused."""
582 
583  def _make_multi_gate_circuit(self, qbit_num):
584  """Build a circuit with enough gates to trigger fusion."""
585  c = Circuit(qbit_num)
586  for q in range(min(qbit_num, 3)):
587  c.add_RX(q)
588  c.add_RZ(q)
589  if qbit_num >= 2:
590  c.add_CNOT(0, 1)
591  c.add_RY(0)
592  if qbit_num >= 3:
593  c.add_CNOT(1, 2)
594  c.add_RZ(2)
595  return c
596 
597  @pytest.mark.parametrize("qbit_num,min_fusion", [
598  (qn, mf) for qn in [3, 4, 5, 6] for mf in [2, 3, 4, 5, 6] if mf <= qn
599  ])
600  def test_fusion_unitary_matches_unfused(self, qbit_num, min_fusion):
601  """Fused circuit unitary should match unfused for various min_fusion values."""
602 
603  circ_ref = self._make_multi_gate_circuit(qbit_num)
604  circ_fused = self._make_multi_gate_circuit(qbit_num)
605  circ_fused.set_min_fusion(min_fusion)
606 
607  params = _params64(circ_ref.get_Parameter_Num())
608  dim = 2 ** qbit_num
609 
610  ref = _identity(dim)
611  circ_ref.apply_to(params, ref)
612 
613  fused = _identity(dim)
614  circ_fused.apply_to(params, fused)
615 
616  _assert_unitary_close(ref, fused, rtol=1e-9)
617 
618  @pytest.mark.parametrize("qbit_num", [3, 4, 5, 6])
619  @pytest.mark.parametrize("min_fusion", [2, 3])
620  def test_fusion_state_vector_matches_unfused(self, qbit_num, min_fusion):
621  """Fused circuit applied to a state should match unfused result."""
622  if min_fusion > qbit_num:
623  pytest.skip("min_fusion > qbit_num — fusion won't activate")
624 
625  circ_ref = self._make_multi_gate_circuit(qbit_num)
626  circ_fused = self._make_multi_gate_circuit(qbit_num)
627  circ_fused.set_min_fusion(min_fusion)
628 
629  params = _params64(circ_ref.get_Parameter_Num())
630  dim = 2 ** qbit_num
631  state = _random_state(dim, dtype=np.complex128)
632 
633  sv_ref = state.reshape(dim, 1).copy()
634  circ_ref.apply_to(params, sv_ref)
635 
636  sv_fused = state.reshape(dim, 1).copy()
637  circ_fused.apply_to(params, sv_fused)
638 
639  _assert_state_close(sv_ref.reshape(-1), sv_fused.reshape(-1), rtol=1e-9)
640 
641  @pytest.mark.parametrize("min_fusion", [2, 3, 5])
643  """
644  With qbit_num=14 the fused path uses apply_large_kernel_to_input_AVX_TBB.
645  Compare against an unfused circuit to verify correctness.
646  """
647  qbit_num = 14
648  circ_ref = Circuit(qbit_num)
649  circ_fused = Circuit(qbit_num)
650 
651  # Only involve qubits 0..1 so fusion kernel sees size==2 involved qubits
652  for _ in range(4):
653  circ_ref.add_RX(0)
654  circ_ref.add_CNOT(0, 1)
655  circ_ref.add_RZ(1)
656  circ_fused.add_RX(0)
657  circ_fused.add_CNOT(0, 1)
658  circ_fused.add_RZ(1)
659 
660  circ_fused.set_min_fusion(min_fusion)
661 
662  params = _params64(circ_ref.get_Parameter_Num())
663  dim = 2 ** qbit_num
664  state = _random_state(dim, dtype=np.complex128, seed=100)
665 
666  sv_ref = state.reshape(dim, 1).copy()
667  circ_ref.apply_to(params, sv_ref)
668 
669  sv_fused = state.reshape(dim, 1).copy()
670  circ_fused.apply_to(params, sv_fused)
671 
672  _assert_state_close(sv_ref.reshape(-1), sv_fused.reshape(-1), rtol=1e-9)
673 
674 
676  """Nested circuits (add_Circuit): sub-circuit applied inside outer circuit."""
677 
678  @pytest.mark.parametrize("qbit_num", [2, 3, 4])
679  def test_nested_matches_flat(self, qbit_num):
680  """Outer circuit containing a sub-circuit matches flat equivalent."""
681  # Flat reference
682  flat = Circuit(qbit_num)
683  flat.add_RX(0)
684  flat.add_CNOT(0, min(1, qbit_num - 1))
685  flat.add_RZ(0)
686  flat.add_H(0)
687 
688  # Sub-circuit with first two gates
689  sub = Circuit(qbit_num)
690  sub.add_RX(0)
691  sub.add_CNOT(0, min(1, qbit_num - 1))
692 
693  # Outer circuit: sub + last two gates
694  outer = Circuit(qbit_num)
695  outer.add_Circuit(sub)
696  outer.add_RZ(0)
697  outer.add_H(0)
698 
699  params = _params64(flat.get_Parameter_Num())
700  assert flat.get_Parameter_Num() == outer.get_Parameter_Num(), (
701  "flat and nested circuits should have the same number of parameters"
702  )
703 
704  dim = 2 ** qbit_num
705  mat_flat = _identity(dim)
706  flat.apply_to(params, mat_flat)
707 
708  mat_nested = _identity(dim)
709  outer.apply_to(params, mat_nested)
710 
711  _assert_unitary_close(mat_flat, mat_nested)
712 
713  @pytest.mark.parametrize("qbit_num", [2, 3])
714  def test_nested_f32_matches_f64(self, qbit_num):
715  """float32 apply on nested circuit matches float64 reference."""
716  sub = Circuit(qbit_num)
717  sub.add_RX(0)
718  sub.add_CNOT(0, min(1, qbit_num - 1))
719 
720  outer = Circuit(qbit_num)
721  outer.add_Circuit(sub)
722  outer.add_RZ(0)
723 
724  p64 = _params64(outer.get_Parameter_Num())
725  p32 = p64.astype(np.float32)
726 
727  dim = 2 ** qbit_num
728  ref64 = _identity(dim)
729  outer.apply_to(p64, ref64)
730 
731  mat32 = _identity(dim, dtype=np.complex64)
732  outer.apply_to(p32, mat32, is_f32=True)
733 
734  _assert_unitary_close(ref64, mat32.astype(np.complex128), rtol=1e-4)
735 
736  @pytest.mark.parametrize("qbit_num", [2, 3, 4])
737  def test_doubly_nested_matches_flat(self, qbit_num):
738  """Two levels of nesting produce the same result as a flat circuit."""
739  flat = Circuit(qbit_num)
740  flat.add_H(0)
741  flat.add_RX(0)
742  flat.add_CNOT(0, min(1, qbit_num - 1))
743  flat.add_RZ(0)
744 
745  inner = Circuit(qbit_num)
746  inner.add_H(0)
747  inner.add_RX(0)
748 
749  middle = Circuit(qbit_num)
750  middle.add_Circuit(inner)
751  middle.add_CNOT(0, min(1, qbit_num - 1))
752 
753  outer = Circuit(qbit_num)
754  outer.add_Circuit(middle)
755  outer.add_RZ(0)
756 
757  params = _params64(flat.get_Parameter_Num())
758  assert flat.get_Parameter_Num() == outer.get_Parameter_Num()
759 
760  dim = 2 ** qbit_num
761  mat_flat = _identity(dim)
762  flat.apply_to(params, mat_flat)
763 
764  mat_outer = _identity(dim)
765  outer.apply_to(params, mat_outer)
766 
767  _assert_unitary_close(mat_flat, mat_outer)
768 
769 
771  """Miscellaneous circuit-level tests."""
772 
774  """get_Parameter_Num matches sum of individual gate parameter counts."""
775  circ = Circuit(3)
776  circ.add_RX(0) # 1 param
777  circ.add_RZ(1) # 1 param
778  circ.add_U3(2) # 3 params
779  circ.add_CNOT(0, 2) # 0 params
780  assert circ.get_Parameter_Num() == 5
781 
782  def test_get_qbit_num(self):
783  for n in [1, 2, 4, 8]:
784  c = Circuit(n)
785  assert c.get_Qbit_Num() == n
786 
788  """An empty circuit should act as identity on any matrix."""
789  for qbit_num in [1, 2, 3, 4]:
790  circ = Circuit(qbit_num)
791  dim = 2 ** qbit_num
792  mat = _identity(dim)
793  circ.apply_to(np.array([], dtype=np.float64), mat)
794  np.testing.assert_allclose(mat, np.eye(dim, dtype=np.complex128), atol=1e-12)
795 
797  """An empty circuit, float32 path, should act as identity."""
798  for qbit_num in [1, 2, 3]:
799  circ = Circuit(qbit_num)
800  dim = 2 ** qbit_num
801  mat = _identity(dim, dtype=np.complex64)
802  circ.apply_to(np.array([], dtype=np.float32), mat, is_f32=True)
803  np.testing.assert_allclose(mat, np.eye(dim, dtype=np.complex64), atol=1e-6)
804 
805  @pytest.mark.parametrize("qbit_num", [1, 2, 3, 4, 5, 6])
807  """get_Matrix and apply_to on identity give the same result for all qubit counts."""
808  circ = Circuit(qbit_num)
809  circ.add_RX(0)
810  circ.add_RZ(0)
811  if qbit_num >= 2:
812  circ.add_CNOT(0, 1)
813 
814  params = _params64(circ.get_Parameter_Num())
815  dim = 2 ** qbit_num
816 
817  ref = np.array(circ.get_Matrix(params))
818  mat = _identity(dim)
819  circ.apply_to(params, mat)
820 
821  _assert_unitary_close(ref, mat)
822 
824  """Non-contiguous parameter arrays should be handled correctly (no crash)."""
825  circ = Circuit(2)
826  circ.add_RX(0)
827  circ.add_RZ(1)
828 
829  params_full = np.array([0.1, 0.0, 0.2], dtype=np.float64)
830  params_strided = params_full[::2] # non-contiguous: values 0.1, 0.2
831  assert not params_strided.flags['C_CONTIGUOUS']
832 
833  mat = _identity(4)
834  circ.apply_to(params_strided, mat)
835  assert not np.allclose(mat, np.eye(4, dtype=np.complex128))
836 
837  @pytest.mark.parametrize("qbit_num", QBIT_NUMS_ALL)
838  def test_h_x_cnot_no_params_f64(self, qbit_num):
839  """Circuits with only parameter-free gates work with empty param arrays."""
840  circ = Circuit(qbit_num)
841  circ.add_H(0)
842  circ.add_X(0)
843  if qbit_num >= 2:
844  circ.add_CNOT(0, 1)
845 
846  params = np.array([], dtype=np.float64)
847  dim = 2 ** qbit_num
848  ref = np.array(circ.get_Matrix(params))
849  mat = _identity(dim)
850  circ.apply_to(params, mat)
851  _assert_unitary_close(ref, mat)
852 
853  @pytest.mark.parametrize("qbit_num", QBIT_NUMS_ALL)
854  def test_h_x_cnot_no_params_f32(self, qbit_num):
855  """Parameter-free circuit float32 path produces correct result."""
856  circ = Circuit(qbit_num)
857  circ.add_H(0)
858  circ.add_X(0)
859  if qbit_num >= 2:
860  circ.add_CNOT(0, 1)
861 
862  p64 = np.array([], dtype=np.float64)
863  p32 = np.array([], dtype=np.float32)
864  dim = 2 ** qbit_num
865 
866  ref = np.array(circ.get_Matrix(p64))
867  mat32 = _identity(dim, dtype=np.complex64)
868  circ.apply_to(p32, mat32, is_f32=True)
869 
870  _assert_unitary_close(ref, mat32.astype(np.complex128), rtol=1e-4)
871 
872 
874  """apply_to_list: applies circuit to a list of matrices, all in float64."""
875 
876  @pytest.mark.parametrize("qbit_num", [2, 3, 4])
878  """apply_to_list result must equal calling apply_to on each matrix separately."""
879  circ = _build_rx_rz_cnot_circuit(qbit_num)
880  params = _params64(circ.get_Parameter_Num())
881  dim = 2 ** qbit_num
882 
883  matrices_list = [_identity(dim) for _ in range(5)]
884  matrices_individual = [m.copy() for m in matrices_list]
885 
886  circ.apply_to_list(matrices_list, params)
887 
888  for m in matrices_individual:
889  circ.apply_to(params, m)
890 
891  for got, expected in zip(matrices_list, matrices_individual):
892  _assert_unitary_close(expected, got)
893 
894  @pytest.mark.parametrize("qbit_num", [2, 3])
896  """Single-element list should behave like apply_to."""
897  circ = _build_rx_rz_cnot_circuit(qbit_num)
898  params = _params64(circ.get_Parameter_Num())
899  dim = 2 ** qbit_num
900 
901  m_list = [_identity(dim)]
902  m_single = _identity(dim)
903 
904  circ.apply_to_list(m_list, params)
905  circ.apply_to(params, m_single)
906 
907  _assert_unitary_close(m_single, m_list[0])
908 
909  @pytest.mark.parametrize("qbit_num", [2, 3, 4])
910  def test_apply_to_list_state_vectors(self, qbit_num):
911  """apply_to_list works on state vector arrays (dim-column vectors)."""
912  circ = _build_rx_rz_cnot_circuit(qbit_num)
913  params = _params64(circ.get_Parameter_Num())
914  dim = 2 ** qbit_num
915 
916  states = [_random_state(dim, seed=i).reshape(dim, 1).copy()
917  for i in range(4)]
918  references = [s.copy() for s in states]
919 
920  circ.apply_to_list(states, params)
921  for ref in references:
922  circ.apply_to(params, ref)
923 
924  for got, expected in zip(states, references):
925  _assert_state_close(expected.reshape(-1), got.reshape(-1))
926 
928  """Empty list should not crash (no-op)."""
929  circ = Circuit(2)
930  circ.add_RX(0)
931  params = _params64(circ.get_Parameter_Num())
932  circ.apply_to_list([], params) # should not raise
933 
934  @pytest.mark.parametrize("qbit_num", [2, 3])
936  """Passing complex64 inputs to apply_to_list should raise a TypeError."""
937  circ = _build_rx_rz_cnot_circuit(qbit_num)
938  params = _params64(circ.get_Parameter_Num())
939  dim = 2 ** qbit_num
940  bad = [_identity(dim, dtype=np.complex64)]
941  with pytest.raises(Exception):
942  circ.apply_to_list(bad, params)
943 
944 
946  """apply_derivate_to: derivative of circuit w.r.t. each free parameter."""
947 
948  @pytest.mark.parametrize("qbit_num", [2, 3, 4])
950  """Number of returned derivative matrices must equal get_Parameter_Num()."""
951  circ = _build_rx_rz_cnot_circuit(qbit_num)
952  params = _params64(circ.get_Parameter_Num())
953  dim = 2 ** qbit_num
954 
955  mat = _identity(dim)
956  derivs = circ.apply_derivate_to(params, mat)
957 
958  assert len(derivs) == circ.get_Parameter_Num()
959 
960  @pytest.mark.parametrize("qbit_num", [2, 3, 4])
962  """Each derivative matrix must have the same shape as the input."""
963  circ = _build_rx_rz_cnot_circuit(qbit_num)
964  params = _params64(circ.get_Parameter_Num())
965  dim = 2 ** qbit_num
966 
967  mat = _identity(dim)
968  derivs = circ.apply_derivate_to(params, mat)
969 
970  for i, d in enumerate(derivs):
971  assert d.shape == (dim, dim), f"deriv[{i}] shape {d.shape} != ({dim},{dim})"
972 
973  @pytest.mark.parametrize("qbit_num", [2, 3])
975  """Derivative matrices must be complex128."""
976  circ = _build_rx_rz_cnot_circuit(qbit_num)
977  params = _params64(circ.get_Parameter_Num())
978  dim = 2 ** qbit_num
979 
980  mat = _identity(dim)
981  derivs = circ.apply_derivate_to(params, mat)
982 
983  for i, d in enumerate(derivs):
984  assert d.dtype == np.complex128, f"deriv[{i}] dtype {d.dtype} != complex128"
985 
986  @pytest.mark.parametrize("qbit_num", [2, 3])
988  """Analytic derivative must match finite-difference estimate."""
989  circ = _build_rx_rz_cnot_circuit(qbit_num)
990  n_params = circ.get_Parameter_Num()
991  params = _params64(n_params)
992  dim = 2 ** qbit_num
993  eps = 1e-5
994 
995  mat = _identity(dim)
996  derivs = circ.apply_derivate_to(params, mat)
997 
998  for k in range(n_params):
999  p_plus = params.copy(); p_plus[k] += eps
1000  p_minus = params.copy(); p_minus[k] -= eps
1001  U_plus = np.array(circ.get_Matrix(p_plus))
1002  U_minus = np.array(circ.get_Matrix(p_minus))
1003  fd = (U_plus - U_minus) / (2.0 * eps)
1004  err = np.linalg.norm(derivs[k] - fd)
1005  assert err < 1e-5, (
1006  f"Param {k}: analytic vs FD error = {err:.3e}"
1007  )
1008 
1009  @pytest.mark.parametrize("qbit_num", [2, 3])
1011  """All derivative matrices must be non-zero for a parametric circuit."""
1012  circ = _build_rx_rz_cnot_circuit(qbit_num)
1013  params = _params64(circ.get_Parameter_Num())
1014  dim = 2 ** qbit_num
1015 
1016  mat = _identity(dim)
1017  derivs = circ.apply_derivate_to(params, mat)
1018 
1019  for i, d in enumerate(derivs):
1020  assert np.linalg.norm(d) > 1e-10, f"deriv[{i}] is unexpectedly zero"
1021 
1023  """Parameter-free circuit should return an empty list of derivatives."""
1024  circ = Circuit(2)
1025  circ.add_H(0)
1026  circ.add_CNOT(0, 1)
1027  params = np.array([], dtype=np.float64)
1028  mat = _identity(4)
1029  derivs = circ.apply_derivate_to(params, mat)
1030  assert len(derivs) == 0
1031 
1033  """Passing float32 parameters to apply_derivate_to should raise."""
1034  circ = Circuit(2)
1035  circ.add_RX(0)
1036  params32 = np.array([0.3], dtype=np.float32)
1037  mat = _identity(4)
1038  with pytest.raises(Exception):
1039  circ.apply_derivate_to(params32, mat)
1040 
1041 
1043  """apply_to_combined: returns [forward_output, derivatives...]."""
1044 
1045  @pytest.mark.parametrize("qbit_num", [2, 3])
1047  circ = _build_rx_rz_cnot_circuit(qbit_num)
1048  params = _params64(circ.get_Parameter_Num())
1049  dim = 2 ** qbit_num
1050 
1051  mat = _identity(dim)
1052  combined = circ.apply_to_combined(params, mat)
1053 
1054  assert isinstance(combined, list)
1055  assert len(combined) == circ.get_Parameter_Num() + 1
1056 
1057  expected_forward = _identity(dim)
1058  circ.apply_to(params, expected_forward)
1059  _assert_unitary_close(expected_forward, np.asarray(combined[0]))
1060 
1061  expected_derivs = circ.apply_derivate_to(params, mat)
1062  assert len(expected_derivs) == len(combined) - 1
1063  for idx, d in enumerate(expected_derivs):
1064  _assert_unitary_close(d, np.asarray(combined[idx + 1]))
1065 
1066  @pytest.mark.parametrize("qbit_num", [2, 3])
1068  circ = _build_rx_rz_cnot_circuit(qbit_num)
1069  params32 = _params32(circ.get_Parameter_Num())
1070  dim = 2 ** qbit_num
1071 
1072  mat32 = _identity(dim, dtype=np.complex64)
1073  combined = circ.apply_to_combined(params32, mat32, is_f32=True)
1074 
1075  assert isinstance(combined, list)
1076  assert len(combined) == circ.get_Parameter_Num() + 1
1077  for arr in combined:
1078  assert np.asarray(arr).dtype == np.complex64
1079 
1080  expected_forward = _identity(dim, dtype=np.complex64)
1081  circ.apply_to(params32, expected_forward, is_f32=True)
1082  _assert_unitary_close(expected_forward.astype(np.complex128), np.asarray(combined[0]).astype(np.complex128), rtol=1e-4)
1083 
1084  expected_derivs = circ.apply_derivate_to(params32, mat32, is_f32=True)
1085  assert len(expected_derivs) == len(combined) - 1
1086  for idx, d in enumerate(expected_derivs):
1087  _assert_unitary_close(d.astype(np.complex128), np.asarray(combined[idx + 1]).astype(np.complex128), rtol=1e-4)
1088 
1090  circ = Circuit(2)
1091  circ.add_H(0)
1092  circ.add_CNOT(0, 1)
1093  params = np.array([], dtype=np.float64)
1094 
1095  mat = _identity(4)
1096  combined = circ.apply_to_combined(params, mat)
1097 
1098  assert len(combined) == 1
1099  expected_forward = _identity(4)
1100  circ.apply_to(params, expected_forward)
1101  _assert_unitary_close(expected_forward, np.asarray(combined[0]))
1102 
1103 
1104 # ---------------------------------------------------------------------------
1105 # Float32 apply_to_list
1106 # ---------------------------------------------------------------------------
1107 
1109  """apply_to_list is_f32=True: float32/complex64 batch path."""
1110 
1111  @pytest.mark.parametrize("qbit_num", [2, 3, 4])
1112  def test_f32_matches_f64_reference(self, qbit_num):
1113  """f32 apply_to_list result must be close to f64 result."""
1114  circ = _build_rx_rz_cnot_circuit(qbit_num)
1115  p64 = _params64(circ.get_Parameter_Num())
1116  p32 = p64.astype(np.float32)
1117  dim = 2 ** qbit_num
1118 
1119  matrices_f64 = [_identity(dim) for _ in range(4)]
1120  matrices_f32 = [_identity(dim, dtype=np.complex64) for _ in range(4)]
1121 
1122  circ.apply_to_list(matrices_f64, p64)
1123  circ.apply_to_list(matrices_f32, p32, is_f32=True)
1124 
1125  for got, expected in zip(matrices_f32, matrices_f64):
1126  _assert_unitary_close(expected, got.astype(np.complex128), rtol=1e-4)
1127 
1128  @pytest.mark.parametrize("qbit_num", [2, 3])
1130  """Single-element f32 list should match apply_to with is_f32=True."""
1131  circ = _build_rx_rz_cnot_circuit(qbit_num)
1132  p32 = _params32(circ.get_Parameter_Num())
1133  dim = 2 ** qbit_num
1134 
1135  m_list = [_identity(dim, dtype=np.complex64)]
1136  m_single = _identity(dim, dtype=np.complex64)
1137 
1138  circ.apply_to_list(m_list, p32, is_f32=True)
1139  circ.apply_to(p32, m_single, is_f32=True)
1140 
1141  _assert_unitary_close(m_single.astype(np.complex128),
1142  m_list[0].astype(np.complex128), rtol=1e-4)
1143 
1144  @pytest.mark.parametrize("qbit_num", [2, 3, 4])
1145  def test_f32_dtype_preserved(self, qbit_num):
1146  """Output dtype should remain complex64 after f32 apply_to_list."""
1147  circ = _build_rx_rz_cnot_circuit(qbit_num)
1148  p32 = _params32(circ.get_Parameter_Num())
1149  dim = 2 ** qbit_num
1150 
1151  matrices = [_identity(dim, dtype=np.complex64) for _ in range(3)]
1152  circ.apply_to_list(matrices, p32, is_f32=True)
1153 
1154  for m in matrices:
1155  assert m.dtype == np.complex64
1156 
1158  """Empty f32 list should not crash."""
1159  circ = Circuit(2)
1160  circ.add_RX(0)
1161  p32 = _params32(circ.get_Parameter_Num())
1162  circ.apply_to_list([], p32, is_f32=True)
1163 
1164  @pytest.mark.parametrize("qbit_num", [2, 3])
1165  def test_f32_wrong_dtype_raises(self, qbit_num):
1166  """Passing complex128 inputs with is_f32=True should raise."""
1167  circ = _build_rx_rz_cnot_circuit(qbit_num)
1168  p32 = _params32(circ.get_Parameter_Num())
1169  dim = 2 ** qbit_num
1170  bad = [_identity(dim, dtype=np.complex128)]
1171  with pytest.raises(Exception):
1172  circ.apply_to_list(bad, p32, is_f32=True)
1173 
1174 
1175 # ---------------------------------------------------------------------------
1176 # Float32 apply_derivate_to
1177 # ---------------------------------------------------------------------------
1178 
1180  """apply_derivate_to is_f32=True: float32/complex64 derivative path."""
1181 
1182  @pytest.mark.parametrize("qbit_num", [2, 3, 4])
1184  """f32 derivative list length must equal get_Parameter_Num()."""
1185  circ = _build_rx_rz_cnot_circuit(qbit_num)
1186  p32 = _params32(circ.get_Parameter_Num())
1187  dim = 2 ** qbit_num
1188 
1189  mat = _identity(dim, dtype=np.complex64)
1190  derivs = circ.apply_derivate_to(p32, mat, is_f32=True)
1191 
1192  assert len(derivs) == circ.get_Parameter_Num()
1193 
1194  @pytest.mark.parametrize("qbit_num", [2, 3, 4])
1196  """Each f32 derivative matrix must have the same shape as the input."""
1197  circ = _build_rx_rz_cnot_circuit(qbit_num)
1198  p32 = _params32(circ.get_Parameter_Num())
1199  dim = 2 ** qbit_num
1200 
1201  mat = _identity(dim, dtype=np.complex64)
1202  derivs = circ.apply_derivate_to(p32, mat, is_f32=True)
1203 
1204  for i, d in enumerate(derivs):
1205  assert d.shape == (dim, dim), f"deriv[{i}] shape {d.shape} != ({dim},{dim})"
1206 
1207  @pytest.mark.parametrize("qbit_num", [2, 3])
1209  """f32 derivative matrices must be complex64."""
1210  circ = _build_rx_rz_cnot_circuit(qbit_num)
1211  p32 = _params32(circ.get_Parameter_Num())
1212  dim = 2 ** qbit_num
1213 
1214  mat = _identity(dim, dtype=np.complex64)
1215  derivs = circ.apply_derivate_to(p32, mat, is_f32=True)
1216 
1217  for i, d in enumerate(derivs):
1218  assert d.dtype == np.complex64, f"deriv[{i}] dtype {d.dtype} != complex64"
1219 
1220  @pytest.mark.parametrize("qbit_num", [2, 3])
1222  """f32 derivatives must be close to f64 reference derivatives."""
1223  circ = _build_rx_rz_cnot_circuit(qbit_num)
1224  p64 = _params64(circ.get_Parameter_Num())
1225  p32 = p64.astype(np.float32)
1226  dim = 2 ** qbit_num
1227 
1228  mat64 = _identity(dim)
1229  mat32 = _identity(dim, dtype=np.complex64)
1230 
1231  derivs64 = circ.apply_derivate_to(p64, mat64)
1232  derivs32 = circ.apply_derivate_to(p32, mat32, is_f32=True)
1233 
1234  assert len(derivs32) == len(derivs64)
1235  for i, (d32, d64) in enumerate(zip(derivs32, derivs64)):
1236  err = np.linalg.norm(d32.astype(np.complex128) - d64)
1237  assert err < 1e-3, f"deriv[{i}]: f32 vs f64 error = {err:.3e}"
1238 
1239  @pytest.mark.parametrize("qbit_num", [2, 3])
1241  """All f32 derivative matrices must be non-zero for a parametric circuit."""
1242  circ = _build_rx_rz_cnot_circuit(qbit_num)
1243  p32 = _params32(circ.get_Parameter_Num())
1244  dim = 2 ** qbit_num
1245 
1246  mat = _identity(dim, dtype=np.complex64)
1247  derivs = circ.apply_derivate_to(p32, mat, is_f32=True)
1248 
1249  for i, d in enumerate(derivs):
1250  assert np.linalg.norm(d) > 1e-7, f"f32 deriv[{i}] is unexpectedly zero"
1251 
1253  """f32 parameter-free circuit should return empty derivative list."""
1254  circ = Circuit(2)
1255  circ.add_H(0)
1256  circ.add_CNOT(0, 1)
1257  params = np.array([], dtype=np.float32)
1258  mat = _identity(4, dtype=np.complex64)
1259  derivs = circ.apply_derivate_to(params, mat, is_f32=True)
1260  assert len(derivs) == 0
1261 
1263  """Passing float64 params with is_f32=True should raise."""
1264  circ = Circuit(2)
1265  circ.add_RX(0)
1266  params64 = np.array([0.3], dtype=np.float64)
1267  mat = _identity(4, dtype=np.complex64)
1268  with pytest.raises(Exception):
1269  circ.apply_derivate_to(params64, mat, is_f32=True)
1270 
1272  """Passing complex128 matrix with is_f32=True should raise."""
1273  circ = Circuit(2)
1274  circ.add_RX(0)
1275  params32 = np.array([0.3], dtype=np.float32)
1276  mat64 = _identity(4, dtype=np.complex128)
1277  with pytest.raises(Exception):
1278  circ.apply_derivate_to(params32, mat64, is_f32=True)
def test_apply_to_list_wrong_dtype_raises(self, qbit_num)
def test_apply_to_f32_state_vector(self, qbit_num)
def test_apply_to_list_single_element(self, qbit_num)
def _build_rx_rz_cnot_circuit(qbit_num)
Definition: test_circuit.py:56
def test_combined_matches_apply_and_derivative_f64(self, qbit_num)
def _ref_matrix64(circuit)
Definition: test_circuit.py:67
def test_derivative_nonzero_for_parametric_circuit(self, qbit_num)
def test_derivative_finite_difference_validation(self, qbit_num)
def test_apply_from_right_f32_close_to_f64(self, qbit_num)
def _max_subset_err_apply_to(circ, params, full_input, is_f32=False)
def test_derivative_count_matches_parameter_num(self, qbit_num)
def test_apply_to_f32_unitary_close_to_f64(self, qbit_num)
def _assert_unitary_close(a, b, rtol=1e-6)
Definition: test_circuit.py:73
def test_apply_to_list_matches_individual_apply_to(self, qbit_num)
def test_f32_derivative_close_to_f64_reference(self, qbit_num)
def test_f32_derivative_count_matches_parameter_num(self, qbit_num)
def test_f32_derivative_nonzero_for_parametric_circuit(self, qbit_num)
def test_derivative_dtype_is_complex128(self, qbit_num)
def test_apply_to_twice_is_squared_unitary(self, qbit_num)
def test_doubly_nested_matches_flat(self, qbit_num)
def test_combined_matches_apply_and_derivative_f32(self, qbit_num)
def test_f32_single_element_matches_apply_to_f32(self, qbit_num)
def test_apply_to_unitary_matches_get_matrix(self, qbit_num)
def test_apply_from_right_single_gate_noparams_f64(self, qbit_num)
def test_apply_from_right_single_gate_f64(self, qbit_num)
def test_get_matrix_matches_apply_to_identity(self, qbit_num)
def test_apply_from_right_mutates_matrix(self, qbit_num)
def test_h_x_cnot_no_params_f64(self, qbit_num)
def test_fusion_large_circuit_avx_tbb_path(self, min_fusion)
def test_fusion_unitary_matches_unfused(self, qbit_num, min_fusion)
def _random_unitary(dim, seed=123)
Definition: test_circuit.py:93
def test_h_x_cnot_no_params_f32(self, qbit_num)
def test_apply_to_list_state_vectors(self, qbit_num)
def _identity(n, dtype=np.complex128)
Definition: test_circuit.py:30
def test_apply_to_f32_dtype_preserved(self, qbit_num)
def _assert_state_close(a, b, rtol=1e-6)
Definition: test_circuit.py:83
def _make_multi_gate_circuit(self, qbit_num)
def _random_state(n, dtype=np.complex128, seed=42)
Definition: test_circuit.py:34
def test_fusion_state_vector_matches_unfused(self, qbit_num, min_fusion)
def test_derivative_shapes_match_input(self, qbit_num)
def _max_subset_err_apply_from_right(circ, params, full_input, is_f32=False)