From 45811d2a1f2a3f188a68b4f1e04b1fd049870078 Mon Sep 17 00:00:00 2001 From: Marquess Valdez Date: Tue, 26 Mar 2024 09:42:11 -0700 Subject: [PATCH 1/5] feat: Expression without parameters have improved compatibility with numpy arrays. --- pyquil/quilatom.py | 43 ++++++++++++++++++++++ test/unit/__snapshots__/test_quilbase.ambr | 19 ++++++++++ test/unit/test_quilbase.py | 12 +++++- 3 files changed, 73 insertions(+), 1 deletion(-) diff --git a/pyquil/quilatom.py b/pyquil/quilatom.py index 7cd7e7c53..cc11087e3 100644 --- a/pyquil/quilatom.py +++ b/pyquil/quilatom.py @@ -537,6 +537,49 @@ 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 + 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. + """ + if dtype == object: + return np.array(None, dtype=object) + try: + return np.asarray(self._evaluate(), dtype=dtype) + except ValueError: + 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 From 2d52fe0ee873f21dc23ea7552eee9329d5fc9b68 Mon Sep 17 00:00:00 2001 From: Marquess Valdez Date: Tue, 26 Mar 2024 09:56:01 -0700 Subject: [PATCH 2/5] simplify __array__, add comment --- pyquil/quilatom.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/pyquil/quilatom.py b/pyquil/quilatom.py index cc11087e3..267637b77 100644 --- a/pyquil/quilatom.py +++ b/pyquil/quilatom.py @@ -573,11 +573,13 @@ def __array__(self, dtype: Optional[np.dtype] = None) -> np.ndarray: Note that expression simplification can be slow for large recursive expressions. """ - if dtype == object: - return np.array(None, dtype=object) try: - return np.asarray(self._evaluate(), dtype=dtype) + 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) From a0980f9d759c4f2f1bd0558487ee00b8818e0f19 Mon Sep 17 00:00:00 2001 From: Marquess Valdez Date: Tue, 26 Mar 2024 10:03:43 -0700 Subject: [PATCH 3/5] remove unused type ignore --- pyquil/paulis.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyquil/paulis.py b/pyquil/paulis.py index 821d24990..e43dffa90 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: From c2f693a1895cbbe1b93713856af4b6c49c416ec2 Mon Sep 17 00:00:00 2001 From: Marquess Valdez Date: Tue, 26 Mar 2024 13:14:52 -0700 Subject: [PATCH 4/5] more mypy lints --- pyquil/paulis.py | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/pyquil/paulis.py b/pyquil/paulis.py index e43dffa90..8ae2b201f 100644 --- a/pyquil/paulis.py +++ b/pyquil/paulis.py @@ -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])) From 541629fea480ea3c478818a92b10d05bc04013a8 Mon Sep 17 00:00:00 2001 From: Marquess Valdez Date: Tue, 9 Apr 2024 16:50:21 -0700 Subject: [PATCH 5/5] specify ignored lint --- pyquil/quilatom.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyquil/quilatom.py b/pyquil/quilatom.py index 267637b77..2d6751275 100644 --- a/pyquil/quilatom.py +++ b/pyquil/quilatom.py @@ -546,7 +546,7 @@ def _evaluate(self) -> np.complex128: number. """ expr = quil_rs_expr.Expression.parse(str(self)) - expr.simplify() # type: ignore + 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())