Skip to content

How to get the unitary matrix of a given kernel #1822

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

Open
ACE07-Sev opened this issue Jun 16, 2024 · 23 comments · May be fixed by #3028
Open

How to get the unitary matrix of a given kernel #1822

ACE07-Sev opened this issue Jun 16, 2024 · 23 comments · May be fixed by #3028
Labels
enhancement New feature or request language Anything related to the CUDA Quantum language specification UnitaryHack Good issue for UnitaryHack

Comments

@ACE07-Sev
Copy link
Contributor

ACE07-Sev commented Jun 16, 2024

Original Request

Greetings there,

Hope all are well. I would like to know how I can get the unitary matrix of a circuit in cudaq. I've seen .sample() and .get_state() for counts and statevector definitions, but haven't been able to find the unitary matrix definition. Thanks in advance!

Unitary Hack Instructions

Description

Define and implement a new API in CUDA-Q to get the unitary matrix of a kernel.
This API returns the matrix representing the unitary of the execution path, i.e., the trace, of the provided kernel.
C++:

template <typename QuantumKernel, typename... Args>
complex_matrix cudaq::get_unitary(QuantumKernel &&kernel, Args &&...args)

which returns a complex_matrix representation of the unitary.

Python:
cudaq.get_unitary(kernel, *args) -> numpy.ndarray

which returns numpy 2D array.

Details

CUDA-Q has a "tracer" mode which is used by the draw API.

