Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions cirq-core/cirq/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -545,6 +545,7 @@
Timestamp,
TParamKey,
TParamVal,
TParamValComplex,
validate_probability,
value_equality,
KET_PLUS,
Expand Down
7 changes: 6 additions & 1 deletion cirq-core/cirq/ops/dense_pauli_string_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -399,11 +399,16 @@ def test_protocols():
def test_parameterizable(resolve_fn):
t = sympy.Symbol('t')
x = cirq.DensePauliString('X')
xt = x * t
x2 = x * 2
q = cirq.LineQubit(0)
assert not cirq.is_parameterized(x)
assert not cirq.is_parameterized(x * 2)
assert cirq.is_parameterized(x * t)
assert resolve_fn(x * t, {'t': 2}) == x * 2
assert resolve_fn(xt, {'t': 2}) == x2
assert resolve_fn(x * 3, {'t': 2}) == x * 3
assert resolve_fn(xt(q), {'t': 2}).gate == x2
assert resolve_fn(xt(q).gate, {'t': 2}) == x2


def test_item_immutable():
Expand Down
16 changes: 6 additions & 10 deletions cirq-core/cirq/ops/diagonal_gate.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ def _gen_gray_code(n: int) -> Iterator[Tuple[int, int]]:
class DiagonalGate(raw_types.Gate):
"""A gate given by a diagonal (2^n)\\times(2^n) matrix."""

def __init__(self, diag_angles_radians: Sequence[value.TParamVal]) -> None:
def __init__(self, diag_angles_radians: Sequence['cirq.TParamVal']) -> None:
r"""A n-qubit gate with only diagonal elements.

This gate's off-diagonal elements are zero and it's on diagonal
Expand All @@ -77,7 +77,7 @@ def __init__(self, diag_angles_radians: Sequence[value.TParamVal]) -> None:
If these values are $(x_0, x_1, \ldots , x_N)$ then the unitary
has diagonal values $(e^{i x_0}, e^{i x_1}, \ldots, e^{i x_N})$.
"""
self._diag_angles_radians: Tuple[value.TParamVal, ...] = tuple(diag_angles_radians)
self._diag_angles_radians: Tuple['cirq.TParamVal', ...] = tuple(diag_angles_radians)

def _num_qubits_(self):
return int(np.log2(len(self._diag_angles_radians)))
Expand Down Expand Up @@ -144,7 +144,7 @@ def _value_equality_values_(self) -> Any:
return tuple(self._diag_angles_radians)

