Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Expressions without parameters have improved compatibility with numpy. #1757

16 changes: 6 additions & 10 deletions pyquil/paulis.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -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!")

Expand Down Expand Up @@ -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]))
Expand Down
45 changes: 45 additions & 0 deletions pyquil/quilatom.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we use np.isclose with a fairly moderate threshold here? Perhaps 1e-8?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd thought about this too but decided it wasn't necessary; if the inputs are all real, imag == 0.0 in all of the inputs and there are no complex operations I can think of that would accumulate a rounding error. So I'd be inclined to leave it unless you have a specific case that would trip it.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@bramathon Any thoughts on the above? Just want to make sure before I merge as-is.

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:
MarquessV marked this conversation as resolved.
Show resolved Hide resolved
# Note: The `None` here is a placeholder for the expression in the numpy array.
# The expression instance will still be accessible in the array.
MarquessV marked this conversation as resolved.
Show resolved Hide resolved
return np.array(None, dtype=object)
MarquessV marked this conversation as resolved.
Show resolved Hide resolved


ParameterSubstitutionsMapDesignator = Mapping[Union["Parameter", "MemoryReference"], ExpressionValueDesignator]

Expand Down
19 changes: 19 additions & 0 deletions test/unit/__snapshots__/test_quilbase.ambr
Original file line number Diff line number Diff line change
Expand Up @@ -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'
# ---
Expand All @@ -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:
Expand Down Expand Up @@ -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:
Expand Down
12 changes: 11 additions & 1 deletion test/unit/test_quilbase.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading