Skip to content

Commit 54484bb

Browse files
daxfohlrht
authored andcommitted
Allow choice of subdimension in cirq.apply_unitary (quantumlib#4910)
Fixes quantumlib#2862 by making the unitary subspace configurable. @Strilanc @dabacon do we want to make this explicit or does it just create extra confusion, especially since it can't do arbitrary subdimensions that can't be represented as a slice?
1 parent 9fa8bf0 commit 54484bb

2 files changed

Lines changed: 314 additions & 10 deletions

File tree

cirq-core/cirq/protocols/apply_unitary_protocol.py

Lines changed: 62 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -58,10 +58,20 @@ class ApplyUnitaryArgs:
5858
dtype as the target tensor.
5959
axes: Which axes the unitary effect is being applied to (e.g. the
6060
qubits that the gate is operating on).
61+
subspaces: Which subspace (in the computational basis) the unitary
62+
effect is being applied to, on each axis. By default it applies
63+
to subspace 0..d-1 on each axis, where d is the dimension of the
64+
unitary effect on that axis. Subspaces on each axis must be
65+
representable as a slice, so the dimensions specified here need to
66+
have a consistent step size.
6167
"""
6268

6369
def __init__(
64-
self, target_tensor: np.ndarray, available_buffer: np.ndarray, axes: Iterable[int]
70+
self,
71+
target_tensor: np.ndarray,
72+
available_buffer: np.ndarray,
73+
axes: Iterable[int],
74+
subspaces: Optional[Sequence[Tuple[int, ...]]] = None,
6575
):
6676
"""Inits ApplyUnitaryArgs.
6777
@@ -75,11 +85,27 @@ def __init__(
7585
dtype as the target tensor.
7686
axes: Which axes the unitary effect is being applied to (e.g. the
7787
qubits that the gate is operating on).
78-
88+
subspaces: Which subspace (in the computational basis) the unitary
89+
effect is being applied to, on each axis. By default it applies
90+
to subspace 0..d-1 on each axis, where d is the dimension of
91+
the unitary effect on that axis. Subspaces on each axis must be
92+
representable as a slice, so the dimensions specified here need
93+
to have a consistent step size.
94+
Raises:
95+
ValueError: If the subspace count does not equal the axis count, if
96+
any subspace has zero dimensions, or if any subspace has
97+
dimensions specified without a consistent step size.
7998
"""
8099
self.target_tensor = target_tensor
81100
self.available_buffer = available_buffer
82101
self.axes = tuple(axes)
102+
if subspaces is not None:
103+
if len(self.axes) != len(subspaces):
104+
raise ValueError('Subspace count does not match axis count.')
105+
for subspace, axis in zip(subspaces, self.axes):
106+
if any(s >= target_tensor.shape[axis] for s in subspace):
107+
raise ValueError('Subspace specified does not exist in axis.')
108+
self.slices = None if subspaces is None else tuple(map(_to_slice, subspaces))
83109

84110
@staticmethod
85111
def default(
@@ -125,7 +151,7 @@ def with_axes_transposed_to_start(self) -> 'ApplyUnitaryArgs':
125151
return ApplyUnitaryArgs(target_tensor, available_buffer, range(len(self.axes)))
126152

127153
def _for_operation_with_qid_shape(
128-
self, indices: Iterable[int], qid_shape: Tuple[int, ...]
154+
self, indices: Iterable[int], slices: Tuple[Union[int, slice], ...]
129155
) -> 'ApplyUnitaryArgs':
130156
"""Creates a sliced and transposed view of `self` appropriate for an
131157
operation with shape `qid_shape` on qubits with the given indices.
@@ -138,14 +164,14 @@ def _for_operation_with_qid_shape(
138164
Args:
139165
indices: Integer indices into `self.axes` specifying which qubits
140166
the operation applies to.
141-
qid_shape: The qid shape of the operation, the expected number of
142-
quantum levels in each qubit the operation applies to.
167+
slices: The slices of the operation, the subdimension in each qubit
168+
the operation applies to.
143169
144170
Returns: A new `ApplyUnitaryArgs` where `sub_args.target_tensor` and
145171
`sub_args.available_buffer` are sliced and transposed views of
146172
`self.target_tensor` and `self.available_buffer` respectively.
147173
"""
148-
slices = [slice(0, size) for size in qid_shape]
174+
slices = tuple(size if isinstance(size, slice) else slice(0, size) for size in slices)
149175
sub_axes = [self.axes[i] for i in indices]
150176
axis_set = set(sub_axes)
151177
other_axes = [axis for axis in range(len(self.target_tensor.shape)) if axis not in axis_set]
@@ -369,8 +395,12 @@ def _strat_apply_unitary_from_apply_unitary(
369395
func = getattr(unitary_value, '_apply_unitary_', None)
370396
if func is None:
371397
return NotImplemented
372-
op_qid_shape = qid_shape_protocol.qid_shape(unitary_value, (2,) * len(args.axes))
373-
sub_args = args._for_operation_with_qid_shape(range(len(op_qid_shape)), op_qid_shape)
398+
if args.slices is None:
399+
op_qid_shape = qid_shape_protocol.qid_shape(unitary_value, (2,) * len(args.axes))
400+
slices = tuple(slice(0, size) for size in op_qid_shape)
401+
else:
402+
slices = args.slices
403+
sub_args = args._for_operation_with_qid_shape(range(len(slices)), slices)
374404
sub_result = func(sub_args)
375405
if sub_result is NotImplemented or sub_result is None:
376406
return sub_result
@@ -390,8 +420,15 @@ def _strat_apply_unitary_from_unitary(
390420
if matrix is NotImplemented or matrix is None:
391421
return matrix
392422

393-
val_qid_shape = qid_shape_protocol.qid_shape(unitary_value, default=(2,) * len(args.axes))
394-
sub_args = args._for_operation_with_qid_shape(range(len(val_qid_shape)), val_qid_shape)
423+
if args.slices is None:
424+
val_qid_shape = qid_shape_protocol.qid_shape(unitary_value, default=(2,) * len(args.axes))
425+
slices = tuple(slice(0, size) for size in val_qid_shape)
426+
else:
427+
slices = args.slices
428+
val_qid_shape = tuple(
429+
((s.step if s.stop is None else s.stop) - s.start) // (s.step or 1) for s in slices
430+
)
431+
sub_args = args._for_operation_with_qid_shape(range(len(slices)), slices)
395432
matrix = matrix.astype(sub_args.target_tensor.dtype)
396433
if len(val_qid_shape) == 1 and val_qid_shape[0] <= 2:
397434
# Special case for single-qubit, 2x2 or 1x1 operations.
@@ -557,3 +594,18 @@ def _incorporate_result_into_target(
557594
return args.available_buffer
558595
sub_args.target_tensor[...] = sub_result
559596
return args.target_tensor
597+
598+
599+
def _to_slice(subspace_def: Tuple[int, ...]):
600+
if len(subspace_def) < 1:
601+
raise ValueError(f'Subspace {subspace_def} has zero dimensions.')
602+
603+
if len(subspace_def) == 1:
604+
return slice(subspace_def[0], subspace_def[0] + 1, 1)
605+
606+
step = subspace_def[1] - subspace_def[0]
607+
for i in range(len(subspace_def) - 1):
608+
if subspace_def[i + 1] - subspace_def[i] != step:
609+
raise ValueError(f'Subspace {subspace_def} does not have consistent step size.')
610+
stop = subspace_def[-1] + step
611+
return slice(subspace_def[0], stop if stop >= 0 else None, step)

cirq-core/cirq/protocols/apply_unitary_protocol_test.py

Lines changed: 252 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -422,6 +422,258 @@ def _unitary_(self):
422422
)
423423

424424

425+
# fmt: off
426+
def test_subspace_size_2():
427+
result = cirq.apply_unitary(
428+
unitary_value=cirq.X,
429+
args=cirq.ApplyUnitaryArgs(
430+
target_tensor=cirq.eye_tensor((3,), dtype=np.complex64),
431+
available_buffer=cirq.eye_tensor((3,), dtype=np.complex64),
432+
axes=(0,),
433+
subspaces=[(0, 1)],
434+
),
435+
)
436+
np.testing.assert_allclose(
437+
result,
438+
np.array(
439+
[
440+
[0, 1, 0],
441+
[1, 0, 0],
442+
[0, 0, 1],
443+
]
444+
),
445+
atol=1e-8,
446+
)
447+
448+
result = cirq.apply_unitary(
449+
unitary_value=cirq.X,
450+
args=cirq.ApplyUnitaryArgs(
451+
target_tensor=cirq.eye_tensor((3,), dtype=np.complex64),
452+
available_buffer=cirq.eye_tensor((3,), dtype=np.complex64),
453+
axes=(0,),
454+
subspaces=[(0, 2)],
455+
),
456+
)
457+
np.testing.assert_allclose(
458+
result,
459+
np.array(
460+
[
461+
[0, 0, 1],
462+
[0, 1, 0],
463+
[1, 0, 0],
464+
]
465+
),
466+
atol=1e-8,
467+
)
468+
469+
result = cirq.apply_unitary(
470+
unitary_value=cirq.X,
471+
args=cirq.ApplyUnitaryArgs(
472+
target_tensor=cirq.eye_tensor((3,), dtype=np.complex64),
473+
available_buffer=cirq.eye_tensor((3,), dtype=np.complex64),
474+
axes=(0,),
475+
subspaces=[(1, 2)],
476+
),
477+
)
478+
np.testing.assert_allclose(
479+
result,
480+
np.array(
481+
[
482+
[1, 0, 0],
483+
[0, 0, 1],
484+
[0, 1, 0],
485+
]
486+
),
487+
atol=1e-8,
488+
)
489+
490+
result = cirq.apply_unitary(
491+
unitary_value=cirq.X,
492+
args=cirq.ApplyUnitaryArgs(
493+
target_tensor=cirq.eye_tensor((4,), dtype=np.complex64),
494+
available_buffer=cirq.eye_tensor((4,), dtype=np.complex64),
495+
axes=(0,),
496+
subspaces=[(1, 2)],
497+
),
498+
)
499+
np.testing.assert_allclose(
500+
result,
501+
np.array(
502+
[
503+
[1, 0, 0, 0],
504+
[0, 0, 1, 0],
505+
[0, 1, 0, 0],
506+
[0, 0, 0, 1],
507+
]
508+
),
509+
atol=1e-8,
510+
)
511+
512+
513+
def test_subspaces_size_3():
514+
plus_one_mod_3_gate = cirq.XPowGate(dimension=3)
515+
516+
result = cirq.apply_unitary(
517+
unitary_value=plus_one_mod_3_gate,
518+
args=cirq.ApplyUnitaryArgs(
519+
target_tensor=cirq.eye_tensor((3,), dtype=np.complex64),
520+
available_buffer=cirq.eye_tensor((3,), dtype=np.complex64),
521+
axes=(0,),
522+
subspaces=[(0, 1, 2)],
523+
),
524+
)
525+
np.testing.assert_allclose(
526+
result,
527+
np.array(
528+
[
529+
[0, 0, 1],
530+
[1, 0, 0],
531+
[0, 1, 0],
532+
]
533+
),
534+
atol=1e-8,
535+
)
536+
537+
result = cirq.apply_unitary(
538+
unitary_value=plus_one_mod_3_gate,
539+
args=cirq.ApplyUnitaryArgs(
540+
target_tensor=cirq.eye_tensor((3,), dtype=np.complex64),
541+
available_buffer=cirq.eye_tensor((3,), dtype=np.complex64),
542+
axes=(0,),
543+
subspaces=[(2, 1, 0)],
544+
),
545+
)
546+
np.testing.assert_allclose(
547+
result,
548+
np.array(
549+
[
550+
[0, 1, 0],
551+
[0, 0, 1],
552+
[1, 0, 0],
553+
]
554+
),
555+
atol=1e-8,
556+
)
557+
558+
result = cirq.apply_unitary(
559+
unitary_value=plus_one_mod_3_gate,
560+
args=cirq.ApplyUnitaryArgs(
561+
target_tensor=cirq.eye_tensor((4,), dtype=np.complex64),
562+
available_buffer=cirq.eye_tensor((4,), dtype=np.complex64),
563+
axes=(0,),
564+
subspaces=[(1, 2, 3)],
565+
),
566+
)
567+
np.testing.assert_allclose(
568+
result,
569+
np.array(
570+
[
571+
[1, 0, 0, 0],
572+
[0, 0, 0, 1],
573+
[0, 1, 0, 0],
574+
[0, 0, 1, 0],
575+
]
576+
),
577+
atol=1e-8,
578+
)
579+
580+
581+
def test_subspaces_size_1():
582+
phase_gate = cirq.MatrixGate(np.array([[1j]]))
583+
584+
result = cirq.apply_unitary(
585+
unitary_value=phase_gate,
586+
args=cirq.ApplyUnitaryArgs(
587+
target_tensor=cirq.eye_tensor((2,), dtype=np.complex64),
588+
available_buffer=cirq.eye_tensor((2,), dtype=np.complex64),
589+
axes=(0,),
590+
subspaces=[(0,)],
591+
),
592+
)
593+
np.testing.assert_allclose(
594+
result,
595+
np.array(
596+
[
597+
[1j, 0],
598+
[0, 1],
599+
]
600+
),
601+
atol=1e-8,
602+
)
603+
604+
result = cirq.apply_unitary(
605+
unitary_value=phase_gate,
606+
args=cirq.ApplyUnitaryArgs(
607+
target_tensor=cirq.eye_tensor((2,), dtype=np.complex64),
608+
available_buffer=cirq.eye_tensor((2,), dtype=np.complex64),
609+
axes=(0,),
610+
subspaces=[(1,)],
611+
),
612+
)
613+
np.testing.assert_allclose(
614+
result,
615+
np.array(
616+
[
617+
[1, 0],
618+
[0, 1j],
619+
]
620+
),
621+
atol=1e-8,
622+
)
623+
624+
result = cirq.apply_unitary(
625+
unitary_value=phase_gate,
626+
args=cirq.ApplyUnitaryArgs(
627+
target_tensor=np.array([[0, 1], [1, 0]], dtype=np.complex64),
628+
available_buffer=np.zeros((2, 2), dtype=np.complex64),
629+
axes=(0,),
630+
subspaces=[(1,)],
631+
),
632+
)
633+
np.testing.assert_allclose(
634+
result,
635+
np.array(
636+
[
637+
[0, 1],
638+
[1j, 0],
639+
]
640+
),
641+
atol=1e-8,
642+
)
643+
# fmt: on
644+
645+
646+
def test_invalid_subspaces():
647+
with pytest.raises(ValueError, match='Subspace specified does not exist in axis'):
648+
_ = cirq.ApplyUnitaryArgs(
649+
target_tensor=cirq.eye_tensor((2,), dtype=np.complex64),
650+
available_buffer=cirq.eye_tensor((2,), dtype=np.complex64),
651+
axes=(0,),
652+
subspaces=[(1, 2)],
653+
)
654+
with pytest.raises(ValueError, match='Subspace count does not match axis count'):
655+
_ = cirq.ApplyUnitaryArgs(
656+
target_tensor=cirq.eye_tensor((2,), dtype=np.complex64),
657+
available_buffer=cirq.eye_tensor((2,), dtype=np.complex64),
658+
axes=(0,),
659+
subspaces=[(0, 1), (0, 1)],
660+
)
661+
with pytest.raises(ValueError, match='has zero dimensions'):
662+
_ = cirq.ApplyUnitaryArgs(
663+
target_tensor=cirq.eye_tensor((2,), dtype=np.complex64),
664+
available_buffer=cirq.eye_tensor((2,), dtype=np.complex64),
665+
axes=(0,),
666+
subspaces=[()],
667+
)
668+
with pytest.raises(ValueError, match='does not have consistent step size'):
669+
_ = cirq.ApplyUnitaryArgs(
670+
target_tensor=cirq.eye_tensor((3,), dtype=np.complex64),
671+
available_buffer=cirq.eye_tensor((3,), dtype=np.complex64),
672+
axes=(0,),
673+
subspaces=[(0, 2, 1)],
674+
)
675+
676+
425677
def test_incorporate_result_not_view():
426678
tensor = np.zeros((2, 2))
427679
tensor2 = np.zeros((2, 2))

0 commit comments

Comments
 (0)