def _decompose_for_basis(
self, index: int, bit_flip: int, theta: value.TParamVal, qubits: Sequence['cirq.Qid']
self, index: int, bit_flip: int, theta: 'cirq.TParamVal', qubits: Sequence['cirq.Qid']
) -> Iterator[Union['cirq.ZPowGate', 'cirq.CXPowGate']]:
if index == 0:
return []
Expand Down Expand Up @@ -183,13 +183,9 @@ def _decompose_(self, qubits: Sequence['cirq.Qid']) -> 'cirq.OP_TREE':
# decomposed gates. On its own it is not physically observable. However, if using this
# diagonal gate for sub-system like controlled gate, it is no longer equivalent. Hence,
# we add global phase.
# Global phase is ignored for parameterized gates as `cirq.GlobalPhaseGate` expects a
# scalar value.
decomposed_circ: List[Any] = (
[global_phase_op.global_phase_operation(np.exp(1j * hat_angles[0]))]
if not protocols.is_parameterized(hat_angles[0])
else []
)
decomposed_circ: List[Any] = [
global_phase_op.global_phase_operation(1j ** (2 * hat_angles[0] / np.pi))
]
for i, bit_flip in _gen_gray_code(n):
decomposed_circ.extend(self._decompose_for_basis(i, bit_flip, -hat_angles[i], qubits))
return decomposed_circ
Expand Down
2 changes: 1 addition & 1 deletion cirq-core/cirq/ops/diagonal_gate_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ def test_decomposition_with_parameterization(n):
)
resolved_op = cirq.resolve_parameters(parameterized_op, resolver)
resolved_circuit = cirq.resolve_parameters(decomposed_circuit, resolver)
cirq.testing.assert_allclose_up_to_global_phase(
np.testing.assert_allclose(
cirq.unitary(resolved_op), cirq.unitary(resolved_circuit), atol=1e-8
)

Expand Down
38 changes: 30 additions & 8 deletions cirq-core/cirq/ops/global_phase_op.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,14 @@
# See the License for the specific language governing permissions and
# limitations under the License.
"""A no-qubit global phase operation."""
from typing import Any, Dict, Sequence, Tuple, TYPE_CHECKING
from typing import AbstractSet, Any, Dict, Sequence, Tuple, TYPE_CHECKING, Union

import numpy as np
import sympy

from cirq import value, protocols
from cirq._compat import deprecated_class
from cirq.type_workarounds import NotImplementedType
from cirq.ops import gate_operation, raw_types

if TYPE_CHECKING:
Expand Down Expand Up @@ -57,30 +59,36 @@ def _json_dict_(self) -> Dict[str, Any]:

@value.value_equality(approximate=True)
class GlobalPhaseGate(raw_types.Gate):
def __init__(self, coefficient: value.Scalar, atol: float = 1e-8) -> None:
if abs(1 - abs(coefficient)) > atol:
def __init__(self, coefficient: 'cirq.TParamValComplex', atol: float = 1e-8) -> None:
if not isinstance(coefficient, sympy.Basic) and abs(1 - abs(coefficient)) > atol:
raise ValueError(f'Coefficient is not unitary: {coefficient!r}')
self._coefficient = coefficient

@property
def coefficient(self) -> value.Scalar:
def coefficient(self) -> 'cirq.TParamValComplex':
return self._coefficient

def _value_equality_values_(self) -> Any:
return self.coefficient

def _has_unitary_(self) -> bool:
return True
return not self._is_parameterized_()

def __pow__(self, power) -> 'cirq.GlobalPhaseGate':
if isinstance(power, (int, float)):
return GlobalPhaseGate(self.coefficient**power)
return NotImplemented

def _unitary_(self) -> np.ndarray:
def _unitary_(self) -> Union[np.ndarray, NotImplementedType]:
if not self._has_unitary_():
return NotImplemented
return np.array([[self.coefficient]])

def _apply_unitary_(self, args) -> np.ndarray:
def _apply_unitary_(
self, args: 'cirq.ApplyUnitaryArgs'
) -> Union[np.ndarray, NotImplementedType]:
if not self._has_unitary_():
return NotImplemented
args.target_tensor *= self.coefficient
return args.target_tensor

Expand All @@ -102,6 +110,20 @@ def _json_dict_(self) -> Dict[str, Any]:
def _qid_shape_(self) -> Tuple[int, ...]:
return tuple()

def _is_parameterized_(self) -> bool:
return protocols.is_parameterized(self.coefficient)

def _parameter_names_(self) -> AbstractSet[str]:
return protocols.parameter_names(self.coefficient)

def _resolve_parameters_(
self, resolver: 'cirq.ParamResolver', recursive: bool
) -> 'cirq.GlobalPhaseGate':
coefficient = protocols.resolve_parameters(self.coefficient, resolver, recursive)
return GlobalPhaseGate(coefficient=coefficient)


def global_phase_operation(coefficient: value.Scalar, atol: float = 1e-8) -> 'cirq.GateOperation':
def global_phase_operation(
coefficient: 'cirq.TParamValComplex', atol: float = 1e-8
) -> 'cirq.GateOperation':
return GlobalPhaseGate(coefficient, atol)()
26 changes: 26 additions & 0 deletions cirq-core/cirq/ops/global_phase_op_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

import numpy as np
import pytest
import sympy

import cirq

Expand Down Expand Up @@ -271,3 +272,28 @@ def test_gate_op_repr():

def test_gate_global_phase_op_json_dict():
assert cirq.GlobalPhaseGate(-1j)._json_dict_() == {'coefficient': -1j}


def test_parameterization():
t = sympy.Symbol('t')
gpt = cirq.GlobalPhaseGate(coefficient=t)
assert cirq.is_parameterized(gpt)
assert cirq.parameter_names(gpt) == {'t'}
assert not cirq.has_unitary(gpt)
assert gpt.coefficient == t
assert (gpt**2).coefficient == t**2


@pytest.mark.parametrize('resolve_fn', [cirq.resolve_parameters, cirq.resolve_parameters_once])
def test_resolve(resolve_fn):
t = sympy.Symbol('t')
gpt = cirq.GlobalPhaseGate(coefficient=t)
assert resolve_fn(gpt, {'t': -1}) == cirq.GlobalPhaseGate(coefficient=-1)


@pytest.mark.parametrize('resolve_fn', [cirq.resolve_parameters, cirq.resolve_parameters_once])
def test_resolve_error(resolve_fn):
t = sympy.Symbol('t')
gpt = cirq.GlobalPhaseGate(coefficient=t)
with pytest.raises(ValueError, match='Coefficient is not unitary'):
resolve_fn(gpt, {'t': -2})
52 changes: 42 additions & 10 deletions cirq-core/cirq/ops/pauli_string.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
)