/// @brief execute the kernel functor (with optional arguments) and return the
/// trace of the execution path.
template <typename KernelFunctor, typename... Args>
cudaq::Trace traceFromKernel(KernelFunctor &&kernel, Args &&...args) {

This returns a list of instructions (gate name, target qubit ids, control qubit ids, and parameters) in an execution path of the kernel.

/// @brief When run under the tracer context, persist the
/// traced quantum resources here.
Trace kernelTrace;

Use this list to compute the unitary matrix for that kernel.
The matrix for each gate can be retrieved with nvqir::getGateByName.

/// @brief Given the gate name (an element of the GateName enum),
/// return the matrix data, optionally parameterized by a rotation angle.
template <typename Scalar>
std::vector<std::complex<Scalar>>
getGateByName(GateName name, const std::vector<Scalar> angles = {}) {

Using third-party libraries is acceptable, as long as CUDA-Q already uses them. No new dependencies.

Anything that the tracer doesn't support is not expected to be handled. In other words, if one can use draw API on a kernel successfully, then one should be able to use get_unitary on it.

@bettinaheim bettinaheim added this to the release 0.8.0 milestone Jul 1, 2024
@schweitzpgi schweitzpgi added enhancement New feature or request language Anything related to the CUDA Quantum language specification labels Jul 5, 2024
@bettinaheim
Copy link
Collaborator

Tentatively moving this to 0.9 for tracking, but this is not yet confirmed as planned.

@zohimchandani
Copy link
Collaborator

+1 for this since we need it for a collaboration, thanks.

@ACE07-Sev
Copy link
Contributor Author

ACE07-Sev commented Aug 22, 2024

I would make a few suggestions for this in the interest of performance. To get the unitary we have to do tensor contraction, and it gets expensive real fast as we increase the entanglement. With that being said, I would suggest using sth like cotengra or contengrust to find optimal order of contraction. Frankly, anything by Dr. Gray is usually very good for this type of operation.

Dr. Nguyen mentioned these have to be accessible in C++, so I'll have a look for any alternatives for this in C++.

Ohh, and one thing. It would be great if you don't do this as we add gates. Some packages (I believe qiskit) do this which adds alot of overhead for deep circuits. I imagine it would be better to contract it at the end if the user wants a unitary matrix of the overall circuit. This also makes it easier to use JIT, or GPU-acceleration for performing this.

@ACE07-Sev
Copy link
Contributor Author

Greetings there,

Hope all are well. May I ask if there's an expected timeline for when this feature will be done?

@zohimchandani
Copy link
Collaborator

You can use this hack in the meantime if it helps:

import cudaq 
import numpy as np
from typing import List


num_qubits = 2
input_state = [0.0] * (2**num_qubits)  #just input a zero state 
N = 2**num_qubits
U = np.zeros((N, N), dtype=np.complex128)
params = [1 ,2]

@cudaq.kernel
def kernel(params: List[float], input_state: List[complex]):

    qubits = cudaq.qvector(input_state)
    
    rx(params[0], qubits[0])
    ry(params[1], qubits[1])
    
    y.ctrl(qubits[0], qubits[1])
    

for j in range(N): 
    state_j    = np.zeros((N), dtype=np.complex128) 
    state_j[j] = 1.0
    
    U[:, j] = np.array(cudaq.get_state(kernel, params, state_j), copy=False)
    
print(U)

@ACE07-Sev
Copy link
Contributor Author

@zohimchandani Thank you!

@ACE07-Sev
Copy link
Contributor Author

ACE07-Sev commented Nov 4, 2024

A bit of an unrelated question, sorry (too small to open a ticket for). How can I pass a list of qubits here? It doesn't let me slice nor pass in a list:

# Create a `Kernel` that accepts a qubit as an argument.
# Apply an X-gate on that qubit.
target_kernel, qubit = cudaq.make_kernel(cudaq.qubit)
target_kernel.tdg(qubit)

# Create another `Kernel` that will apply `target_kernel`
# as a controlled operation.
kernel = cudaq.make_kernel()
qubits = kernel.qalloc(3)

# In this case, `control` performs the equivalent of a
# controlled-X gate between `control_qubit` and `target_qubit`.
kernel.control(target_kernel, qubits[:2], qubits[2])

print(cudaq.draw(kernel))

I want to add a controlled Tdg gate, with qubits 0 and 1 being the control indices, and qubit 2 being the target.

@zohimchandani
Copy link
Collaborator

See this for an example and let us know if that helps.

It is recommended that you switch to the new way of creating kernels with the @cudaq.kernel decorator as shown in the example.

@ACE07-Sev
Copy link
Contributor Author

Ohh I have read that. I am wrapping cuda-quantum in my package and I use a UI similar to qiskit, hence why I need the addition of the gates to be in form of methods as opposed to all in one with the decorator. Is there a way around it?

@ACE07-Sev
Copy link
Contributor Author

Maybe the better question is how to do .ctrl with the way I'm creating the kernel. When I try circuit.x.ctrl it doesn't work given circuit.x is a partial.

I'd really appreciate some help in making the UI similar to what Qiskit, Cirq, or TKET have. This is more like Pennylane, which is what I'm trying to avoid. I'm not really comfortable with using decorated functions to represent circuits. Makes it really hard to wrap and use externally like I am.

@zohimchandani
Copy link
Collaborator

import cudaq 

n_qubits = 2

kernel = cudaq.make_kernel()

qubits = kernel.qalloc(n_qubits)

kernel.h(qubits[0])

kernel.cx(qubits[0], qubits[1])

cudaq.draw(kernel)

result = cudaq.sample(kernel)

print(result)

@ACE07-Sev
Copy link
Contributor Author

Right, but then you'll get stuck with tdg for instance. There is no ctdg. It's no longer an issue as I added native decomposition for my library to use CX and U3, and you fortunately support those two.

On a more related note, I was wondering how I could get the unitary of a circuit when using the syntax you showed (the one I use hehe). Essentially when you define a circuit

import cudaq 

n_qubits = 2

kernel = cudaq.make_kernel()

qubits = kernel.qalloc(n_qubits)

kernel.h(qubits[0])

kernel.cx(qubits[0], qubits[1])

How to get this one's unitary?

ACE07-Sev added a commit to Qualition/quick that referenced this issue Dec 8, 2024
- Added native decomposition for multi-controlled gates with `qickit/synthesis/gate_decompositions/multi_controlled_gate_decomposition` module using CX, U3, and Global Phase.
- Updated `qickit.circuit` subclasses to avoid recreation of mapping dictionary on every method call.
- Added `qickit.circuit.from_framework` module for creating `qickit.circuit.Circuit` instances from other supported frameworks.
- Moved `circuit.from_cirq()`, `circuit.from_qiskit()`, `circuit.from_tket()`, `circuit.from_quimb()`, and `circuit.from_qasm()` to `qickit.circuit.from_framework` module, and only wrapped them within `qickit.circuit.Circuit` using said functions as shortcut.
- Improved the polymorphic implementation of `qickit.circuit` subclasses with `circuit._gate_mapping()` in contrast to the prior scattered implementation.
- Deprecated `circuit.clbit_condition()` as it will be re-implemented using control flow manager in future commits properly.
- Added transpile calls to `qickit.backend.AerBackend` for using AerSimulator due to Qiskit/qiskit#13162.
- Removed `cudaq` from dependencies for the moment until NVIDIA/cuda-quantum#1822 is resolved.
- Removed `qiskit-aer-gpu` dependency for compatibility for users without GPU.
- Updated stubs to reflect the differences.
- Added additional testers.
- Suppressed testers for `qickit.synthesis.unitarypreparation.Diffusion` due to long runtime when pushed to github.
@arulandu
Copy link

arulandu commented Dec 11, 2024

Was this not included in the 0.9.0 release? I think this feature would be nice, happy to work on it as well @bettinaheim

@ACE07-Sev
Copy link
Contributor Author

It wasn't. I'd really want to see this ASAP too.

@bettinaheim
Copy link
Collaborator

Apologies for the incorrect labeling. Unfortunately, we needed to defer it.

@arulandu It would be great if you want to work on it!
A separate API very similar to draw makes sense here. Let me know if you want to give it a go, and we can give some more concrete pointers.

@ACE07-Sev
Copy link
Contributor Author

Greetings,

Hope all are well. May I ask if there has been any progress on this task?

@efratshabtai efratshabtai added the UnitaryHack Good issue for UnitaryHack label Apr 23, 2025
@ACE07-Sev
Copy link
Contributor Author

I can work on this, but would it be possible to use quimb? Makes it easier to finish the work, and is more efficient than any manual approach given the significant optimizations quimb provides, i.e., optimal path contraction.

@ACE07-Sev
Copy link
Contributor Author

Shall I add this? I have done something similar for tequila, and for quick. quimb is really nice for these use-cases. Let me know and I'll add it in. One thing though, I can only add it to the python side.

@nvidia-dobri
Copy link
Collaborator

Hi @ACE07-Sev - thanks for your interest in the issue. Generally, we try to avoid external dependencies unless the benefits significantly outweigh the cost (integration, maintenance, testing, etc.). quimb doesn't seem to match the criteria for this specific use case. Similarly, we try to maintain parity between C++ and Python features, instead of adding features to only one language.

Check out the Unitary Hack Instructions we added to the original description. In this case, it may be even be easier to start with the C++ support and create simple bindings for Python.

@ACE07-Sev
Copy link
Contributor Author

Right, but the point I was trying to make is that quimb takes care of the simulation (aka tensor contraction) given its high-level api, whereas if we do c++ we'd have to re-implement the whole thing from scratch and would not have the advantages that come from quimb, aka optimal path contraction, caching (huge time saver), and approximate tensor network representations (MPS/MPO save alot of time in simulation especially beyond 20 qubits or so).

@nvidia-dobri
Copy link
Collaborator

The simulation part should be handled for you by the traceFromKernel call mentioned in the unitary hack summary, no?

@khalatepradnya
Copy link
Collaborator

This issue is still open and up for grabs for Unitary Hack.

@Randl Randl linked a pull request Jun 10, 2025 that will close this issue
2 tasks
@Randl
Copy link

Randl commented Jun 10, 2025

I've opened a draft with initial implementation of the feature. It should work with any 1-qubit gate but not with multiqubit gates as for now

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request language Anything related to the CUDA Quantum language specification UnitaryHack Good issue for UnitaryHack
Projects
None yet
Development

Successfully merging a pull request may close this issue.

9 participants