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

[Closes #6047] Implements custom compute_decomposition method in Hermitian #6062

Merged
merged 13 commits into from
Aug 12, 2024

Conversation

andrijapau
Copy link
Contributor

@andrijapau andrijapau commented Aug 1, 2024

Context:

Prior to this fix, the compute_decomposition method of Hermitian returned a qml.DecompositionUndefinedError (due to its Operator base class inheritance). The desired fix, as laid out by the issue, was to use the existing pauli_decompose method to perform the desired decomposition.

Description of the Change:

Implemented a custom compute_decomposition static method in Hermitian to have it call the existing pauli_decompose method.

Examples

>>> op = qml.X(0) + qml.Y(1) + 2 * qml.X(0) @ qml.Z(3)
>>> op_matrix = qml.matrix(op)
>>> qml.Hermitian.compute_decomposition(op_matrix, wires=['a', 'b', 'aux'])
[(
      1.0 * (I('a') @ Y('b') @ I('aux'))
    + 1.0 * (X('a') @ I('b') @ I('aux'))
    + 2.0 * (X('a') @ I('b') @ Z('aux'))
)]

>>> op = np.array([[1, 1], [1, -1]]) / np.sqrt(2)
>>> qml.Hermitian.compute_decomposition(op, wires=0)
[(
      0.7071067811865475 * X(0)
    + 0.7071067811865475 * Z(0)
)]

Benchmarking Performance

Performance was benchmarked as a function of matrix size (see figure below). I heuristically chose to raise an inefficiency warning if the decomposition time would take longer than a second (open to suggestions 🙂). Any observable with more than seven wires will raise a user warning indicating that the decomposition may be inefficient.

I've also plotted the theoretical time complexity of the pauli_decompose method and the data is in agreement.

benchmark.pdf

Benefits:

Can now compute the decomposition of any Hermitian matrix.

Possible Drawbacks:

N/A

Related GitHub Issues:

Closes #6047

@andrijapau andrijapau marked this pull request as draft August 1, 2024 17:35
@andrijapau andrijapau changed the title [#6047] Implements custom compute_decomposition method in Hermitian [WIP] Implements custom compute_decomposition method in Hermitian Aug 1, 2024
@andrijapau
Copy link
Contributor Author

andrijapau commented Aug 1, 2024

Hi @lillian542,

I've opened up this PR as a draft in case you have some early comments.

Two things I could use some clarification on:

(1) Any feedback on my approach to benchmarking the method's performance? To me over a second of computation time could warrant some warning from the program. Although, perhaps my experience is a bit biased since I'm used to relatively small quantum system sizes 😁.

(2) Could you confirm the desired output type of qml.Hermitian.compute_decomposition? From the example in #6047 it seems like PauliSentence would be ideal, but that goes against the expected base class return type of list[Operator] causing the jax-tests above to fail. I have a fix ready to go which passes the test suite locally but it essentially acknowledges and waives the error in the relevant ops/functions/conftest.py file.

addressed in latest commit e2c9d6d

Just wanted to double check, thanks!

Copy link

codecov bot commented Aug 5, 2024

Codecov Report

All modified and coverable lines are covered by tests ✅

Project coverage is 99.65%. Comparing base (9a98871) to head (61545d0).

Additional details and impacted files
@@            Coverage Diff             @@
##           master    #6062      +/-   ##
==========================================
- Coverage   99.66%   99.65%   -0.01%     
==========================================
  Files         430      430              
  Lines       41845    41560     -285     
==========================================
- Hits        41704    41418     -286     
- Misses        141      142       +1     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

@andrijapau andrijapau marked this pull request as ready for review August 5, 2024 19:42
@andrijapau andrijapau changed the title [WIP] Implements custom compute_decomposition method in Hermitian [Closes #6047] Implements custom compute_decomposition method in Hermitian Aug 5, 2024
Copy link
Contributor

@mudit2812 mudit2812 left a comment

Choose a reason for hiding this comment

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

Thanks for opening this PR @andrijapau ! It looks really polished. I've left a few comments that need to be addressed before I can approve the changes, but overall, looking great 😄

doc/releases/changelog-dev.md Outdated Show resolved Hide resolved
pennylane/ops/qubit/observables.py Outdated Show resolved Hide resolved
pennylane/ops/qubit/observables.py Outdated Show resolved Hide resolved
pennylane/ops/qubit/observables.py Outdated Show resolved Hide resolved
Copy link
Contributor

@mudit2812 mudit2812 left a comment

Choose a reason for hiding this comment

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

Thanks for addressing all the comments so fast! I just left a few more comments pertaining to tests. I'm very happy to approve once these comments are addressed.

pennylane/ops/qubit/observables.py Show resolved Hide resolved
tests/ops/qubit/test_observables.py Outdated Show resolved Hide resolved
tests/ops/qubit/test_observables.py Outdated Show resolved Hide resolved
tests/ops/qubit/test_observables.py Outdated Show resolved Hide resolved
tests/ops/qubit/test_observables.py Show resolved Hide resolved
Copy link
Contributor

@mudit2812 mudit2812 left a comment

Choose a reason for hiding this comment

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

Nice work @andrijapau ! Thanks for the fast responses and for adding this feature 🎉

I'll request a 2nd review from the team.

tests/ops/qubit/test_observables.py Outdated Show resolved Hide resolved
@mudit2812 mudit2812 requested review from albi3ro and removed request for albi3ro August 9, 2024 13:36
Copy link
Contributor

@albi3ro albi3ro left a comment

Choose a reason for hiding this comment

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

Thanks for addressing @mudit2812 's comments 👍 The benchmarking graph looks great :) thanks for producing that.

No concerns from my side, and I'm happy to approve 🚀

@andrijapau
Copy link
Contributor Author

Great! Looks like the PR is ready to be merged whenever you are.

Thank you all for your help and guidance and I'm excited to be a Pennylane contributor! 😊

@Alex-Preciado Alex-Preciado self-requested a review August 12, 2024 06:45
@Alex-Preciado
Copy link
Contributor

Thank you so much, @andrijapau! I appreciate the effort you put into the implementation. I have a few questions about your plot: I noticed you plotted the theoretical time complexity O(n 4^n). Could you explain why this is the expected complexity? Also, how should I interpret the horizontal bars on your plot?

@andrijapau
Copy link
Contributor Author

andrijapau commented Aug 12, 2024

Certainly, @Alex-Preciado!

Could you explain why this is the expected complexity?

The short answer is that I read it in the documentation.

⏰ Open for Time Complexity Analysis ⏰
The long answer is that we can analyze the operations in `_generalized_pauli_decompose` that won't take constant time and determine the dominant time complexity.
# Permute by XORing
indices = [qml.math.array(range(shape[0]))] 
for idx in range(shape[0] - 1): # O(2^n)
    indices.append(qml.math.bitwise_xor(indices[-1], (idx + 1) ^ (idx))) 

term_mat = qml.math.cast(
    qml.math.stack(
        [qml.math.gather(matrix[idx], indice) for idx, indice in enumerate(indices)]
    ),
    complex,
) # O((2^n)^2), stacking 2^n arrays of size 2^n.

Permuting indices would be overall $\mathcal{O}(4^n)$.

# Perform Hadamard transformation on columns
hadamard_transform_mat = _walsh_hadamard_transform(qml.math.transpose(term_mat)) # O(n 2^n), from documentation

# Account for the phases from Y
phase_mat = qml.math.ones(shape, dtype=complex).reshape((2,) * (2 * num_qubits))
for idx in range(num_qubits):
    index = [slice(None)] * (2 * num_qubits)
    index[idx] = index[idx + num_qubits] = 1
    phase_mat[tuple(index)] *= 1j
phase_mat = qml.math.convert_like(qml.math.reshape(phase_mat, shape), matrix) # O(n)

WHT would be dominated by $\mathcal{O}(n 2^n)$.

# c_00 + c_11 -> I; c_00 - c_11 -> Z; c_01 + c_10 -> X; 1j*(c_10 - c_01) -> Y
# https://quantumcomputing.stackexchange.com/a/31790
term_mat = qml.math.transpose(qml.math.multiply(hadamard_transform_mat, phase_mat)) # O((2^n)^2)

Element wise multiplication has complexity $\mathcal{O}(4^n)$.

# Obtain the coefficients for each Pauli word
coeffs, obs = [], []
for pauli_rep in product("IXYZ", repeat=num_qubits):
    bit_array = qml.math.array(
        [[(rep in "YZ"), (rep in "XY")] for rep in pauli_rep], dtype=int
    ).T
    coefficient = term_mat[tuple(int("".join(map(str, x)), 2) for x in bit_array)]

    if not is_abstract(matrix) and qml.math.allclose(coefficient, 0):
        continue

    observables = (
        [(o, w) for w, o in zip(wire_order, pauli_rep) if o != I]
        if hide_identity and not all(t == I for t in pauli_rep)
        else [(o, w) for w, o in zip(wire_order, pauli_rep)]
    )
    if observables:
        coeffs.append(coefficient)
        obs.append(observables)

coeffs = qml.math.stack(coeffs)

I believe this is where the poor time complexity comes from. We need to (1) loop through all possible pauli_rep tensor products ($4^n$, four choices per qubit) and (2) create the bit_array by looping through the pauli_rep tuple of length $n$ 😱. With indexing being a constant time, the complexity of this section would be $\mathcal{O}(n 4^n)$.

The total time complexity of this algorithm would be the sum of each of these operations $\mathcal{O}(4^n + n2^n + 4^n + n4^n)$ but would be dominated by $\mathcal{O}(n4^n)$ overall.

Also, how should I interpret the horizontal bars on your plot?

The horizontal bars are auto-generated box-and-whisker plots for the performance of each hermitian decomposition test case.

When the benchmarking test is called, the following table is automatically generated,

benchmark-table.pdf

The error bars in the histogram come from the Rounds metric. The benchmarking protocol will execute the test case multiple times to account for any variations in performance. As an illustration, I've outlined a test in white which took on average $\Delta t = 257\ \mu s$ to complete (statistics generated from the 826 rounds of execution).

@Alex-Preciado
Copy link
Contributor

Awesome! Thank you so much for the detailed explanation, @andrijapau!! I’ll go ahead and update this branch and merge It 😎

@Alex-Preciado Alex-Preciado merged commit 5a7a163 into PennyLaneAI:master Aug 12, 2024
40 checks passed
mudit2812 added a commit that referenced this pull request Aug 13, 2024
After the merge of #6062 , the legacy op math tests in the plugin test
matrix fail (see
[here](https://github.com/PennyLaneAI/pennylane/actions/runs/10362479768/job/28685492331)),
since the test uses the `Operator.matrix` method to verify the
decomposition, but the decomposed operator is a legacy `Hamiltonian`
with new op math disabled, which doesn't have a matrix. This PR just
updates the test to use `qml.matrix` instead of `Operator.matrix`.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Implement Hermitian.decomposition
5 participants