import numpy as np
import sympy

from cirq import value, protocols, linalg, qis
from cirq._doc import document
Expand Down Expand Up @@ -110,7 +111,7 @@ def __init__(
self,
*contents: 'cirq.PAULI_STRING_LIKE',
qubit_pauli_map: Optional[Dict[TKey, 'cirq.Pauli']] = None,
coefficient: Union[int, float, complex] = 1,
coefficient: 'cirq.TParamValComplex' = 1,
):
"""Initializes a new PauliString.

Expand Down Expand Up @@ -146,7 +147,7 @@ def __init__(
argument specifies values that are logically *before* factors
specified in `contents`; `contents` are *right* multiplied onto
the values in this dictionary.
coefficient: Initial scalar coefficient. Defaults to 1.
coefficient: Initial scalar coefficient or symbol. Defaults to 1.

Raises:
TypeError: If the `qubit_pauli_map` has values that are not Paulis.
Expand All @@ -157,14 +158,16 @@ def __init__(
raise TypeError(f'{v} is not a Pauli')

self._qubit_pauli_map: Dict[TKey, 'cirq.Pauli'] = qubit_pauli_map or {}
self._coefficient = complex(coefficient)
self._coefficient = (
coefficient if isinstance(coefficient, sympy.Basic) else complex(coefficient)
)
if contents:
m = self.mutable_copy().inplace_left_multiply_by(contents).frozen()
self._qubit_pauli_map = m._qubit_pauli_map
self._coefficient = m._coefficient

@property
def coefficient(self) -> complex:
def coefficient(self) -> 'cirq.TParamValComplex':
return self._coefficient

def _value_equality_values_(self):
Expand Down Expand Up @@ -335,8 +338,10 @@ def _circuit_diagram_info_(self, args: 'cirq.CircuitDiagramInfoArgs') -> List[st
prefix = 'i'
elif self.coefficient == -1j:
prefix = '-i'
else:
elif isinstance(self.coefficient, numbers.Number):
prefix = f'({args.format_complex(self.coefficient)})*'
else:
prefix = f'({self.coefficient})*'
symbols[0] = f'PauliString({prefix}{symbols[0]})'
return symbols

Expand All @@ -346,7 +351,7 @@ def with_qubits(self, *new_qubits: 'cirq.Qid') -> 'PauliString':
coefficient=self._coefficient,
)

def with_coefficient(self, new_coefficient: Union[int, float, complex]) -> 'PauliString':
def with_coefficient(self, new_coefficient: 'cirq.TParamValComplex') -> 'PauliString':
return PauliString(qubit_pauli_map=dict(self._qubit_pauli_map), coefficient=new_coefficient)

def values(self) -> ValuesView[pauli_gates.Pauli]:
Expand Down Expand Up @@ -436,6 +441,8 @@ def matrix(self, qubits: Optional[Iterable[TKey]] = None) -> np.ndarray:
return linalg.kron(self.coefficient, *[protocols.unitary(f) for f in factors])

def _has_unitary_(self) -> bool:
if self._is_parameterized_():
return False
return abs(1 - abs(self.coefficient)) < 1e-6

def _unitary_(self) -> Optional[np.ndarray]:
Expand Down Expand Up @@ -489,10 +496,14 @@ def expectation_from_state_vector(
The expectation value of the input state.

Raises:
NotImplementedError: If this PauliString is non-Hermitian.
NotImplementedError: If this PauliString is non-Hermitian or
parameterized.
TypeError: If the input state is not complex.
ValueError: If the input state does not have the correct shape.
"""
if self._is_parameterized_():
raise NotImplementedError('Cannot get expectation value when parameterized')

