diff --git a/pyquil/paulis.py b/pyquil/paulis.py index 821d24990..8ae2b201f 100644 --- a/pyquil/paulis.py +++ b/pyquil/paulis.py @@ -224,7 +224,7 @@ def __eq__(self, other: object) -> bool: return other == self else: return self.operations_as_set() == other.operations_as_set() and np.allclose( - self.coefficient, other.coefficient # type: ignore + self.coefficient, other.coefficient ) def __hash__(self) -> int: @@ -825,11 +825,11 @@ def simplify_pauli_sum(pauli_sum: PauliSum) -> PauliSum: terms = [] for term_list in like_terms.values(): first_term = term_list[0] - if len(term_list) == 1 and not np.isclose(first_term.coefficient, 0.0): # type: ignore + if len(term_list) == 1 and not np.isclose(first_term.coefficient, 0.0): terms.append(first_term) else: coeff = sum(t.coefficient for t in term_list) - if not np.isclose(coeff, 0.0): # type: ignore + if not np.isclose(coeff, 0.0): terms.append(term_with_coeff(term_list[0], coeff)) return PauliSum(terms) @@ -896,13 +896,9 @@ def is_identity(term: PauliDesignator) -> bool: :returns: True if the PauliTerm or PauliSum is a scalar multiple of identity, False otherwise """ if isinstance(term, PauliTerm): - return (len(term) == 0) and (not np.isclose(term.coefficient, 0)) # type: ignore + return (len(term) == 0) and (not np.isclose(term.coefficient, 0)) elif isinstance(term, PauliSum): - return ( - (len(term.terms) == 1) - and (len(term.terms[0]) == 0) - and (not np.isclose(term.terms[0].coefficient, 0)) # type: ignore - ) + return (len(term.terms) == 1) and (len(term.terms[0]) == 0) and (not np.isclose(term.terms[0].coefficient, 0)) else: raise TypeError("is_identity only checks PauliTerms and PauliSum objects!") @@ -1024,7 +1020,7 @@ def exponentiate_pauli_sum( assert isinstance(coeff, Number) qubit_paulis = {qubit: pauli for qubit, pauli in term.operations_as_set()} paulis = [qubit_paulis[q] if q in qubit_paulis else "I" for q in qubits] - matrix = float(np.real(coeff)) * reduce(np.kron, [pauli_matrices[p] for p in paulis]) # type: ignore + matrix = float(np.real(coeff)) * reduce(np.kron, [pauli_matrices[p] for p in paulis]) matrices.append(matrix) generated_unitary = expm(-1j * np.pi * sum(matrices)) phase = np.exp(-1j * np.angle(generated_unitary[0, 0])) diff --git a/pyquil/quilatom.py b/pyquil/quilatom.py index ab23c1200..d18c34ec2 100644 --- a/pyquil/quilatom.py +++ b/pyquil/quilatom.py @@ -537,6 +537,51 @@ def __neg__(self) -> "Mul": def _substitute(self, d: Any) -> ExpressionDesignator: return self + def _evaluate(self) -> np.complex128: + """ + Attempts to evaluate the expression to by simplifying it to a complex number. + + Expression simplification can be slow, especially for large recursive expressions. + This method will raise a ValueError if the expression cannot be simplified to a complex + number. + """ + expr = quil_rs_expr.Expression.parse(str(self)) + expr.simplify() # type: ignore[no-untyped-call] + if not expr.is_number(): + raise ValueError(f"Cannot evaluate expression {self} to a number. Got {expr}.") + return np.complex128(expr.to_number()) + + def __float__(self) -> float: + """ + Returns a copy of the expression as a float by attempting to simplify the expression. + + Expression simplification can be slow, especially for large recursive expressions. + This cast will raise a ValueError if simplification doesn't result in a real number. + """ + value = self._evaluate() + if value.imag != 0: + raise ValueError(f"Cannot convert complex value with non-zero imaginary value to float: {value}") + return float(value.real) + + def __array__(self, dtype: Optional[np.dtype] = None) -> np.ndarray: + """ + Implements the numpy array protocol for this expression. + + If the dtype is not object, then there will be an attempt to simplify the expression to a + complex number. If the expression cannot be simplified to one, then fallback to the + object representation of the expression. + + Note that expression simplification can be slow for large recursive expressions. + """ + try: + if dtype != object: + return np.asarray(self._evaluate(), dtype=dtype) + raise ValueError + except ValueError: + # Note: The `None` here is a placeholder for the expression in the numpy array. + # The expression instance will still be accessible in the array. + return np.array(None, dtype=object) + ParameterSubstitutionsMapDesignator = Mapping[Union["Parameter", "MemoryReference"], ExpressionValueDesignator] diff --git a/test/unit/__snapshots__/test_quilbase.ambr b/test/unit/__snapshots__/test_quilbase.ambr index 716994a56..c7811f422 100644 --- a/test/unit/__snapshots__/test_quilbase.ambr +++ b/test/unit/__snapshots__/test_quilbase.ambr @@ -229,6 +229,9 @@ # name: TestDefGate.test_get_constructor[No-Params] 'NoParamGate 123' # --- +# name: TestDefGate.test_get_constructor[ParameterlessExpression] + 'ParameterlessExpressions 123' +# --- # name: TestDefGate.test_get_constructor[Params] 'ParameterizedGate(%theta) 123' # --- @@ -250,6 +253,14 @@ ''' # --- +# name: TestDefGate.test_out[ParameterlessExpression] + ''' + DEFGATE ParameterlessExpressions AS MATRIX: + 1, 1.2246467991473532e-16 + 1.2246467991473532e-16, -1 + + ''' +# --- # name: TestDefGate.test_out[Params] ''' DEFGATE ParameterizedGate(%X) AS MATRIX: @@ -278,6 +289,14 @@ ''' # --- +# name: TestDefGate.test_str[ParameterlessExpression] + ''' + DEFGATE ParameterlessExpressions AS MATRIX: + 1, 1.2246467991473532e-16 + 1.2246467991473532e-16, -1 + + ''' +# --- # name: TestDefGate.test_str[Params] ''' DEFGATE ParameterizedGate(%X) AS MATRIX: diff --git a/test/unit/test_quilbase.py b/test/unit/test_quilbase.py index f1c7fcdbf..1b1541745 100644 --- a/test/unit/test_quilbase.py +++ b/test/unit/test_quilbase.py @@ -192,8 +192,18 @@ def test_compile(self, program: Program, compiler: QPUCompiler): ), [Parameter("X")], ), + ( + "ParameterlessExpressions", + np.array( + [ + [-quil_cos(np.pi), quil_sin(np.pi)], + [quil_sin(np.pi), quil_cos(np.pi)], + ] + ), + [], + ), ], - ids=("No-Params", "Params", "MixedTypes"), + ids=("No-Params", "Params", "MixedTypes", "ParameterlessExpression"), ) class TestDefGate: @pytest.fixture