2 Copyright 2020 Peter Rakyta, Ph.D. 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 8 http://www.apache.org/licenses/LICENSE-2.0 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. 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). 31 return np.eye(n, dtype=dtype, order=
'C')
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)
39 return np.ascontiguousarray(v / np.linalg.norm(v))
43 """Float64 parameter array of length n.""" 45 return np.array([], dtype=np.float64)
46 return np.linspace(0.1, 0.1 * n, n, dtype=np.float64)
50 """Float32 parameter array of length n.""" 52 return np.array([], dtype=np.float32)
53 return np.linspace(0.1, 0.1 * n, n, dtype=np.float32)
57 """Build RX(0) â RZ(0) â CNOT(0,1) circuit if qbit_num >= 2, else RX â RZ.""" 68 """Compute reference unitary via get_Matrix (float64).""" 69 params =
_params64(circuit.get_Parameter_Num())
70 return np.array(circuit.get_Matrix(params))
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))
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}" 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)
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}" 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)
99 phase = np.where(np.abs(d) > 0, d / np.abs(d), 1.0)
101 return np.ascontiguousarray(q, dtype=np.complex128)
110 QBIT_NUMS_SMALL = [1, 2, 3]
111 QBIT_NUMS_MEDIUM = [4, 5, 6]
112 QBIT_NUMS_ALL = QBIT_NUMS_SMALL + QBIT_NUMS_MEDIUM
117 """Apply_to float64: unitary and state-vector correctness.""" 119 @pytest.mark.parametrize(
"qbit_num", QBIT_NUMS_ALL)
121 """apply_to on identity should reproduce get_Matrix output.""" 123 params =
_params64(circ.get_Parameter_Num())
124 ref = np.array(circ.get_Matrix(params))
128 circ.apply_to(params, identity)
132 @pytest.mark.parametrize(
"qbit_num", QBIT_NUMS_ALL)
134 """apply_to on a state vector should give U @ |state>.""" 136 params =
_params64(circ.get_Parameter_Num())
137 U = np.array(circ.get_Matrix(params))
144 circ.apply_to(params, sv.reshape(dim, 1))
145 result = sv.reshape(-1)
149 @pytest.mark.parametrize(
"qbit_num", QBIT_NUMS_ALL)
151 """Applying circuit twice equals U^2 applied to identity.""" 153 params =
_params64(circ.get_Parameter_Num())
154 U = np.array(circ.get_Matrix(params))
158 circ.apply_to(params, mtx)
159 circ.apply_to(params, mtx)
166 """Apply_to float32: complex64 precision correctness against float64 reference.""" 168 @pytest.mark.parametrize(
"qbit_num", QBIT_NUMS_ALL)
170 """float32 apply_to result should be close to float64 result.""" 172 p64 =
_params64(circ.get_Parameter_Num())
173 p32 = p64.astype(np.float32)
178 ref64 =
_identity(dim, dtype=np.complex128)
179 circ.apply_to(p64, ref64)
182 mat32 =
_identity(dim, dtype=np.complex64)
183 circ.apply_to(p32, mat32, is_f32=
True)
187 @pytest.mark.parametrize(
"qbit_num", QBIT_NUMS_ALL)
189 """float32 state vector apply matches float64 reference.""" 191 p64 =
_params64(circ.get_Parameter_Num())
192 p32 = p64.astype(np.float32)
196 state32 = state64.astype(np.complex64)
198 sv64 = state64.reshape(dim, 1).copy()
199 circ.apply_to(p64, sv64)
200 result64 = sv64.reshape(-1)
202 sv32 = state32.reshape(dim, 1).copy()
203 circ.apply_to(p32, sv32, is_f32=
True)
204 result32 = sv32.reshape(-1)
208 @pytest.mark.parametrize(
"qbit_num", QBIT_NUMS_ALL)
210 """Output dtype should remain complex64 after float32 apply_to.""" 212 p32 =
_params32(circ.get_Parameter_Num())
216 circ.apply_to(p32, mat, is_f32=
True)
218 assert mat.dtype == np.complex64, f
"expected complex64, got {mat.dtype}" 221 """Passing complex128 with is_f32=True should raise.""" 226 with pytest.raises(Exception):
227 circ.apply_to(params, mat, is_f32=
True)
230 """Passing complex64 with is_f32=False (default) should raise.""" 235 with pytest.raises(Exception):
236 circ.apply_to(params, mat, is_f32=
False)
240 """apply_from_right: both f64 and f32. 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. 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)
255 params =
_params64(circ.get_Parameter_Num())
256 U = np.array(circ.get_Matrix(params))
260 circ.apply_from_right(params, mat)
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)
270 params = np.array([], dtype=np.float64)
271 U = np.array(circ.get_Matrix(params))
275 circ.apply_from_right(params, mat)
279 @pytest.mark.parametrize(
"qbit_num", QBIT_NUMS_SMALL + [4])
281 """apply_from_right must modify the input matrix in-place.""" 283 params =
_params64(circ.get_Parameter_Num())
287 circ.apply_from_right(params, mat)
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" 294 @pytest.mark.parametrize(
"qbit_num", QBIT_NUMS_SMALL + [4])
296 """float32 apply_from_right result should match float64 reference.""" 298 p64 =
_params64(circ.get_Parameter_Num())
299 p32 = p64.astype(np.float32)
302 rng = np.random.default_rng(9)
303 A64 = np.ascontiguousarray(
304 rng.standard_normal((dim, dim)) + 1j * rng.standard_normal((dim, dim)),
307 A32 = A64.astype(np.complex64).copy(order=
'C')
310 circ.apply_from_right(p64, ref)
312 circ.apply_from_right(p32, A32, is_f32=
True)
318 """4-qubit rectangular matrix coverage for all apply* methods. 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. 335 out_full = np.ascontiguousarray(full_input.copy())
337 circ.apply_to(params, out_full, is_f32=
True)
339 circ.apply_to(params, out_full)
342 for ncols
in range(1, RECT_COLS_MAX + 1):
343 part = np.ascontiguousarray(full_input[:, :ncols].copy())
345 circ.apply_to(params, part, is_f32=
True)
347 circ.apply_to(params, part)
348 errs.append(np.linalg.norm(part - out_full[:, :ncols]))
350 return float(max(errs))
354 out_full = np.ascontiguousarray(full_input.copy())
356 circ.apply_from_right(params, out_full, is_f32=
True)
358 circ.apply_from_right(params, out_full)
361 for nrows
in range(1, RECT_COLS_MAX + 1):
362 part = np.ascontiguousarray(full_input[:nrows, :].copy())
364 circ.apply_from_right(params, part, is_f32=
True)
366 circ.apply_from_right(params, part)
367 errs.append(np.linalg.norm(part - out_full[:nrows, :]))
369 return float(max(errs))
373 params =
_params64(circ.get_Parameter_Num())
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)),
383 assert max_err < 1e-10, f
"apply_to f64 16xN subset mismatch: {max_err:.3e}" 387 params =
_params32(circ.get_Parameter_Num())
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)),
397 assert max_err < 5e-5, f
"apply_to f32 16xN subset mismatch: {max_err:.3e}" 401 params =
_params64(circ.get_Parameter_Num())
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)),
411 assert max_err < 1e-10, f
"apply_from_right f64 Nx16 subset mismatch: {max_err:.3e}" 415 params =
_params32(circ.get_Parameter_Num())
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)),
425 assert max_err < 5e-5, f
"apply_from_right f32 Nx16 subset mismatch: {max_err:.3e}" 429 """Coverage for explicit GENERAL_OPERATION matrix insertion.""" 444 circ = Circuit(qbit_num)
447 params = np.array([], dtype=np.float64)
450 got = np.asarray(circ.get_Matrix(params), dtype=np.complex128)
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)
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)
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)
477 deriv = circ.apply_derivate_to(params,
_identity(dim))
478 assert isinstance(deriv, list)
479 assert len(deriv) == 0
485 U32 = np.ascontiguousarray(U64.astype(np.complex64))
487 circ = Circuit(qbit_num)
488 circ.add_GENERAL(U32, is_f32=
True)
490 params32 = np.array([], dtype=np.float32)
493 got32 = np.asarray(circ.get_Matrix(params32, is_f32=
True), dtype=np.complex64)
494 assert np.allclose(got32, U32, atol=5e-5)
496 rng = np.random.default_rng(194)
499 A32 = np.ascontiguousarray(
500 rng.standard_normal((dim, dim)) + 1j * rng.standard_normal((dim, dim)),
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)
509 B32 = np.ascontiguousarray(
510 rng.standard_normal((dim, dim)) + 1j * rng.standard_normal((dim, dim)),
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)
519 M0 = np.ascontiguousarray(
520 rng.standard_normal((dim, dim)) + 1j * rng.standard_normal((dim, dim)),
523 M1 = np.ascontiguousarray(
524 rng.standard_normal((dim, 1)) + 1j * rng.standard_normal((dim, 1)),
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)
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
539 params =
_params64(circ.get_Parameter_Num())
541 rng = np.random.default_rng(125)
543 np.ascontiguousarray(
544 rng.standard_normal((16, ncols)) + 1j * rng.standard_normal((16, ncols)),
547 for ncols
in range(1, RECT_COLS_MAX + 1)
549 refs = [m.copy()
for m
in mats]
551 circ.apply_to_list(mats, params)
553 circ.apply_to(params, ref)
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}" 560 params =
_params32(circ.get_Parameter_Num())
562 rng = np.random.default_rng(125)
564 np.ascontiguousarray(
565 rng.standard_normal((16, ncols)) + 1j * rng.standard_normal((16, ncols)),
568 for ncols
in range(1, RECT_COLS_MAX + 1)
570 refs = [m.copy()
for m
in mats]
572 circ.apply_to_list(mats, params, is_f32=
True)
574 circ.apply_to(params, ref, is_f32=
True)
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}" 581 """Gate fusion via set_min_fusion: fused results must match unfused.""" 584 """Build a circuit with enough gates to trigger fusion.""" 585 c = Circuit(qbit_num)
586 for q
in range(min(qbit_num, 3)):
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
601 """Fused circuit unitary should match unfused for various min_fusion values.""" 605 circ_fused.set_min_fusion(min_fusion)
607 params =
_params64(circ_ref.get_Parameter_Num())
611 circ_ref.apply_to(params, ref)
614 circ_fused.apply_to(params, fused)
618 @pytest.mark.parametrize(
"qbit_num", [3, 4, 5, 6])
619 @pytest.mark.parametrize(
"min_fusion", [2, 3])
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")
627 circ_fused.set_min_fusion(min_fusion)
629 params =
_params64(circ_ref.get_Parameter_Num())
633 sv_ref = state.reshape(dim, 1).copy()
634 circ_ref.apply_to(params, sv_ref)
636 sv_fused = state.reshape(dim, 1).copy()
637 circ_fused.apply_to(params, sv_fused)
641 @pytest.mark.parametrize(
"min_fusion", [2, 3, 5])
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. 648 circ_ref = Circuit(qbit_num)
649 circ_fused = Circuit(qbit_num)
654 circ_ref.add_CNOT(0, 1)
657 circ_fused.add_CNOT(0, 1)
660 circ_fused.set_min_fusion(min_fusion)
662 params =
_params64(circ_ref.get_Parameter_Num())
666 sv_ref = state.reshape(dim, 1).copy()
667 circ_ref.apply_to(params, sv_ref)
669 sv_fused = state.reshape(dim, 1).copy()
670 circ_fused.apply_to(params, sv_fused)
676 """Nested circuits (add_Circuit): sub-circuit applied inside outer circuit.""" 678 @pytest.mark.parametrize(
"qbit_num", [2, 3, 4])
680 """Outer circuit containing a sub-circuit matches flat equivalent.""" 682 flat = Circuit(qbit_num)
684 flat.add_CNOT(0, min(1, qbit_num - 1))
689 sub = Circuit(qbit_num)
691 sub.add_CNOT(0, min(1, qbit_num - 1))
694 outer = Circuit(qbit_num)
695 outer.add_Circuit(sub)
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" 706 flat.apply_to(params, mat_flat)
709 outer.apply_to(params, mat_nested)
713 @pytest.mark.parametrize(
"qbit_num", [2, 3])
715 """float32 apply on nested circuit matches float64 reference.""" 716 sub = Circuit(qbit_num)
718 sub.add_CNOT(0, min(1, qbit_num - 1))
720 outer = Circuit(qbit_num)
721 outer.add_Circuit(sub)
724 p64 =
_params64(outer.get_Parameter_Num())
725 p32 = p64.astype(np.float32)
729 outer.apply_to(p64, ref64)
731 mat32 =
_identity(dim, dtype=np.complex64)
732 outer.apply_to(p32, mat32, is_f32=
True)
736 @pytest.mark.parametrize(
"qbit_num", [2, 3, 4])
738 """Two levels of nesting produce the same result as a flat circuit.""" 739 flat = Circuit(qbit_num)
742 flat.add_CNOT(0, min(1, qbit_num - 1))
745 inner = Circuit(qbit_num)
749 middle = Circuit(qbit_num)
750 middle.add_Circuit(inner)
751 middle.add_CNOT(0, min(1, qbit_num - 1))
753 outer = Circuit(qbit_num)
754 outer.add_Circuit(middle)
757 params =
_params64(flat.get_Parameter_Num())
758 assert flat.get_Parameter_Num() == outer.get_Parameter_Num()
762 flat.apply_to(params, mat_flat)
765 outer.apply_to(params, mat_outer)
771 """Miscellaneous circuit-level tests.""" 774 """get_Parameter_Num matches sum of individual gate parameter counts.""" 780 assert circ.get_Parameter_Num() == 5
783 for n
in [1, 2, 4, 8]:
785 assert c.get_Qbit_Num() == n
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)
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)
797 """An empty circuit, float32 path, should act as identity.""" 798 for qbit_num
in [1, 2, 3]:
799 circ = Circuit(qbit_num)
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)
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)
814 params =
_params64(circ.get_Parameter_Num())
817 ref = np.array(circ.get_Matrix(params))
819 circ.apply_to(params, mat)
824 """Non-contiguous parameter arrays should be handled correctly (no crash).""" 829 params_full = np.array([0.1, 0.0, 0.2], dtype=np.float64)
830 params_strided = params_full[::2]
831 assert not params_strided.flags[
'C_CONTIGUOUS']
834 circ.apply_to(params_strided, mat)
835 assert not np.allclose(mat, np.eye(4, dtype=np.complex128))
837 @pytest.mark.parametrize(
"qbit_num", QBIT_NUMS_ALL)
839 """Circuits with only parameter-free gates work with empty param arrays.""" 840 circ = Circuit(qbit_num)
846 params = np.array([], dtype=np.float64)
848 ref = np.array(circ.get_Matrix(params))
850 circ.apply_to(params, mat)
853 @pytest.mark.parametrize(
"qbit_num", QBIT_NUMS_ALL)
855 """Parameter-free circuit float32 path produces correct result.""" 856 circ = Circuit(qbit_num)
862 p64 = np.array([], dtype=np.float64)
863 p32 = np.array([], dtype=np.float32)
866 ref = np.array(circ.get_Matrix(p64))
867 mat32 =
_identity(dim, dtype=np.complex64)
868 circ.apply_to(p32, mat32, is_f32=
True)
874 """apply_to_list: applies circuit to a list of matrices, all in float64.""" 876 @pytest.mark.parametrize(
"qbit_num", [2, 3, 4])
878 """apply_to_list result must equal calling apply_to on each matrix separately.""" 880 params =
_params64(circ.get_Parameter_Num())
883 matrices_list = [
_identity(dim)
for _
in range(5)]
884 matrices_individual = [m.copy()
for m
in matrices_list]
886 circ.apply_to_list(matrices_list, params)
888 for m
in matrices_individual:
889 circ.apply_to(params, m)
891 for got, expected
in zip(matrices_list, matrices_individual):
894 @pytest.mark.parametrize(
"qbit_num", [2, 3])
896 """Single-element list should behave like apply_to.""" 898 params =
_params64(circ.get_Parameter_Num())
904 circ.apply_to_list(m_list, params)
905 circ.apply_to(params, m_single)
909 @pytest.mark.parametrize(
"qbit_num", [2, 3, 4])
911 """apply_to_list works on state vector arrays (dim-column vectors).""" 913 params =
_params64(circ.get_Parameter_Num())
918 references = [s.copy()
for s
in states]
920 circ.apply_to_list(states, params)
921 for ref
in references:
922 circ.apply_to(params, ref)
924 for got, expected
in zip(states, references):
928 """Empty list should not crash (no-op).""" 931 params =
_params64(circ.get_Parameter_Num())
932 circ.apply_to_list([], params)
934 @pytest.mark.parametrize(
"qbit_num", [2, 3])
936 """Passing complex64 inputs to apply_to_list should raise a TypeError.""" 938 params =
_params64(circ.get_Parameter_Num())
940 bad = [
_identity(dim, dtype=np.complex64)]
941 with pytest.raises(Exception):
942 circ.apply_to_list(bad, params)
946 """apply_derivate_to: derivative of circuit w.r.t. each free parameter.""" 948 @pytest.mark.parametrize(
"qbit_num", [2, 3, 4])
950 """Number of returned derivative matrices must equal get_Parameter_Num().""" 952 params =
_params64(circ.get_Parameter_Num())
956 derivs = circ.apply_derivate_to(params, mat)
958 assert len(derivs) == circ.get_Parameter_Num()
960 @pytest.mark.parametrize(
"qbit_num", [2, 3, 4])
962 """Each derivative matrix must have the same shape as the input.""" 964 params =
_params64(circ.get_Parameter_Num())
968 derivs = circ.apply_derivate_to(params, mat)
970 for i, d
in enumerate(derivs):
971 assert d.shape == (dim, dim), f
"deriv[{i}] shape {d.shape} != ({dim},{dim})" 973 @pytest.mark.parametrize(
"qbit_num", [2, 3])
975 """Derivative matrices must be complex128.""" 977 params =
_params64(circ.get_Parameter_Num())
981 derivs = circ.apply_derivate_to(params, mat)
983 for i, d
in enumerate(derivs):
984 assert d.dtype == np.complex128, f
"deriv[{i}] dtype {d.dtype} != complex128" 986 @pytest.mark.parametrize(
"qbit_num", [2, 3])
988 """Analytic derivative must match finite-difference estimate.""" 990 n_params = circ.get_Parameter_Num()
996 derivs = circ.apply_derivate_to(params, mat)
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}" 1009 @pytest.mark.parametrize(
"qbit_num", [2, 3])
1011 """All derivative matrices must be non-zero for a parametric circuit.""" 1013 params =
_params64(circ.get_Parameter_Num())
1017 derivs = circ.apply_derivate_to(params, mat)
1019 for i, d
in enumerate(derivs):
1020 assert np.linalg.norm(d) > 1e-10, f
"deriv[{i}] is unexpectedly zero" 1023 """Parameter-free circuit should return an empty list of derivatives.""" 1027 params = np.array([], dtype=np.float64)
1029 derivs = circ.apply_derivate_to(params, mat)
1030 assert len(derivs) == 0
1033 """Passing float32 parameters to apply_derivate_to should raise.""" 1036 params32 = np.array([0.3], dtype=np.float32)
1038 with pytest.raises(Exception):
1039 circ.apply_derivate_to(params32, mat)
1043 """apply_to_combined: returns [forward_output, derivatives...].""" 1045 @pytest.mark.parametrize(
"qbit_num", [2, 3])
1048 params =
_params64(circ.get_Parameter_Num())
1052 combined = circ.apply_to_combined(params, mat)
1054 assert isinstance(combined, list)
1055 assert len(combined) == circ.get_Parameter_Num() + 1
1058 circ.apply_to(params, expected_forward)
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):
1066 @pytest.mark.parametrize(
"qbit_num", [2, 3])
1069 params32 =
_params32(circ.get_Parameter_Num())
1072 mat32 =
_identity(dim, dtype=np.complex64)
1073 combined = circ.apply_to_combined(params32, mat32, is_f32=
True)
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
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)
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)
1093 params = np.array([], dtype=np.float64)
1096 combined = circ.apply_to_combined(params, mat)
1098 assert len(combined) == 1
1100 circ.apply_to(params, expected_forward)
1109 """apply_to_list is_f32=True: float32/complex64 batch path.""" 1111 @pytest.mark.parametrize(
"qbit_num", [2, 3, 4])
1113 """f32 apply_to_list result must be close to f64 result.""" 1115 p64 =
_params64(circ.get_Parameter_Num())
1116 p32 = p64.astype(np.float32)
1119 matrices_f64 = [
_identity(dim)
for _
in range(4)]
1120 matrices_f32 = [
_identity(dim, dtype=np.complex64)
for _
in range(4)]
1122 circ.apply_to_list(matrices_f64, p64)
1123 circ.apply_to_list(matrices_f32, p32, is_f32=
True)
1125 for got, expected
in zip(matrices_f32, matrices_f64):
1128 @pytest.mark.parametrize(
"qbit_num", [2, 3])
1130 """Single-element f32 list should match apply_to with is_f32=True.""" 1132 p32 =
_params32(circ.get_Parameter_Num())
1135 m_list = [
_identity(dim, dtype=np.complex64)]
1136 m_single =
_identity(dim, dtype=np.complex64)
1138 circ.apply_to_list(m_list, p32, is_f32=
True)
1139 circ.apply_to(p32, m_single, is_f32=
True)
1142 m_list[0].astype(np.complex128), rtol=1e-4)
1144 @pytest.mark.parametrize(
"qbit_num", [2, 3, 4])
1146 """Output dtype should remain complex64 after f32 apply_to_list.""" 1148 p32 =
_params32(circ.get_Parameter_Num())
1151 matrices = [
_identity(dim, dtype=np.complex64)
for _
in range(3)]
1152 circ.apply_to_list(matrices, p32, is_f32=
True)
1155 assert m.dtype == np.complex64
1158 """Empty f32 list should not crash.""" 1161 p32 =
_params32(circ.get_Parameter_Num())
1162 circ.apply_to_list([], p32, is_f32=
True)
1164 @pytest.mark.parametrize(
"qbit_num", [2, 3])
1166 """Passing complex128 inputs with is_f32=True should raise.""" 1168 p32 =
_params32(circ.get_Parameter_Num())
1170 bad = [
_identity(dim, dtype=np.complex128)]
1171 with pytest.raises(Exception):
1172 circ.apply_to_list(bad, p32, is_f32=
True)
1180 """apply_derivate_to is_f32=True: float32/complex64 derivative path.""" 1182 @pytest.mark.parametrize(
"qbit_num", [2, 3, 4])
1184 """f32 derivative list length must equal get_Parameter_Num().""" 1186 p32 =
_params32(circ.get_Parameter_Num())
1189 mat =
_identity(dim, dtype=np.complex64)
1190 derivs = circ.apply_derivate_to(p32, mat, is_f32=
True)
1192 assert len(derivs) == circ.get_Parameter_Num()
1194 @pytest.mark.parametrize(
"qbit_num", [2, 3, 4])
1196 """Each f32 derivative matrix must have the same shape as the input.""" 1198 p32 =
_params32(circ.get_Parameter_Num())
1201 mat =
_identity(dim, dtype=np.complex64)
1202 derivs = circ.apply_derivate_to(p32, mat, is_f32=
True)
1204 for i, d
in enumerate(derivs):
1205 assert d.shape == (dim, dim), f
"deriv[{i}] shape {d.shape} != ({dim},{dim})" 1207 @pytest.mark.parametrize(
"qbit_num", [2, 3])
1209 """f32 derivative matrices must be complex64.""" 1211 p32 =
_params32(circ.get_Parameter_Num())
1214 mat =
_identity(dim, dtype=np.complex64)
1215 derivs = circ.apply_derivate_to(p32, mat, is_f32=
True)
1217 for i, d
in enumerate(derivs):
1218 assert d.dtype == np.complex64, f
"deriv[{i}] dtype {d.dtype} != complex64" 1220 @pytest.mark.parametrize(
"qbit_num", [2, 3])
1222 """f32 derivatives must be close to f64 reference derivatives.""" 1224 p64 =
_params64(circ.get_Parameter_Num())
1225 p32 = p64.astype(np.float32)
1229 mat32 =
_identity(dim, dtype=np.complex64)
1231 derivs64 = circ.apply_derivate_to(p64, mat64)
1232 derivs32 = circ.apply_derivate_to(p32, mat32, is_f32=
True)
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}" 1239 @pytest.mark.parametrize(
"qbit_num", [2, 3])
1241 """All f32 derivative matrices must be non-zero for a parametric circuit.""" 1243 p32 =
_params32(circ.get_Parameter_Num())
1246 mat =
_identity(dim, dtype=np.complex64)
1247 derivs = circ.apply_derivate_to(p32, mat, is_f32=
True)
1249 for i, d
in enumerate(derivs):
1250 assert np.linalg.norm(d) > 1e-7, f
"f32 deriv[{i}] is unexpectedly zero" 1253 """f32 parameter-free circuit should return empty derivative list.""" 1257 params = np.array([], dtype=np.float32)
1259 derivs = circ.apply_derivate_to(params, mat, is_f32=
True)
1260 assert len(derivs) == 0
1263 """Passing float64 params with is_f32=True should raise.""" 1266 params64 = np.array([0.3], dtype=np.float64)
1268 with pytest.raises(Exception):
1269 circ.apply_derivate_to(params64, mat, is_f32=
True)
1272 """Passing complex128 matrix with is_f32=True should raise.""" 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_f32_derivative_shapes_match_input(self, qbit_num)
def test_apply_to_list_wrong_dtype_raises(self, qbit_num)
def _build_circuit_q4(self)
def test_apply_to_f32_state_vector(self, qbit_num)
def test_general_operation_all_paths_f64(self)
def test_apply_to_list_single_element(self, qbit_num)
def test_parameter_num_consistent(self)
def test_nested_matches_flat(self, qbit_num)
def _build_rx_rz_cnot_circuit(qbit_num)
def test_combined_matches_apply_and_derivative_f64(self, qbit_num)
def test_apply_to_list_rectangular_sweep_f64(self)
def test_apply_to_f32_wrong_dtype_raises(self)
def test_empty_circuit_is_identity_f64(self)
def _ref_matrix64(circuit)
def test_apply_to_f64_wrong_dtype_raises(self)
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_nested_f32_matches_f64(self, qbit_num)
def test_derivative_count_matches_parameter_num(self, qbit_num)
def test_apply_to_f32_unitary_close_to_f64(self, qbit_num)
def test_general_operation_all_paths_f32(self)
def _assert_unitary_close(a, b, rtol=1e-6)
def _build_circuit_q4(self)
def test_apply_to_list_matches_individual_apply_to(self, qbit_num)
def test_f32_empty_list_no_crash(self)
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_f32_wrong_param_dtype_raises(self)
def test_doubly_nested_matches_flat(self, qbit_num)
def test_f32_wrong_matrix_dtype_raises(self)
def test_combined_matches_apply_and_derivative_f32(self, qbit_num)
def test_f32_derivative_dtype_is_complex64(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_apply_from_right_rectangular_subset_consistency_f64(self)
def test_h_x_cnot_no_params_f64(self, qbit_num)
def test_apply_to_list_rectangular_sweep_f32(self)
def test_fusion_large_circuit_avx_tbb_path(self, min_fusion)
def test_f32_no_params_returns_empty_list(self)
def test_no_params_returns_empty_list(self)
def test_apply_to_list_empty_list_no_crash(self)
def test_apply_to_rectangular_subset_consistency_f64(self)
def test_fusion_unitary_matches_unfused(self, qbit_num, min_fusion)
def _random_unitary(dim, seed=123)
def test_h_x_cnot_no_params_f32(self, qbit_num)
def test_apply_to_list_state_vectors(self, qbit_num)
def test_wrong_param_dtype_raises(self)
def _identity(n, dtype=np.complex128)
def test_f32_matches_f64_reference(self, qbit_num)
def test_apply_to_f32_dtype_preserved(self, qbit_num)
def test_get_qbit_num(self)
def _assert_state_close(a, b, rtol=1e-6)
def test_apply_to_state_vector(self, qbit_num)
def _make_multi_gate_circuit(self, qbit_num)
def test_f32_dtype_preserved(self, qbit_num)
def test_combined_no_params_returns_forward_only(self)
def test_apply_to_rectangular_subset_consistency_f32(self)
def test_apply_from_right_rectangular_subset_consistency_f32(self)
def test_non_contiguous_params_handled(self)
def _random_state(n, dtype=np.complex128, seed=42)
def test_f32_wrong_dtype_raises(self, qbit_num)
def test_empty_circuit_is_identity_f32(self)
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)