if abs(self.coefficient.imag) > 0.0001:
raise NotImplementedError(
'Cannot compute expectation value of a non-Hermitian '
Expand Down Expand Up @@ -593,10 +604,13 @@ def expectation_from_density_matrix(
The expectation value of the input state.

Raises:
NotImplementedError: If this PauliString is non-Hermitian.
NotImplementedError: If this PauliString is non-Hermitian or
parameterized.
TypeError: If the input state is not complex.
ValueError: If the input state does not have the correct shape.
"""
if self._is_parameterized_():
raise NotImplementedError('Cannot get expectation value when parameterized')
if abs(self.coefficient.imag) > 0.0001:
raise NotImplementedError(
'Cannot compute expectation value of a non-Hermitian '
Expand Down Expand Up @@ -698,6 +712,8 @@ def __pow__(self, power):
return PauliString(
qubit_pauli_map=self._qubit_pauli_map, coefficient=self.coefficient**-1
)
if self._is_parameterized_():
return NotImplemented
if isinstance(power, (int, float)):
r, i = cmath.polar(self.coefficient)
if abs(r - 1) > 0.0001:
Expand Down Expand Up @@ -726,6 +742,8 @@ def __pow__(self, power):
return NotImplemented

def __rpow__(self, base):
if self._is_parameterized_():
return NotImplemented
if isinstance(base, (int, float)) and base > 0:
if abs(self.coefficient.real) > 0.0001:
raise NotImplementedError(
Expand Down Expand Up @@ -941,6 +959,18 @@ def pass_operations_over(
coef = -self._coefficient if should_negate else self.coefficient
return PauliString(qubit_pauli_map=pauli_map, coefficient=coef)

def _is_parameterized_(self) -> bool:
return protocols.is_parameterized(self.coefficient)

def _parameter_names_(self) -> AbstractSet[str]:
return protocols.parameter_names(self.coefficient)

def _resolve_parameters_(
self, resolver: 'cirq.ParamResolver', recursive: bool
) -> 'cirq.PauliString':
coefficient = protocols.resolve_parameters(self.coefficient, resolver, recursive)
return PauliString(qubit_pauli_map=self._qubit_pauli_map, coefficient=coefficient)


def _validate_qubit_mapping(
qubit_map: Mapping[TKey, int], pauli_qubits: Tuple[TKey, ...], num_state_qubits: int
Expand Down Expand Up @@ -1048,10 +1078,12 @@ class MutablePauliString(Generic[TKey]):
def __init__(
self,
*contents: 'cirq.PAULI_STRING_LIKE',
coefficient: Union[int, float, complex] = 1,
coefficient: 'cirq.TParamValComplex' = 1,
pauli_int_dict: Optional[Dict[TKey, int]] = None,
):
self.coefficient = complex(coefficient)
self.coefficient = (
coefficient if isinstance(coefficient, sympy.Basic) else complex(coefficient)
)
self.pauli_int_dict: Dict[TKey, int] = {} if pauli_int_dict is None else pauli_int_dict
if contents:
self.inplace_left_multiply_by(contents)
Expand Down
37 changes: 37 additions & 0 deletions cirq-core/cirq/ops/pauli_string_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -1918,3 +1918,40 @@ def test_transform_qubits():
assert m is m2
assert m == p2
assert m2 == p2


def test_parameterization():
t = sympy.Symbol('t')
q = cirq.LineQubit(0)
pst = cirq.PauliString({q: 'x'}, coefficient=t)
assert cirq.is_parameterized(pst)
assert cirq.parameter_names(pst) == {'t'}
assert pst.coefficient == 1.0 * t
assert not cirq.has_unitary(pst)
assert not cirq.is_parameterized(pst.with_coefficient(2))
with pytest.raises(TypeError):
cirq.decompose_once(pst)
with pytest.raises(NotImplementedError, match='parameterized'):
pst.expectation_from_state_vector(np.array([]), {})
with pytest.raises(NotImplementedError, match='parameterized'):
pst.expectation_from_density_matrix(np.array([]), {})
assert pst**1 == pst
assert pst**-1 == pst.with_coefficient(1.0 / t)
assert (-pst) ** 1 == -pst
assert (-pst) ** -1 == -pst.with_coefficient(1.0 / t)
assert (1j * pst) ** 1 == 1j * pst
assert (1j * pst) ** -1 == -1j * pst.with_coefficient(1.0 / t)
with pytest.raises(TypeError):
_ = pst**2
with pytest.raises(TypeError):
_ = 1**pst
cirq.testing.assert_has_diagram(cirq.Circuit(pst), '0: ───PauliString((1.0*t)*X)───')


@pytest.mark.parametrize('resolve_fn', [cirq.resolve_parameters, cirq.resolve_parameters_once])
def test_resolve(resolve_fn):
t = sympy.Symbol('t')
q = cirq.LineQubit(0)
pst = cirq.PauliString({q: 'x'}, coefficient=t)
ps1 = cirq.PauliString({q: 'x'}, coefficient=1j)
assert resolve_fn(pst, {'t': 1j}) == ps1
Loading