Examples#

In this section, we show basic examples on how to define a quantum operator, quantum state, and then compute the action of the quantum operator on a quantum state, and, optionally, backward-differentiate the operator action (compute gradients) with respect to user-provided real parameters parameterizing the operator. We also show examples on how to propagate an MPS state in time using the TDVP method (both the single-site update and the two-site update that adapts the MPS bond extents), how to apply a matrix product operator (MPO) to an MPS via variational ALS fitting, how to compute the extreme eigenspectrum of a given operator, and how to compute the ground state of an MPO acting on an MPS using the split-scope DMRG method (both the 1-site and 2-site variants). For clarity, the quantum operator for each example is defined inside a separate C++ header, specifically transverse_ising_full_fused.h, transverse_ising_full_fused_noisy.h and transverse_ising_full_fused_noisy_grad.h, where it is wrapped in a helper C++ class UserDefinedLiouvillian. We also provide a utility header helpers.h containing convenient GPU array creation/destruction, initialization, copying, and printing helper functions.

Building code#

Assuming cuQuantum has been extracted in CUQUANTUM_ROOT and cuTENSOR is in CUTENSOR_ROOT, we update the library path as follows:

export LD_LIBRARY_PATH=${CUQUANTUM_ROOT}/lib:${CUTENSOR_ROOT}/lib:${LD_LIBRARY_PATH}

A serial sample code discussed below (operator_action_example.cpp) can be built via the following command:

nvcc operator_action_example.cpp -I${CUQUANTUM_ROOT}/include -L${CUQUANTUM_ROOT}/lib -L${CUTENSOR_ROOT}/lib -lcudensitymat -lcutensornet -lcutensor -lcusolver -lcublas -lcurand -o operator_action_example

For static linking against the cuDensityMat library, use the following command:

nvcc operator_action_example.cpp -I${CUQUANTUM_ROOT}/include ${CUQUANTUM_ROOT}/lib/libcudensitymat_static.a ${CUQUANTUM_ROOT}/lib/libcutensornet_static.a -L${CUTENSOR_ROOT}/lib -lcutensor -lcusolver -lcublas -lcurand -o operator_action_example

In order to build a parallel (MPI) version of the example operator_action_mpi_example.cpp, one will need to have a CUDA-aware MPI library installed (e.g., recent OpenMPI, MPICH or MVAPICH) and then set the environment variable $CUDENSITYMAT_COMM_LIB to the path to the MPI interface wrapper shared library libcudensitymat_distributed_interface_mpi.so. The MPI interface wrapper shared library libcudensitymat_distributed_interface_mpi.so can be built inside the ${CUQUANTUM_ROOT}/distributed_interfaces folder by calling the build script provided there. In order to link the executable to a CUDA-aware MPI library, one will need to add -I${MPI_PATH}/include and -L${MPI_PATH}/lib -lmpi to the build command:

nvcc operator_action_mpi_example.cpp -DMPI_ENABLED -I${CUQUANTUM_ROOT}/include -I${MPI_PATH}/include -L${CUQUANTUM_ROOT}/lib -L${CUTENSOR_ROOT}/lib -L${MPI_PATH}/lib -lcudensitymat -lcutensornet -lcutensor -lcusolver -lcublas -lcurand -lmpi -o operator_action_mpi_example

In order to build a parallel (NCCL) version of the example operator_action_nccl_example.cpp, one will additionally need the NCCL library installed and the environment variable $CUDENSITYMAT_COMM_LIB set to the path to the NCCL interface wrapper shared library libcudensitymat_distributed_interface_nccl.so. The NCCL interface wrapper shared library libcudensitymat_distributed_interface_nccl.so can be built inside the ${CUQUANTUM_ROOT}/distributed_interfaces folder by calling the build script provided there. Note that MPI is still required for bootstrapping (e.g., broadcasting the ncclUniqueId). In order to link the executable, one will need to add -I${NCCL_PATH}/include and -L${NCCL_PATH}/lib -lnccl in addition to the MPI flags:

nvcc operator_action_nccl_example.cpp -DNCCL_ENABLED -DMPI_ENABLED -I${CUQUANTUM_ROOT}/include -I${MPI_PATH}/include -I${NCCL_PATH}/include -L${CUQUANTUM_ROOT}/lib -L${CUTENSOR_ROOT}/lib -L${MPI_PATH}/lib -L${NCCL_PATH}/lib -lcudensitymat -lcutensornet -lcutensor -lcusolver -lcublas -lcurand -lmpi -lnccl -o operator_action_nccl_example

Warning

When running operator_action_mpi_example.cpp with a non-CUDA-aware MPI library, the program will crash.

Note

Depending on the installation of the cuQuantum SDK package, you may need to replace lib above by lib64, depending which folder name is used inside your cuQuantum SDK package.

Code example (serial execution on a single GPU)#

The following code example illustrates the common steps necessary to use the cuDensityMat library to compute the action of a quantum many-body operator on a quantum state. The full sample code can be found in the NVIDIA/cuQuantum repository (main serial code and operator definition as well as the utility code).

First let’s introduce a helper class to construct a specific quantum many-body operator, for example, the transverse field Ising Hamiltonian with fused ZZ terms and an additional noise term. Here we choose to make the f(t) coefficient depend on time and a single user-provided real parameter Omega. We use a CPU-side user-defined scalar callback function to define the dependence of the f(t) coefficient on time and the user-provided real parameter Omega. Note that inside the callback function definition, we explicitly expect the data type to be CUDA_C_64F (double-precision complex numbers), which applies to the scalar coefficient f(t) set by the callback function in-place.

  1/* Copyright (c) 2024-2025, NVIDIA CORPORATION & AFFILIATES.
  2 *
  3 * SPDX-License-Identifier: BSD-3-Clause
  4 */
  5
  6#pragma once
  7
  8#include <cudensitymat.h> // cuDensityMat library header
  9#include "helpers.h"      // GPU helper functions
 10
 11#include <cmath>
 12#include <complex>
 13#include <vector>
 14#include <iostream>
 15#include <cassert>
 16
 17
 18/* DESCRIPTION:
 19   Time-dependent transverse-field Ising Hamiltonian operator
 20   with ordered and fused ZZ terms, plus fused unitary dissipation terms:
 21    H = sum_{i} {h_i * X_i}                // transverse field sum of X_i operators with static h_i coefficients 
 22      + f(t) * sum_{i < j} {g_ij * ZZ_ij}  // modulated sum of the fused ordered {Z_i * Z_j} terms with static g_ij coefficients
 23      + d * sum_{i} {Y_i * {..} * Y_i}     // scaled sum of the dissipation terms {Y_i * {..} * Y_i} fused into the YY_ii super-operators
 24   where {..} is the placeholder for the density matrix to show that the Y_i operators act from different sides.
 25*/
 26
 27/** Define the numerical type and data type for the GPU computations (same) */
 28using NumericalType = std::complex<double>;      // do not change
 29constexpr cudaDataType_t dataType = CUDA_C_64F;  // do not change
 30
 31
 32/** Example of a user-provided scalar CPU callback C function
 33 *  defining a time-dependent coefficient inside the Hamiltonian:
 34 *  f(t) = exp(i * Omega * t) = cos(Omega * t) + i * sin(Omega * t)
 35 */
 36extern "C"
 37int32_t fCoefComplex64(
 38  double time,             //in: time point
 39  int64_t batchSize,       //in: user-defined batch size (number of coefficients in the batch)
 40  int32_t numParams,       //in: number of external user-provided Hamiltonian parameters (this function expects one parameter, Omega)
 41  const double * params,   //in: params[0:numParams-1][0:batchSize-1]: GPU-accessible F-ordered array of user-provided Hamiltonian parameters for all instances of the batch
 42  cudaDataType_t dataType, //in: data type (expecting CUDA_C_64F in this specific callback function)
 43  void * scalarStorage,    //inout: CPU-accessible storage for the returned coefficient value(s) of shape [0:batchSize-1]
 44  cudaStream_t stream)     //in: CUDA stream (default is 0x0)
 45{
 46  if (dataType == CUDA_C_64F) {
 47    auto * tdCoef = static_cast<cuDoubleComplex*>(scalarStorage); // casting to cuDoubleComplex because this callback function expects CUDA_C_64F data type
 48    for (int64_t i = 0; i < batchSize; ++i) {
 49      const auto omega = params[i * numParams + 0]; // params[0][i]: 0-th parameter for i-th instance of the batch
 50      tdCoef[i] = make_cuDoubleComplex(std::cos(omega * time), std::sin(omega * time)); // value of the i-th instance of the coefficients batch
 51    }
 52  } else {
 53    return 1; // error code (1: Error)
 54  }
 55  return 0; // error code (0: Success)
 56}
 57
 58
 59/** Convenience class which encapsulates a user-defined Liouvillian operator (system Hamiltonian + dissipation terms):
 60 *  - Constructor constructs the desired Liouvillian operator (`cudensitymatOperator_t`)
 61 *  - Method `get()` returns a reference to the constructed Liouvillian operator
 62 *  - Destructor releases all resources used by the Liouvillian operator
 63 */
 64class UserDefinedLiouvillian final
 65{
 66private:
 67  // Data members
 68  cudensitymatHandle_t handle;             // library context handle
 69  int64_t stateBatchSize;                  // quantum state batch size
 70  const std::vector<int64_t> spaceShape;   // Hilbert space shape (extents of the modes of the composite Hilbert space)
 71  void * spinXelems {nullptr};             // elements of the X spin operator in GPU RAM (F-order storage)
 72  void * spinYYelems {nullptr};            // elements of the fused YY two-spin operator in GPU RAM (F-order storage)
 73  void * spinZZelems {nullptr};            // elements of the fused ZZ two-spin operator in GPU RAM (F-order storage)
 74  cudensitymatElementaryOperator_t spinX;  // X spin operator (elementary tensor operator)
 75  cudensitymatElementaryOperator_t spinYY; // fused YY two-spin operator (elementary tensor operator)
 76  cudensitymatElementaryOperator_t spinZZ; // fused ZZ two-spin operator (elementary tensor operator)
 77  cudensitymatOperatorTerm_t oneBodyTerm;  // operator term: H1 = sum_{i} {h_i * X_i} (one-body term)
 78  cudensitymatOperatorTerm_t twoBodyTerm;  // operator term: H2 = f(t) * sum_{i < j} {g_ij * ZZ_ij} (two-body term)
 79  cudensitymatOperatorTerm_t noiseTerm;    // operator term: D1 = d * sum_{i} {YY_ii}  // Y_i operators act from different sides on the density matrix (two-body mixed term)
 80  cudensitymatOperator_t liouvillian;      // full operator: (-i * (H1 + H2) * {..}) + (i * {..} * (H1 + H2)) + D1{..} (super-operator)
 81
 82public:
 83
 84  // Constructor constructs a user-defined Liouvillian operator
 85  UserDefinedLiouvillian(cudensitymatHandle_t contextHandle,             // library context handle
 86                         const std::vector<int64_t> & hilbertSpaceShape, // Hilbert space shape
 87                         int64_t batchSize):                             // batch size for the quantum state
 88    handle(contextHandle), stateBatchSize(batchSize), spaceShape(hilbertSpaceShape)
 89  {
 90    // Define the necessary operator tensors in GPU memory (F-order storage!)
 91    spinXelems = createInitializeArrayGPU<NumericalType>(  // X[i0; j0]
 92                  {{0.0, 0.0}, {1.0, 0.0},   // 1st column of matrix X
 93                   {1.0, 0.0}, {0.0, 0.0}}); // 2nd column of matrix X
 94
 95    spinYYelems = createInitializeArrayGPU<NumericalType>(  // YY[i0, i1; j0, j1] := Y[i0; j0] * Y[i1; j1]
 96                    {{0.0, 0.0},  {0.0, 0.0}, {0.0, 0.0}, {-1.0, 0.0},  // 1st column of matrix YY
 97                     {0.0, 0.0},  {0.0, 0.0}, {1.0, 0.0}, {0.0, 0.0},   // 2nd column of matrix YY
 98                     {0.0, 0.0},  {1.0, 0.0}, {0.0, 0.0}, {0.0, 0.0},   // 3rd column of matrix YY
 99                     {-1.0, 0.0}, {0.0, 0.0}, {0.0, 0.0}, {0.0, 0.0}}); // 4th column of matrix YY
100
101    spinZZelems = createInitializeArrayGPU<NumericalType>(  // ZZ[i0, i1; j0, j1] := Z[i0; j0] * Z[i1; j1]
102                    {{1.0, 0.0}, {0.0, 0.0},  {0.0, 0.0},  {0.0, 0.0},   // 1st column of matrix ZZ
103                     {0.0, 0.0}, {-1.0, 0.0}, {0.0, 0.0},  {0.0, 0.0},   // 2nd column of matrix ZZ
104                     {0.0, 0.0}, {0.0, 0.0},  {-1.0, 0.0}, {0.0, 0.0},   // 3rd column of matrix ZZ
105                     {0.0, 0.0}, {0.0, 0.0},  {0.0, 0.0},  {1.0, 0.0}}); // 4th column of matrix ZZ
106
107    // Construct the necessary Elementary Tensor Operators
108    //  X_i operator
109    HANDLE_CUDM_ERROR(cudensitymatCreateElementaryOperator(handle,
110                        1,                                   // one-body operator
111                        std::vector<int64_t>({2}).data(),    // acts in tensor space of shape {2}
112                        CUDENSITYMAT_OPERATOR_SPARSITY_NONE, // dense tensor storage
113                        0,                                   // 0 for dense tensors
114                        nullptr,                             // nullptr for dense tensors
115                        dataType,                            // data type
116                        spinXelems,                          // tensor elements in GPU memory
117                        cudensitymatTensorCallbackNone,      // no tensor callback function (tensor is not time-dependent)
118                        cudensitymatTensorGradientCallbackNone, // no tensor gradient callback function
119                        &spinX));                            // the created elementary tensor operator
120    //  ZZ_ij = Z_i * Z_j fused operator
121    HANDLE_CUDM_ERROR(cudensitymatCreateElementaryOperator(handle,
122                        2,                                   // two-body operator
123                        std::vector<int64_t>({2,2}).data(),  // acts in tensor space of shape {2,2}
124                        CUDENSITYMAT_OPERATOR_SPARSITY_NONE, // dense tensor storage
125                        0,                                   // 0 for dense tensors
126                        nullptr,                             // nullptr for dense tensors
127                        dataType,                            // data type
128                        spinZZelems,                         // tensor elements in GPU memory
129                        cudensitymatTensorCallbackNone,      // no tensor callback function (tensor is not time-dependent)
130                        cudensitymatTensorGradientCallbackNone, // no tensor gradient callback function
131                        &spinZZ));                           // the created elementary tensor operator
132    //  YY_ii = Y_i * {..} * Y_i fused operator (note action from different sides)
133    HANDLE_CUDM_ERROR(cudensitymatCreateElementaryOperator(handle,
134                        2,                                   // two-body operator
135                        std::vector<int64_t>({2,2}).data(),  // acts in tensor space of shape {2,2}
136                        CUDENSITYMAT_OPERATOR_SPARSITY_NONE, // dense tensor storage
137                        0,                                   // 0 for dense tensors
138                        nullptr,                             // nullptr for dense tensors
139                        dataType,                            // data type
140                        spinYYelems,                         // tensor elements in GPU memory
141                        cudensitymatTensorCallbackNone,      // no tensor callback function (tensor is not time-dependent)
142                        cudensitymatTensorGradientCallbackNone, // no tensor gradient callback function
143                        &spinYY));                           // the created elementary tensor operator
144
145    // Construct the necessary Operator Terms from tensor products of Elementary Tensor Operators
146    //  Create an empty operator term
147    HANDLE_CUDM_ERROR(cudensitymatCreateOperatorTerm(handle,
148                        spaceShape.size(),                   // Hilbert space rank (number of modes)
149                        spaceShape.data(),                   // Hilbert space shape (mode extents)
150                        &oneBodyTerm));                      // the created empty operator term
151    //  Define the operator term: H1 = sum_{i} {h_i * X_i}
152    for (int32_t i = 0; i < spaceShape.size(); ++i) {
153      const double h_i = 1.0 / static_cast<double>(i+1); // assign some value to the time-independent h_i coefficient
154      HANDLE_CUDM_ERROR(cudensitymatOperatorTermAppendElementaryProduct(handle,
155                          oneBodyTerm,
156                          1,                                                             // number of elementary tensor operators in the product
157                          std::vector<cudensitymatElementaryOperator_t>({spinX}).data(), // elementary tensor operators forming the product
158                          std::vector<int32_t>({i}).data(),                              // space modes acted on by the operator product
159                          std::vector<int32_t>({0}).data(),                              // space mode action duality (0: from the left; 1: from the right)
160                          make_cuDoubleComplex(h_i, 0.0),                                // h_i constant coefficient: Always 64-bit-precision complex number
161                          cudensitymatScalarCallbackNone,                                // no time-dependent coefficient associated with this operator product
162                          cudensitymatScalarGradientCallbackNone));                      // no coefficient gradient associated with this operator product
163    }
164    //  Create an empty operator term
165    HANDLE_CUDM_ERROR(cudensitymatCreateOperatorTerm(handle,
166                        spaceShape.size(),                   // Hilbert space rank (number of modes)
167                        spaceShape.data(),                   // Hilbert space shape (mode extents)
168                        &twoBodyTerm));                      // the created empty operator term
169    //  Define the operator term: H2 = f(t) * sum_{i < j} {g_ij * ZZ_ij}
170    for (int32_t i = 0; i < spaceShape.size() - 1; ++i) {
171      for (int32_t j = (i + 1); j < spaceShape.size(); ++j) {
172        const double g_ij = -1.0 / static_cast<double>(i + j + 1); // assign some value to the time-independent g_ij coefficient
173        HANDLE_CUDM_ERROR(cudensitymatOperatorTermAppendElementaryProduct(handle,
174                            twoBodyTerm,
175                            1,                                                              // number of elementary tensor operators in the product
176                            std::vector<cudensitymatElementaryOperator_t>({spinZZ}).data(), // elementary tensor operators forming the product
177                            std::vector<int32_t>({i, j}).data(),                            // space modes acted on by the operator product
178                            std::vector<int32_t>({0, 0}).data(),                            // space mode action duality (0: from the left; 1: from the right)
179                            make_cuDoubleComplex(g_ij, 0.0),                                // g_ij constant coefficient: Always 64-bit-precision complex number
180                            cudensitymatScalarCallbackNone,                                 // no time-dependent coefficient associated with this operator product
181                            cudensitymatScalarGradientCallbackNone));                       // no coefficient gradient associated with this operator product
182      }
183    }
184    //  Create an empty operator term
185    HANDLE_CUDM_ERROR(cudensitymatCreateOperatorTerm(handle,
186                        spaceShape.size(),                   // Hilbert space rank (number of modes)
187                        spaceShape.data(),                   // Hilbert space shape (mode extents)
188                        &noiseTerm));                        // the created empty operator term
189    //  Define the operator term: D1 = d * sum_{i} {YY_ii}
190    for (int32_t i = 0; i < spaceShape.size(); ++i) {
191      HANDLE_CUDM_ERROR(cudensitymatOperatorTermAppendElementaryProduct(handle,
192                          noiseTerm,
193                          1,                                                              // number of elementary tensor operators in the product
194                          std::vector<cudensitymatElementaryOperator_t>({spinYY}).data(), // elementary tensor operators forming the product
195                          std::vector<int32_t>({i, i}).data(),                            // space modes acted on by the operator product (from different sides)
196                          std::vector<int32_t>({0, 1}).data(),                            // space mode action duality (0: from the left; 1: from the right)
197                          make_cuDoubleComplex(1.0, 0.0),                                 // default coefficient: Always 64-bit-precision complex number
198                          cudensitymatScalarCallbackNone,                                 // no time-dependent coefficient associated with this operator product
199                          cudensitymatScalarGradientCallbackNone));                       // no coefficient gradient associated with this operator product
200    }
201
202    // Construct the full Liouvillian operator as a sum of the operator terms
203    //  Create an empty operator (super-operator)
204    HANDLE_CUDM_ERROR(cudensitymatCreateOperator(handle,
205                        spaceShape.size(),                // Hilbert space rank (number of modes)
206                        spaceShape.data(),                // Hilbert space shape (modes extents)
207                        &liouvillian));                   // the created empty operator (super-operator)
208    //  Append an operator term to the operator (super-operator)
209    HANDLE_CUDM_ERROR(cudensitymatOperatorAppendTerm(handle,
210                        liouvillian,
211                        oneBodyTerm,                      // appended operator term
212                        0,                                // operator term action duality as a whole (0: acting from the left; 1: acting from the right)
213                        make_cuDoubleComplex(0.0, -1.0),  // -i constant
214                        cudensitymatScalarCallbackNone,   // no time-dependent coefficient associated with the operator term as a whole
215                        cudensitymatScalarGradientCallbackNone)); // no coefficient gradient associated with the operator term as a whole
216    //  Append an operator term to the operator (super-operator)
217    HANDLE_CUDM_ERROR(cudensitymatOperatorAppendTerm(handle,
218                        liouvillian,
219                        twoBodyTerm,                     // appended operator term
220                        0,                               // operator term action duality as a whole (0: acting from the left; 1: acting from the right)
221                        make_cuDoubleComplex(0.0, -1.0), // -i constant
222                        {fCoefComplex64, CUDENSITYMAT_CALLBACK_DEVICE_CPU, nullptr}, // CPU scalar callback function defining the time-dependent coefficient associated with this operator term as a whole
223                        cudensitymatScalarGradientCallbackNone)); // no coefficient gradient associated with this operator term as a whole
224    //  Append an operator term to the operator (super-operator)
225    HANDLE_CUDM_ERROR(cudensitymatOperatorAppendTerm(handle,
226                        liouvillian,
227                        oneBodyTerm,                      // appended operator term
228                        1,                                // operator term action duality as a whole (0: acting from the left; 1: acting from the right)
229                        make_cuDoubleComplex(0.0, 1.0),   // i constant
230                        cudensitymatScalarCallbackNone,   // no time-dependent coefficient associated with the operator term as a whole
231                        cudensitymatScalarGradientCallbackNone)); // no coefficient gradient associated with the operator term as a whole
232    //  Append an operator term to the operator (super-operator)
233    HANDLE_CUDM_ERROR(cudensitymatOperatorAppendTerm(handle,
234                        liouvillian,
235                        twoBodyTerm,                     // appended operator term
236                        1,                               // operator term action duality as a whole (0: acting from the left; 1: acting from the right)
237                        make_cuDoubleComplex(0.0, 1.0),  // i constant
238                        {fCoefComplex64, CUDENSITYMAT_CALLBACK_DEVICE_CPU, nullptr}, // CPU scalar callback function defining the time-dependent coefficient associated with this operator term as a whole
239                        cudensitymatScalarGradientCallbackNone)); // no coefficient gradient associated with this operator term as a whole
240    //  Append an operator term to the operator (super-operator)
241    const double d = 0.42; // assign some value to the time-independent coefficient
242    HANDLE_CUDM_ERROR(cudensitymatOperatorAppendTerm(handle,
243                        liouvillian,
244                        noiseTerm,                        // appended operator term
245                        0,                                // operator term action duality as a whole (no duality reversing in this case)
246                        make_cuDoubleComplex(d, 0.0),     // constant coefficient associated with the operator term as a whole
247                        cudensitymatScalarCallbackNone,   // no time-dependent coefficient associated with the operator term as a whole
248                        cudensitymatScalarGradientCallbackNone)); // no coefficient gradient associated with the operator term as a whole
249  }
250
251  // Destructor destructs the user-defined Liouvillian operator
252  ~UserDefinedLiouvillian()
253  {
254    // Destroy the Liouvillian operator
255    HANDLE_CUDM_ERROR(cudensitymatDestroyOperator(liouvillian));
256
257    // Destroy operator terms
258    HANDLE_CUDM_ERROR(cudensitymatDestroyOperatorTerm(noiseTerm));
259    HANDLE_CUDM_ERROR(cudensitymatDestroyOperatorTerm(twoBodyTerm));
260    HANDLE_CUDM_ERROR(cudensitymatDestroyOperatorTerm(oneBodyTerm));
261
262    // Destroy elementary tensor operators
263    HANDLE_CUDM_ERROR(cudensitymatDestroyElementaryOperator(spinYY));
264    HANDLE_CUDM_ERROR(cudensitymatDestroyElementaryOperator(spinZZ));
265    HANDLE_CUDM_ERROR(cudensitymatDestroyElementaryOperator(spinX));
266
267    // Destroy operator tensors
268    destroyArrayGPU(spinYYelems);
269    destroyArrayGPU(spinZZelems);
270    destroyArrayGPU(spinXelems);
271  }
272
273  // Disable copy constructor/assignment (GPU resources are private, no deep copy)
274  UserDefinedLiouvillian(const UserDefinedLiouvillian &) = delete;
275  UserDefinedLiouvillian & operator=(const UserDefinedLiouvillian &) = delete;
276  UserDefinedLiouvillian(UserDefinedLiouvillian &&) = delete;
277  UserDefinedLiouvillian & operator=(UserDefinedLiouvillian &&) = delete;
278
279  /** Returns the number of externally provided Hamiltonian parameters. */
280  int32_t getNumParameters() const
281  {
282    return 1; // one parameter Omega
283  }
284
285  /** Get access to the constructed Liouvillian operator. */
286  cudensitymatOperator_t & get()
287  {
288    return liouvillian;
289  }
290
291};

Now we can use this parameterized quantum many-body operator in our main code to compute the action of the operator on a mixed quantum state (density matrix).

  1/* Copyright (c) 2024-2025, NVIDIA CORPORATION & AFFILIATES.
  2 *
  3 * SPDX-License-Identifier: BSD-3-Clause
  4 */
  5
  6#include <cudensitymat.h>  // cuDensityMat library header
  7#include "helpers.h"       // helper functions
  8
  9
 10// Transverse Ising Hamiltonian with double summation ordering
 11// and spin-operator fusion, plus fused dissipation terms
 12#include "transverse_ising_full_fused_noisy.h"  // user-defined Liouvillian operator example
 13
 14#include <cmath>
 15#include <complex>
 16#include <vector>
 17#include <chrono>
 18#include <iostream>
 19#include <cassert>
 20
 21
 22// Number of times to perform operator action on a quantum state
 23constexpr int NUM_REPEATS = 2;
 24
 25// Logging verbosity
 26bool verbose = true;
 27
 28
 29// Example workflow
 30void exampleWorkflow(cudensitymatHandle_t handle)
 31{
 32  // Define the composite Hilbert space shape and
 33  // quantum state batch size (number of individual quantum states in a batched simulation)
 34  const std::vector<int64_t> spaceShape({2,2,2,2,2,2,2,2}); // dimensions of quantum degrees of freedom
 35  const int64_t batchSize = 1;                              // number of quantum states per batch (default is 1)
 36
 37  if (verbose) {
 38    std::cout << "Hilbert space rank = " << spaceShape.size() << "; Shape = (";
 39    for (const auto & dimsn: spaceShape)
 40      std::cout << dimsn << ",";
 41    std::cout << ")" << std::endl;
 42    std::cout << "Quantum state batch size = " << batchSize << std::endl;
 43  }
 44
 45  // Construct a user-defined Liouvillian operator using a convenience C++ class
 46  UserDefinedLiouvillian liouvillian(handle, spaceShape, batchSize);
 47  if (verbose)
 48    std::cout << "Constructed the Liouvillian operator\n";
 49
 50  // Set and place external user-provided Hamiltonian parameters in GPU memory
 51  const int32_t numParams = liouvillian.getNumParameters(); // number of external user-provided Hamiltonian parameters
 52  std::vector<double> cpuHamParams(numParams * batchSize);
 53  for (int64_t j = 0; j < batchSize; ++j) {
 54    for (int32_t i = 0; i < numParams; ++i) {
 55      cpuHamParams[j * numParams + i] = double(i+1) / double(j+1); // just setting some parameter values for each instance of the batch
 56    }
 57  }
 58  auto * hamiltonianParams = static_cast<double *>(createInitializeArrayGPU(cpuHamParams));
 59
 60  // Declare the input quantum state
 61  cudensitymatState_t inputState;
 62  HANDLE_CUDM_ERROR(cudensitymatCreateState(handle,
 63                      CUDENSITYMAT_STATE_PURITY_MIXED,  // pure (state vector) or mixed (density matrix) state
 64                      spaceShape.size(),
 65                      spaceShape.data(),
 66                      batchSize,
 67                      dataType,
 68                      &inputState));
 69
 70  // Query the size of the quantum state storage
 71  std::size_t storageSize {0}; // only one storage component (tensor) is needed (no tensor factorization)
 72  HANDLE_CUDM_ERROR(cudensitymatStateGetComponentStorageSize(handle,
 73                      inputState,
 74                      1,               // only one storage component (tensor)
 75                      &storageSize));  // storage size in bytes
 76  const std::size_t stateVolume = storageSize / sizeof(NumericalType);  // quantum state tensor volume (number of elements)
 77  if (verbose)
 78    std::cout << "Quantum state storage size (bytes) = " << storageSize << std::endl;
 79
 80  // Prepare some initial value for the input quantum state batch
 81  std::vector<NumericalType> inputStateValue(stateVolume);
 82  if constexpr (std::is_same_v<NumericalType, float>) {
 83    for (std::size_t i = 0; i < stateVolume; ++i) {
 84      inputStateValue[i] = 1.0f / float(i+1); // just some value
 85    }
 86  } else if constexpr (std::is_same_v<NumericalType, double>) {
 87    for (std::size_t i = 0; i < stateVolume; ++i) {
 88      inputStateValue[i] = 1.0 / double(i+1); // just some value
 89    }
 90  } else if constexpr (std::is_same_v<NumericalType, std::complex<float>>) {
 91    for (std::size_t i = 0; i < stateVolume; ++i) {
 92      inputStateValue[i] = NumericalType{1.0f / float(i+1), -1.0f / float(i+2)}; // just some value
 93    }
 94  } else if constexpr (std::is_same_v<NumericalType, std::complex<double>>) {
 95    for (std::size_t i = 0; i < stateVolume; ++i) {
 96      inputStateValue[i] = NumericalType{1.0 / double(i+1), -1.0 / double(i+2)}; // just some value
 97    }
 98  } else {
 99    std::cerr << "Error: Unsupported data type!\n";
100    std::exit(1);
101  }
102  // Allocate initialized GPU storage for the input quantum state with prepared values
103  auto * inputStateElems = createInitializeArrayGPU(inputStateValue);
104  if (verbose)
105    std::cout << "Allocated input quantum state storage and initialized it to some value\n";
106
107  // Attach initialized GPU storage to the input quantum state
108  HANDLE_CUDM_ERROR(cudensitymatStateAttachComponentStorage(handle,
109                      inputState,
110                      1,                                                 // only one storage component (tensor)
111                      std::vector<void*>({inputStateElems}).data(),      // pointer to the GPU storage for the quantum state
112                      std::vector<std::size_t>({storageSize}).data()));  // size of the GPU storage for the quantum state
113  if (verbose)
114    std::cout << "Constructed input quantum state\n";
115
116  // Declare the output quantum state of the same shape
117  cudensitymatState_t outputState;
118  HANDLE_CUDM_ERROR(cudensitymatCreateState(handle,
119                      CUDENSITYMAT_STATE_PURITY_MIXED,  // pure (state vector) or mixed (density matrix) state
120                      spaceShape.size(),
121                      spaceShape.data(),
122                      batchSize,
123                      dataType,
124                      &outputState));
125
126  // Allocate initialized GPU storage for the output quantum state
127  auto * outputStateElems = createArrayGPU<NumericalType>(stateVolume);
128  if (verbose)
129    std::cout << "Allocated output quantum state storage\n";
130
131  // Attach GPU storage to the output quantum state
132  HANDLE_CUDM_ERROR(cudensitymatStateAttachComponentStorage(handle,
133                      outputState,
134                      1,                                                 // only one storage component (tensor)
135                      std::vector<void*>({outputStateElems}).data(),     // pointer to the GPU storage for the quantum state
136                      std::vector<std::size_t>({storageSize}).data()));  // size of the GPU storage for the quantum state
137  if (verbose)
138    std::cout << "Constructed output quantum state\n";
139
140  // Declare a workspace descriptor
141  cudensitymatWorkspaceDescriptor_t workspaceDescr;
142  HANDLE_CUDM_ERROR(cudensitymatCreateWorkspace(handle, &workspaceDescr));
143
144  // Query free GPU memory
145  std::size_t freeMem = 0, totalMem = 0;
146  HANDLE_CUDA_ERROR(cudaMemGetInfo(&freeMem, &totalMem));
147  freeMem = static_cast<std::size_t>(static_cast<double>(freeMem) * 0.95); // take 95% of the free memory for the workspace buffer
148  if (verbose)
149    std::cout << "Max workspace buffer size (bytes) = " << freeMem << std::endl;
150
151  // Prepare the Liouvillian operator action on a quantum state (needs to be done only once)
152  const auto startTime = std::chrono::high_resolution_clock::now();
153  HANDLE_CUDM_ERROR(cudensitymatOperatorPrepareAction(handle,
154                      liouvillian.get(),
155                      inputState,
156                      outputState,
157                      CUDENSITYMAT_COMPUTE_64F,  // GPU compute type
158                      freeMem,                   // max available GPU free memory for the workspace
159                      workspaceDescr,            // workspace descriptor
160                      0x0));                     // default CUDA stream
161  const auto finishTime = std::chrono::high_resolution_clock::now();
162  const std::chrono::duration<double> timeSec = finishTime - startTime;
163  if (verbose)
164    std::cout << "Operator action preparation time (sec) = " << timeSec.count() << std::endl;
165
166  // Query the required workspace buffer size (bytes)
167  std::size_t requiredBufferSize {0};
168  HANDLE_CUDM_ERROR(cudensitymatWorkspaceGetMemorySize(handle,
169                      workspaceDescr,
170                      CUDENSITYMAT_MEMSPACE_DEVICE,
171                      CUDENSITYMAT_WORKSPACE_SCRATCH,
172                      &requiredBufferSize));
173  if (verbose)
174    std::cout << "Required workspace buffer size (bytes) = " << requiredBufferSize << std::endl;
175
176  // Allocate GPU storage for the workspace buffer
177  const std::size_t bufferVolume = requiredBufferSize / sizeof(NumericalType);
178  auto * workspaceBuffer = createArrayGPU<NumericalType>(bufferVolume);
179  if (verbose)
180    std::cout << "Allocated workspace buffer of size (bytes) = " << requiredBufferSize << std::endl;
181
182  // Attach the workspace buffer to the workspace descriptor
183  HANDLE_CUDM_ERROR(cudensitymatWorkspaceSetMemory(handle,
184                      workspaceDescr,
185                      CUDENSITYMAT_MEMSPACE_DEVICE,
186                      CUDENSITYMAT_WORKSPACE_SCRATCH,
187                      workspaceBuffer,
188                      requiredBufferSize));
189  if (verbose)
190    std::cout << "Attached workspace buffer of size (bytes) = " << requiredBufferSize << std::endl;
191
192  // Apply the Liouvillian operator to the input quatum state
193  // and accumulate its action into the output quantum state (note accumulative += semantics)
194  for (int32_t repeat = 0; repeat < NUM_REPEATS; ++repeat) { // repeat multiple times for accurate timing
195    // Zero out the output quantum state
196    HANDLE_CUDM_ERROR(cudensitymatStateInitializeZero(handle,
197                        outputState,
198                        0x0));
199    if (verbose)
200      std::cout << "Initialized the output quantum state to zero\n";
201    HANDLE_CUDA_ERROR(cudaDeviceSynchronize());
202    const auto startTime = std::chrono::high_resolution_clock::now();
203    HANDLE_CUDM_ERROR(cudensitymatOperatorComputeAction(handle,
204                        liouvillian.get(),
205                        0.3,                                   // time point (some value)
206                        batchSize,                             // user-defined batch size
207                        numParams,                             // number of external user-defined Hamiltonian parameters
208                        hamiltonianParams,                     // external Hamiltonian parameters in GPU memory
209                        inputState,                            // input quantum state
210                        outputState,                           // output quantum state
211                        workspaceDescr,                        // workspace descriptor
212                        0x0));                                 // default CUDA stream
213    HANDLE_CUDA_ERROR(cudaDeviceSynchronize());
214    const auto finishTime = std::chrono::high_resolution_clock::now();
215    const std::chrono::duration<double> timeSec = finishTime - startTime;
216    if (verbose)
217      std::cout << "Operator action computation time (sec) = " << timeSec.count() << std::endl;
218  }
219
220  // Compute the squared norm of the output quantum state
221  void * norm2 = createInitializeArrayGPU(std::vector<double>(batchSize, 0.0));
222  HANDLE_CUDM_ERROR(cudensitymatStateComputeNorm(handle,
223                      outputState,
224                      norm2,
225                      0x0));
226  if (verbose) {
227    std::cout << "Computed the output quantum state norm:\n";
228    printArrayGPU<double>(norm2, batchSize);
229  }
230
231  HANDLE_CUDA_ERROR(cudaDeviceSynchronize());
232
233  // Destroy the norm2 array
234  destroyArrayGPU(norm2);
235
236  // Destroy workspace descriptor
237  HANDLE_CUDM_ERROR(cudensitymatDestroyWorkspace(workspaceDescr));
238
239  // Destroy workspace buffer storage
240  destroyArrayGPU(workspaceBuffer);
241
242  // Destroy quantum states
243  HANDLE_CUDM_ERROR(cudensitymatDestroyState(outputState));
244  HANDLE_CUDM_ERROR(cudensitymatDestroyState(inputState));
245
246  // Destroy quantum state storage
247  destroyArrayGPU(outputStateElems);
248  destroyArrayGPU(inputStateElems);
249
250  // Destroy external Hamiltonian parameters
251  destroyArrayGPU(static_cast<void *>(hamiltonianParams));
252
253  if (verbose)
254    std::cout << "Destroyed resources\n" << std::flush;
255}
256
257
258int main(int argc, char ** argv)
259{
260  // Assign a GPU to the process
261  HANDLE_CUDA_ERROR(cudaSetDevice(0));
262  if (verbose)
263    std::cout << "Set active device\n";
264
265  // Create a library handle
266  cudensitymatHandle_t handle;
267  HANDLE_CUDM_ERROR(cudensitymatCreate(&handle));
268  if (verbose)
269    std::cout << "Created a library handle\n";
270
271  // Run the example
272  exampleWorkflow(handle);
273
274  // Destroy the library handle
275  HANDLE_CUDM_ERROR(cudensitymatDestroy(handle));
276  if (verbose)
277    std::cout << "Destroyed the library handle\n";
278
279  HANDLE_CUDA_ERROR(cudaDeviceReset());
280
281  // Done
282  return 0;
283}

Code example (parallel execution on multiple GPUs)#

It is straightforward to adapt the main serial code and enable parallel execution across multiple/many GPU devices (across multiple/many nodes). Two distributed communication backends are supported: MPI and NCCL (experimental).

MPI backend#

We will illustrate parallel execution with an example using the Message Passing Interface (MPI) as the communication layer. Below we show the minor additions that need to be made in order to enable distributed parallel execution without making any changes to the original serial source code.

The full sample code can be found in the NVIDIA/cuQuantum repository (main MPI code and operator definition as well as the utility code).

Here is the updated main code for multi-GPU runs using MPI.

  1/* Copyright (c) 2024-2025, NVIDIA CORPORATION & AFFILIATES.
  2 *
  3 * SPDX-License-Identifier: BSD-3-Clause
  4 */
  5
  6#include <cudensitymat.h>  // cuDensityMat library header
  7#include "helpers.h"       // helper functions
  8
  9
 10// Transverse Ising Hamiltonian with double summation ordering
 11// and spin-operator fusion, plus fused dissipation terms
 12#include "transverse_ising_full_fused_noisy.h"  // user-defined Liouvillian operator example
 13
 14
 15// MPI library (optional)
 16#ifdef MPI_ENABLED
 17#include <mpi.h>
 18#endif
 19
 20#include <cmath>
 21#include <complex>
 22#include <vector>
 23#include <chrono>
 24#include <iostream>
 25#include <cassert>
 26
 27
 28// Number of times to perform operator action on a quantum state
 29constexpr int NUM_REPEATS = 2;
 30
 31// Logging verbosity
 32bool verbose = true;
 33
 34
 35// Example workflow
 36void exampleWorkflow(cudensitymatHandle_t handle)
 37{
 38  // Define the composite Hilbert space shape and
 39  // quantum state batch size (number of individual quantum states in a batched simulation)
 40  const std::vector<int64_t> spaceShape({2,2,2,2,2,2,2,2}); // dimensions of quantum degrees of freedom
 41  const int64_t batchSize = 1;                              // number of quantum states per batch (default is 1)
 42
 43  if (verbose) {
 44    std::cout << "Hilbert space rank = " << spaceShape.size() << "; Shape = (";
 45    for (const auto & dimsn: spaceShape)
 46      std::cout << dimsn << ",";
 47    std::cout << ")" << std::endl;
 48    std::cout << "Quantum state batch size = " << batchSize << std::endl;
 49  }
 50
 51  // Construct a user-defined Liouvillian operator using a convenience C++ class
 52  UserDefinedLiouvillian liouvillian(handle, spaceShape, batchSize);
 53  if (verbose)
 54    std::cout << "Constructed the Liouvillian operator\n";
 55
 56  // Set and place external user-provided Hamiltonian parameters in GPU memory
 57  const int32_t numParams = liouvillian.getNumParameters(); // number of external user-provided Hamiltonian parameters
 58  std::vector<double> cpuHamParams(numParams * batchSize);
 59  for (int64_t j = 0; j < batchSize; ++j) {
 60    for (int32_t i = 0; i < numParams; ++i) {
 61      cpuHamParams[j * numParams + i] = double(i+1) / double(j+1); // just setting some parameter values for each instance of the batch
 62    }
 63  }
 64  auto * hamiltonianParams = static_cast<double *>(createInitializeArrayGPU(cpuHamParams));
 65
 66  // Declare the input quantum state
 67  cudensitymatState_t inputState;
 68  HANDLE_CUDM_ERROR(cudensitymatCreateState(handle,
 69                      CUDENSITYMAT_STATE_PURITY_MIXED,  // pure (state vector) or mixed (density matrix) state
 70                      spaceShape.size(),
 71                      spaceShape.data(),
 72                      batchSize,
 73                      dataType,
 74                      &inputState));
 75
 76  // Query the size of the quantum state storage
 77  std::size_t storageSize {0}; // only one storage component (tensor) is needed (no tensor factorization)
 78  HANDLE_CUDM_ERROR(cudensitymatStateGetComponentStorageSize(handle,
 79                      inputState,
 80                      1,               // only one storage component (tensor)
 81                      &storageSize));  // storage size in bytes
 82  const std::size_t stateVolume = storageSize / sizeof(NumericalType);  // quantum state tensor volume (number of elements)
 83  if (verbose)
 84    std::cout << "Quantum state storage size (bytes) = " << storageSize << std::endl;
 85
 86  // Prepare some initial value for the input quantum state batch
 87  std::vector<NumericalType> inputStateValue(stateVolume);
 88  if constexpr (std::is_same_v<NumericalType, float>) {
 89    for (std::size_t i = 0; i < stateVolume; ++i) {
 90      inputStateValue[i] = 1.0f / float(i+1); // just some value
 91    }
 92  } else if constexpr (std::is_same_v<NumericalType, double>) {
 93    for (std::size_t i = 0; i < stateVolume; ++i) {
 94      inputStateValue[i] = 1.0 / double(i+1); // just some value
 95    }
 96  } else if constexpr (std::is_same_v<NumericalType, std::complex<float>>) {
 97    for (std::size_t i = 0; i < stateVolume; ++i) {
 98      inputStateValue[i] = NumericalType{1.0f / float(i+1), -1.0f / float(i+2)}; // just some value
 99    }
100  } else if constexpr (std::is_same_v<NumericalType, std::complex<double>>) {
101    for (std::size_t i = 0; i < stateVolume; ++i) {
102      inputStateValue[i] = NumericalType{1.0 / double(i+1), -1.0 / double(i+2)}; // just some value
103    }
104  } else {
105    std::cerr << "Error: Unsupported data type!\n";
106    std::exit(1);
107  }
108  // Allocate initialized GPU storage for the input quantum state with prepared values
109  auto * inputStateElems = createInitializeArrayGPU(inputStateValue);
110  if (verbose)
111    std::cout << "Allocated input quantum state storage and initialized it to some value\n";
112
113  // Attach initialized GPU storage to the input quantum state
114  HANDLE_CUDM_ERROR(cudensitymatStateAttachComponentStorage(handle,
115                      inputState,
116                      1,                                                 // only one storage component (tensor)
117                      std::vector<void*>({inputStateElems}).data(),      // pointer to the GPU storage for the quantum state
118                      std::vector<std::size_t>({storageSize}).data()));  // size of the GPU storage for the quantum state
119  if (verbose)
120    std::cout << "Constructed input quantum state\n";
121
122  // Declare the output quantum state of the same shape
123  cudensitymatState_t outputState;
124  HANDLE_CUDM_ERROR(cudensitymatCreateState(handle,
125                      CUDENSITYMAT_STATE_PURITY_MIXED,  // pure (state vector) or mixed (density matrix) state
126                      spaceShape.size(),
127                      spaceShape.data(),
128                      batchSize,
129                      dataType,
130                      &outputState));
131
132  // Allocate initialized GPU storage for the output quantum state
133  auto * outputStateElems = createArrayGPU<NumericalType>(stateVolume);
134  if (verbose)
135    std::cout << "Allocated output quantum state storage\n";
136
137  // Attach GPU storage to the output quantum state
138  HANDLE_CUDM_ERROR(cudensitymatStateAttachComponentStorage(handle,
139                      outputState,
140                      1,                                                 // only one storage component (tensor)
141                      std::vector<void*>({outputStateElems}).data(),     // pointer to the GPU storage for the quantum state
142                      std::vector<std::size_t>({storageSize}).data()));  // size of the GPU storage for the quantum state
143  if (verbose)
144    std::cout << "Constructed output quantum state\n";
145
146  // Declare a workspace descriptor
147  cudensitymatWorkspaceDescriptor_t workspaceDescr;
148  HANDLE_CUDM_ERROR(cudensitymatCreateWorkspace(handle, &workspaceDescr));
149
150  // Query free GPU memory
151  std::size_t freeMem = 0, totalMem = 0;
152  HANDLE_CUDA_ERROR(cudaMemGetInfo(&freeMem, &totalMem));
153  freeMem = static_cast<std::size_t>(static_cast<double>(freeMem) * 0.95); // take 95% of the free memory for the workspace buffer
154  if (verbose)
155    std::cout << "Max workspace buffer size (bytes) = " << freeMem << std::endl;
156
157  // Prepare the Liouvillian operator action on a quantum state (needs to be done only once)
158  const auto startTime = std::chrono::high_resolution_clock::now();
159  HANDLE_CUDM_ERROR(cudensitymatOperatorPrepareAction(handle,
160                      liouvillian.get(),
161                      inputState,
162                      outputState,
163                      CUDENSITYMAT_COMPUTE_64F,  // GPU compute type
164                      freeMem,                   // max available GPU free memory for the workspace
165                      workspaceDescr,            // workspace descriptor
166                      0x0));                     // default CUDA stream
167  const auto finishTime = std::chrono::high_resolution_clock::now();
168  const std::chrono::duration<double> timeSec = finishTime - startTime;
169  if (verbose)
170    std::cout << "Operator action preparation time (sec) = " << timeSec.count() << std::endl;
171
172  // Query the required workspace buffer size (bytes)
173  std::size_t requiredBufferSize {0};
174  HANDLE_CUDM_ERROR(cudensitymatWorkspaceGetMemorySize(handle,
175                      workspaceDescr,
176                      CUDENSITYMAT_MEMSPACE_DEVICE,
177                      CUDENSITYMAT_WORKSPACE_SCRATCH,
178                      &requiredBufferSize));
179  if (verbose)
180    std::cout << "Required workspace buffer size (bytes) = " << requiredBufferSize << std::endl;
181
182  // Allocate GPU storage for the workspace buffer
183  const std::size_t bufferVolume = requiredBufferSize / sizeof(NumericalType);
184  auto * workspaceBuffer = createArrayGPU<NumericalType>(bufferVolume);
185  if (verbose)
186    std::cout << "Allocated workspace buffer of size (bytes) = " << requiredBufferSize << std::endl;
187
188  // Attach the workspace buffer to the workspace descriptor
189  HANDLE_CUDM_ERROR(cudensitymatWorkspaceSetMemory(handle,
190                      workspaceDescr,
191                      CUDENSITYMAT_MEMSPACE_DEVICE,
192                      CUDENSITYMAT_WORKSPACE_SCRATCH,
193                      workspaceBuffer,
194                      requiredBufferSize));
195  if (verbose)
196    std::cout << "Attached workspace buffer of size (bytes) = " << requiredBufferSize << std::endl;
197
198  // Apply the Liouvillian operator to the input quatum state
199  // and accumulate its action into the output quantum state (note accumulative += semantics)
200  for (int32_t repeat = 0; repeat < NUM_REPEATS; ++repeat) { // repeat multiple times for accurate timing
201    // Zero out the output quantum state
202    HANDLE_CUDM_ERROR(cudensitymatStateInitializeZero(handle,
203                        outputState,
204                        0x0));
205    if (verbose)
206      std::cout << "Initialized the output quantum state to zero\n";
207    HANDLE_CUDA_ERROR(cudaDeviceSynchronize());
208    const auto startTime = std::chrono::high_resolution_clock::now();
209    HANDLE_CUDM_ERROR(cudensitymatOperatorComputeAction(handle,
210                        liouvillian.get(),
211                        0.3,                                   // time point (some value)
212                        batchSize,                             // user-defined batch size
213                        numParams,                             // number of external user-defined Hamiltonian parameters
214                        hamiltonianParams,                     // external Hamiltonian parameters in GPU memory
215                        inputState,                            // input quantum state
216                        outputState,                           // output quantum state
217                        workspaceDescr,                        // workspace descriptor
218                        0x0));                                 // default CUDA stream
219    HANDLE_CUDA_ERROR(cudaDeviceSynchronize());
220    const auto finishTime = std::chrono::high_resolution_clock::now();
221    const std::chrono::duration<double> timeSec = finishTime - startTime;
222    if (verbose)
223      std::cout << "Operator action computation time (sec) = " << timeSec.count() << std::endl;
224  }
225
226  // Compute the squared norm of the output quantum state
227  void * norm2 = createInitializeArrayGPU(std::vector<double>(batchSize, 0.0));
228  HANDLE_CUDM_ERROR(cudensitymatStateComputeNorm(handle,
229                      outputState,
230                      norm2,
231                      0x0));
232  if (verbose) {
233    std::cout << "Computed the output quantum state norm:\n";
234    printArrayGPU<double>(norm2, batchSize);
235  }
236
237  HANDLE_CUDA_ERROR(cudaDeviceSynchronize());
238
239  // Destroy the norm2 array
240  destroyArrayGPU(norm2);
241
242  // Destroy workspace descriptor
243  HANDLE_CUDM_ERROR(cudensitymatDestroyWorkspace(workspaceDescr));
244
245  // Destroy workspace buffer storage
246  destroyArrayGPU(workspaceBuffer);
247
248  // Destroy quantum states
249  HANDLE_CUDM_ERROR(cudensitymatDestroyState(outputState));
250  HANDLE_CUDM_ERROR(cudensitymatDestroyState(inputState));
251
252  // Destroy quantum state storage
253  destroyArrayGPU(outputStateElems);
254  destroyArrayGPU(inputStateElems);
255
256  // Destroy external Hamiltonian parameters
257  destroyArrayGPU(static_cast<void *>(hamiltonianParams));
258
259  if (verbose)
260    std::cout << "Destroyed resources\n" << std::flush;
261}
262
263
264int main(int argc, char ** argv)
265{
266  // Initialize MPI library (if needed)
267#ifdef MPI_ENABLED
268  HANDLE_MPI_ERROR(MPI_Init(&argc, &argv));
269  int procRank {-1};
270  HANDLE_MPI_ERROR(MPI_Comm_rank(MPI_COMM_WORLD, &procRank));
271  int numProcs {0};
272  HANDLE_MPI_ERROR(MPI_Comm_size(MPI_COMM_WORLD, &numProcs));
273  if (procRank != 0) verbose = false;
274  if (verbose)
275    std::cout << "Initialized MPI library\n";
276#else
277  const int procRank {0};
278  const int numProcs {1};
279#endif
280
281  // Assign a GPU to the process
282  int numDevices {0};
283  HANDLE_CUDA_ERROR(cudaGetDeviceCount(&numDevices));
284  const int deviceId = procRank % numDevices;
285  HANDLE_CUDA_ERROR(cudaSetDevice(deviceId));
286  if (verbose)
287    std::cout << "Set active device\n";
288
289  // Create a library handle
290  cudensitymatHandle_t handle;
291  HANDLE_CUDM_ERROR(cudensitymatCreate(&handle));
292  if (verbose)
293    std::cout << "Created a library handle\n";
294
295  // Reset distributed configuration (once)
296#ifdef MPI_ENABLED
297  MPI_Comm comm;
298  HANDLE_MPI_ERROR(MPI_Comm_dup(MPI_COMM_WORLD, &comm));
299  HANDLE_CUDM_ERROR(cudensitymatResetDistributedConfiguration(handle,
300                      CUDENSITYMAT_DISTRIBUTED_PROVIDER_MPI,
301                      &comm, sizeof(comm)));
302#endif
303
304  // Run the example
305  exampleWorkflow(handle);
306
307  // Synchronize MPI processes
308#ifdef MPI_ENABLED
309  HANDLE_MPI_ERROR(MPI_Barrier(MPI_COMM_WORLD));
310#endif
311
312  // Destroy the library handle
313  HANDLE_CUDM_ERROR(cudensitymatDestroy(handle));
314  if (verbose)
315    std::cout << "Destroyed the library handle\n";
316
317  HANDLE_CUDA_ERROR(cudaDeviceReset());
318
319  // Finalize the MPI library
320#ifdef MPI_ENABLED
321  HANDLE_MPI_ERROR(MPI_Finalize());
322  if (verbose)
323    std::cout << "Finalized MPI library\n";
324#endif
325
326  // Done
327  return 0;
328}

NCCL backend (experimental)#

NCCL (NVIDIA Collective Communications Library) can provide better performance for GPU-to-GPU communication, especially within a single node with NVLink connectivity. Below we show an example using NCCL as the communication layer. Note that the NCCL backend is currently experimental. MPI is used for process spawning and bootstrapping (e.g., broadcasting the ncclUniqueId), but all GPU-to-GPU communication uses NCCL.

The full sample code can be found in the NVIDIA/cuQuantum repository (main NCCL code and operator definition as well as the utility code).

Here is the main code for multi-GPU runs using NCCL.

  1/* Copyright (c) 2024-2026, NVIDIA CORPORATION & AFFILIATES.
  2 *
  3 * SPDX-License-Identifier: BSD-3-Clause
  4 */
  5
  6#include <cudensitymat.h>  // cuDensityMat library header
  7#include "helpers.h"       // helper functions
  8
  9
 10// Transverse Ising Hamiltonian with double summation ordering
 11// and spin-operator fusion, plus fused dissipation terms
 12#include "transverse_ising_full_fused_noisy.h"  // user-defined Liouvillian operator example
 13
 14
 15// NCCL library (required for this example)
 16#ifdef NCCL_ENABLED
 17#include <nccl.h>
 18#endif
 19
 20// MPI library (used for bootstrapping NCCL communicator)
 21#ifdef MPI_ENABLED
 22#include <mpi.h>
 23#endif
 24
 25#include <cmath>
 26#include <complex>
 27#include <vector>
 28#include <chrono>
 29#include <iostream>
 30#include <cassert>
 31
 32
 33// Number of times to perform operator action on a quantum state
 34constexpr int NUM_REPEATS = 2;
 35
 36// Logging verbosity
 37bool verbose = true;
 38
 39
 40#ifdef NCCL_ENABLED
 41// Error handling macro for NCCL
 42#define HANDLE_NCCL_ERROR(x)                                 \
 43{                                                            \
 44  const ncclResult_t err = x;                                \
 45  if (err != ncclSuccess)                                    \
 46  {                                                          \
 47    printf("NCCL Error: %s in line %d\n",                    \
 48           ncclGetErrorString(err), __LINE__);               \
 49    fflush(stdout);                                          \
 50    std::abort();                                            \
 51  }                                                          \
 52};
 53#endif
 54
 55
 56// Example workflow
 57void exampleWorkflow(cudensitymatHandle_t handle)
 58{
 59  // Define the composite Hilbert space shape and
 60  // quantum state batch size (number of individual quantum states in a batched simulation)
 61  const std::vector<int64_t> spaceShape({2,2,2,2,2,2,2,2}); // dimensions of quantum degrees of freedom
 62  const int64_t batchSize = 1;                              // number of quantum states per batch (default is 1)
 63
 64  if (verbose) {
 65    std::cout << "Hilbert space rank = " << spaceShape.size() << "; Shape = (";
 66    for (const auto & dimsn: spaceShape)
 67      std::cout << dimsn << ",";
 68    std::cout << ")" << std::endl;
 69    std::cout << "Quantum state batch size = " << batchSize << std::endl;
 70  }
 71
 72  // Construct a user-defined Liouvillian operator using a convenience C++ class
 73  UserDefinedLiouvillian liouvillian(handle, spaceShape, batchSize);
 74  if (verbose)
 75    std::cout << "Constructed the Liouvillian operator\n";
 76
 77  // Set and place external user-provided Hamiltonian parameters in GPU memory
 78  const int32_t numParams = liouvillian.getNumParameters(); // number of external user-provided Hamiltonian parameters
 79  std::vector<double> cpuHamParams(numParams * batchSize);
 80  for (int64_t j = 0; j < batchSize; ++j) {
 81    for (int32_t i = 0; i < numParams; ++i) {
 82      cpuHamParams[j * numParams + i] = double(i+1) / double(j+1); // just setting some parameter values for each instance of the batch
 83    }
 84  }
 85  auto * hamiltonianParams = static_cast<double *>(createInitializeArrayGPU(cpuHamParams));
 86
 87  // Declare the input quantum state
 88  cudensitymatState_t inputState;
 89  HANDLE_CUDM_ERROR(cudensitymatCreateState(handle,
 90                      CUDENSITYMAT_STATE_PURITY_MIXED,  // pure (state vector) or mixed (density matrix) state
 91                      spaceShape.size(),
 92                      spaceShape.data(),
 93                      batchSize,
 94                      dataType,
 95                      &inputState));
 96
 97  // Query the size of the quantum state storage
 98  std::size_t storageSize {0}; // only one storage component (tensor) is needed (no tensor factorization)
 99  HANDLE_CUDM_ERROR(cudensitymatStateGetComponentStorageSize(handle,
100                      inputState,
101                      1,               // only one storage component (tensor)
102                      &storageSize));  // storage size in bytes
103  const std::size_t stateVolume = storageSize / sizeof(NumericalType);  // quantum state tensor volume (number of elements)
104  if (verbose)
105    std::cout << "Quantum state storage size (bytes) = " << storageSize << std::endl;
106
107  // Prepare some initial value for the input quantum state batch
108  std::vector<NumericalType> inputStateValue(stateVolume);
109  if constexpr (std::is_same_v<NumericalType, float>) {
110    for (std::size_t i = 0; i < stateVolume; ++i) {
111      inputStateValue[i] = 1.0f / float(i+1); // just some value
112    }
113  } else if constexpr (std::is_same_v<NumericalType, double>) {
114    for (std::size_t i = 0; i < stateVolume; ++i) {
115      inputStateValue[i] = 1.0 / double(i+1); // just some value
116    }
117  } else if constexpr (std::is_same_v<NumericalType, std::complex<float>>) {
118    for (std::size_t i = 0; i < stateVolume; ++i) {
119      inputStateValue[i] = NumericalType{1.0f / float(i+1), -1.0f / float(i+2)}; // just some value
120    }
121  } else if constexpr (std::is_same_v<NumericalType, std::complex<double>>) {
122    for (std::size_t i = 0; i < stateVolume; ++i) {
123      inputStateValue[i] = NumericalType{1.0 / double(i+1), -1.0 / double(i+2)}; // just some value
124    }
125  } else {
126    std::cerr << "Error: Unsupported data type!\n";
127    std::exit(1);
128  }
129  // Allocate initialized GPU storage for the input quantum state with prepared values
130  auto * inputStateElems = createInitializeArrayGPU(inputStateValue);
131  if (verbose)
132    std::cout << "Allocated input quantum state storage and initialized it to some value\n";
133
134  // Attach initialized GPU storage to the input quantum state
135  HANDLE_CUDM_ERROR(cudensitymatStateAttachComponentStorage(handle,
136                      inputState,
137                      1,                                                 // only one storage component (tensor)
138                      std::vector<void*>({inputStateElems}).data(),      // pointer to the GPU storage for the quantum state
139                      std::vector<std::size_t>({storageSize}).data()));  // size of the GPU storage for the quantum state
140  if (verbose)
141    std::cout << "Constructed input quantum state\n";
142
143  // Declare the output quantum state of the same shape
144  cudensitymatState_t outputState;
145  HANDLE_CUDM_ERROR(cudensitymatCreateState(handle,
146                      CUDENSITYMAT_STATE_PURITY_MIXED,  // pure (state vector) or mixed (density matrix) state
147                      spaceShape.size(),
148                      spaceShape.data(),
149                      batchSize,
150                      dataType,
151                      &outputState));
152
153  // Allocate initialized GPU storage for the output quantum state
154  auto * outputStateElems = createArrayGPU<NumericalType>(stateVolume);
155  if (verbose)
156    std::cout << "Allocated output quantum state storage\n";
157
158  // Attach GPU storage to the output quantum state
159  HANDLE_CUDM_ERROR(cudensitymatStateAttachComponentStorage(handle,
160                      outputState,
161                      1,                                                 // only one storage component (tensor)
162                      std::vector<void*>({outputStateElems}).data(),     // pointer to the GPU storage for the quantum state
163                      std::vector<std::size_t>({storageSize}).data()));  // size of the GPU storage for the quantum state
164  if (verbose)
165    std::cout << "Constructed output quantum state\n";
166
167  // Declare a workspace descriptor
168  cudensitymatWorkspaceDescriptor_t workspaceDescr;
169  HANDLE_CUDM_ERROR(cudensitymatCreateWorkspace(handle, &workspaceDescr));
170
171  // Query free GPU memory
172  std::size_t freeMem = 0, totalMem = 0;
173  HANDLE_CUDA_ERROR(cudaMemGetInfo(&freeMem, &totalMem));
174  freeMem = static_cast<std::size_t>(static_cast<double>(freeMem) * 0.95); // take 95% of the free memory for the workspace buffer
175  if (verbose)
176    std::cout << "Max workspace buffer size (bytes) = " << freeMem << std::endl;
177
178  // Prepare the Liouvillian operator action on a quantum state (needs to be done only once)
179  const auto startTime = std::chrono::high_resolution_clock::now();
180  HANDLE_CUDM_ERROR(cudensitymatOperatorPrepareAction(handle,
181                      liouvillian.get(),
182                      inputState,
183                      outputState,
184                      CUDENSITYMAT_COMPUTE_64F,  // GPU compute type
185                      freeMem,                   // max available GPU free memory for the workspace
186                      workspaceDescr,            // workspace descriptor
187                      0x0));                     // default CUDA stream
188  const auto finishTime = std::chrono::high_resolution_clock::now();
189  const std::chrono::duration<double> timeSec = finishTime - startTime;
190  if (verbose)
191    std::cout << "Operator action preparation time (sec) = " << timeSec.count() << std::endl;
192
193  // Query the required workspace buffer size (bytes)
194  std::size_t requiredBufferSize {0};
195  HANDLE_CUDM_ERROR(cudensitymatWorkspaceGetMemorySize(handle,
196                      workspaceDescr,
197                      CUDENSITYMAT_MEMSPACE_DEVICE,
198                      CUDENSITYMAT_WORKSPACE_SCRATCH,
199                      &requiredBufferSize));
200  if (verbose)
201    std::cout << "Required workspace buffer size (bytes) = " << requiredBufferSize << std::endl;
202
203  // Allocate GPU storage for the workspace buffer
204  const std::size_t bufferVolume = requiredBufferSize / sizeof(NumericalType);
205  auto * workspaceBuffer = createArrayGPU<NumericalType>(bufferVolume);
206  if (verbose)
207    std::cout << "Allocated workspace buffer of size (bytes) = " << requiredBufferSize << std::endl;
208
209  // Attach the workspace buffer to the workspace descriptor
210  HANDLE_CUDM_ERROR(cudensitymatWorkspaceSetMemory(handle,
211                      workspaceDescr,
212                      CUDENSITYMAT_MEMSPACE_DEVICE,
213                      CUDENSITYMAT_WORKSPACE_SCRATCH,
214                      workspaceBuffer,
215                      requiredBufferSize));
216  if (verbose)
217    std::cout << "Attached workspace buffer of size (bytes) = " << requiredBufferSize << std::endl;
218
219  // Apply the Liouvillian operator to the input quatum state
220  // and accumulate its action into the output quantum state (note accumulative += semantics)
221  for (int32_t repeat = 0; repeat < NUM_REPEATS; ++repeat) { // repeat multiple times for accurate timing
222    // Zero out the output quantum state
223    HANDLE_CUDM_ERROR(cudensitymatStateInitializeZero(handle,
224                        outputState,
225                        0x0));
226    if (verbose)
227      std::cout << "Initialized the output quantum state to zero\n";
228    HANDLE_CUDA_ERROR(cudaDeviceSynchronize());
229    const auto startTime = std::chrono::high_resolution_clock::now();
230    HANDLE_CUDM_ERROR(cudensitymatOperatorComputeAction(handle,
231                        liouvillian.get(),
232                        0.3,                                   // time point (some value)
233                        batchSize,                             // user-defined batch size
234                        numParams,                             // number of external user-defined Hamiltonian parameters
235                        hamiltonianParams,                     // external Hamiltonian parameters in GPU memory
236                        inputState,                            // input quantum state
237                        outputState,                           // output quantum state
238                        workspaceDescr,                        // workspace descriptor
239                        0x0));                                 // default CUDA stream
240    HANDLE_CUDA_ERROR(cudaDeviceSynchronize());
241    const auto finishTime = std::chrono::high_resolution_clock::now();
242    const std::chrono::duration<double> timeSec = finishTime - startTime;
243    if (verbose)
244      std::cout << "Operator action computation time (sec) = " << timeSec.count() << std::endl;
245  }
246
247  // Compute the squared norm of the output quantum state
248  void * norm2 = createInitializeArrayGPU(std::vector<double>(batchSize, 0.0));
249  HANDLE_CUDM_ERROR(cudensitymatStateComputeNorm(handle,
250                      outputState,
251                      norm2,
252                      0x0));
253  if (verbose) {
254    std::cout << "Computed the output quantum state norm:\n";
255    printArrayGPU<double>(norm2, batchSize);
256  }
257
258  HANDLE_CUDA_ERROR(cudaDeviceSynchronize());
259
260  // Destroy the norm2 array
261  destroyArrayGPU(norm2);
262
263  // Destroy workspace descriptor
264  HANDLE_CUDM_ERROR(cudensitymatDestroyWorkspace(workspaceDescr));
265
266  // Destroy workspace buffer storage
267  destroyArrayGPU(workspaceBuffer);
268
269  // Destroy quantum states
270  HANDLE_CUDM_ERROR(cudensitymatDestroyState(outputState));
271  HANDLE_CUDM_ERROR(cudensitymatDestroyState(inputState));
272
273  // Destroy quantum state storage
274  destroyArrayGPU(outputStateElems);
275  destroyArrayGPU(inputStateElems);
276
277  // Destroy external Hamiltonian parameters
278  destroyArrayGPU(static_cast<void *>(hamiltonianParams));
279
280  if (verbose)
281    std::cout << "Destroyed resources\n" << std::flush;
282}
283
284
285int main(int argc, char ** argv)
286{
287#if defined(NCCL_ENABLED) && defined(MPI_ENABLED)
288  // Initialize MPI library (used to bootstrap NCCL)
289  HANDLE_MPI_ERROR(MPI_Init(&argc, &argv));
290  int procRank {-1};
291  HANDLE_MPI_ERROR(MPI_Comm_rank(MPI_COMM_WORLD, &procRank));
292  int numProcs {0};
293  HANDLE_MPI_ERROR(MPI_Comm_size(MPI_COMM_WORLD, &numProcs));
294  if (procRank != 0) verbose = false;
295  if (verbose)
296    std::cout << "Initialized MPI library (for NCCL bootstrap)\n";
297
298  // Assign a GPU to the process
299  int numDevices {0};
300  HANDLE_CUDA_ERROR(cudaGetDeviceCount(&numDevices));
301  const int deviceId = procRank % numDevices;
302  HANDLE_CUDA_ERROR(cudaSetDevice(deviceId));
303  if (verbose)
304    std::cout << "Set active device to GPU " << deviceId << "\n";
305
306  // Initialize NCCL communicator
307  // Step 1: Generate unique ID on rank 0 and broadcast to all ranks
308  ncclUniqueId ncclId;
309  if (procRank == 0) {
310    HANDLE_NCCL_ERROR(ncclGetUniqueId(&ncclId));
311  }
312  HANDLE_MPI_ERROR(MPI_Bcast(&ncclId, sizeof(ncclId), MPI_BYTE, 0, MPI_COMM_WORLD));
313
314  // Step 2: Initialize NCCL communicator with the shared unique ID
315  ncclComm_t ncclComm;
316  HANDLE_NCCL_ERROR(ncclCommInitRank(&ncclComm, numProcs, ncclId, procRank));
317  if (verbose)
318    std::cout << "Initialized NCCL communicator with " << numProcs << " ranks\n";
319
320  // Create a library handle
321  cudensitymatHandle_t handle;
322  HANDLE_CUDM_ERROR(cudensitymatCreate(&handle));
323  if (verbose)
324    std::cout << "Created a library handle\n";
325
326  // Reset distributed configuration with NCCL communicator
327  // The barrier buffer is now managed internally by cuDensityMat
328  HANDLE_CUDM_ERROR(cudensitymatResetDistributedConfiguration(handle,
329                      CUDENSITYMAT_DISTRIBUTED_PROVIDER_NCCL,
330                      &ncclComm, sizeof(ncclComm)));
331  if (verbose)
332    std::cout << "Configured distributed execution with NCCL\n";
333
334  // Run the example
335  exampleWorkflow(handle);
336
337  // Synchronize processes via NCCL barrier (uses allreduce internally)
338  HANDLE_CUDA_ERROR(cudaDeviceSynchronize());
339  HANDLE_MPI_ERROR(MPI_Barrier(MPI_COMM_WORLD));
340
341  // Destroy the library handle
342  HANDLE_CUDM_ERROR(cudensitymatDestroy(handle));
343  if (verbose)
344    std::cout << "Destroyed the library handle\n";
345
346  // Finalize NCCL communicator
347  HANDLE_NCCL_ERROR(ncclCommFinalize(ncclComm));
348  HANDLE_NCCL_ERROR(ncclCommDestroy(ncclComm));
349  if (verbose)
350    std::cout << "Finalized NCCL communicator\n";
351
352  HANDLE_CUDA_ERROR(cudaDeviceReset());
353
354  // Finalize the MPI library
355  HANDLE_MPI_ERROR(MPI_Finalize());
356  if (verbose)
357    std::cout << "Finalized MPI library\n";
358
359#else
360  // Fallback for when NCCL or MPI is not enabled
361  (void)argc;
362  (void)argv;
363  std::cerr << "This example requires both NCCL_ENABLED and MPI_ENABLED to be defined.\n";
364  std::cerr << "NCCL uses MPI for bootstrapping (sharing ncclUniqueId across processes).\n";
365  std::cerr << "Build with: -DENABLE_NCCL=TRUE -DENABLE_MPI=TRUE\n";
366  return 1;
367#endif
368
369  // Done
370  return 0;
371}

Code example (serial execution with backward differentiation)#

The following code example illustrates how to use the cuDensityMat library to not only compute the action of a quantum many-body operator on a quantum state, but also backward-differentiate it (compute gradients) with respect to user-provided real parameters parameterizing the operator (one real parameter Omega in this example). The full sample code can be found in the NVIDIA/cuQuantum repository (main serial gradient code and operator definition for gradient as well as the utility code).

First let’s construct a specific quantum many-body operator which, in this case, is a slightly modified version of the quantum many-body operator used in main serial code. Here we make both the h(t) and f(t) scalar coefficients depend on time and a single user-provided real parameter Omega via different (made-up) functional forms. In order to backward-differentiate the operator action with respect to this single user-provided real parameter Omega, we need to manually define a gradient callback function for each regular callback function we have (for h(t) and f(t) in this example). In our example, we define two CPU-side scalar gradient callback functions which compute the vector-jacobian product (VJP) of the scalar adjoint of h(t) and f(t) with respect to the user-provided real parameter Omega, respectively. A gradient callback function is expected to accumulate the VJP result into the paramsGrad output array, the final value of which will be the gradient(s) of the user-defined cost function with respect to the user-provided real parameters parameterizing the operator. As before, all regular and gradient callback functions used in our example explicitly expect the data type to be CUDA_C_64F (double-precision complex numbers).

  1/* Copyright (c) 2024-2026, NVIDIA CORPORATION & AFFILIATES.
  2 *
  3 * SPDX-License-Identifier: BSD-3-Clause
  4 */
  5
  6#pragma once
  7
  8#include <cudensitymat.h> // cuDensityMat library header
  9#include "helpers.h"      // GPU helper functions
 10
 11#include <cmath>
 12#include <complex>
 13#include <vector>
 14#include <iostream>
 15#include <cassert>
 16
 17
 18/* DESCRIPTION:
 19   Time-dependent transverse-field Ising Hamiltonian operator
 20   with ordered and fused ZZ terms, plus fused unitary dissipation terms:
 21    H = sum_{i} {h_i(t) * X_i}             // transverse field sum of X_i operators with time-dependent h_i(t) coefficients 
 22      + f(t) * sum_{i < j} {g_ij * ZZ_ij}  // modulated sum of the fused ordered {Z_i * Z_j} terms with static g_ij coefficients
 23      + d * sum_{i} {Y_i * {..} * Y_i}     // scaled sum of the dissipation terms {Y_i * {..} * Y_i} fused into the YY_ii super-operators
 24   where {..} is the placeholder for the density matrix to show that the Y_i operators act from different sides.
 25*/
 26
 27/** Define the numerical type and data type for the GPU computations (same) */
 28using NumericalType = std::complex<double>;      // do not change
 29constexpr cudaDataType_t dataType = CUDA_C_64F;  // do not change
 30
 31
 32/** Example of a user-provided scalar CPU callback C function
 33 *  defining a time-dependent coefficient h_i(t) inside the Hamiltonian:
 34 *  h_i(t) = exp(-Omega * t)
 35 */
 36extern "C"
 37int32_t hCoefComplex64(
 38  double time,             //in: time point
 39  int64_t batchSize,       //in: user-defined batch size (number of coefficients in the batch)
 40  int32_t numParams,       //in: number of external user-provided Hamiltonian parameters (this function expects one parameter, Omega)
 41  const double * params,   //in: params[0:numParams-1][0:batchSize-1]: GPU-accessible F-ordered array of user-provided Hamiltonian parameters for all instances of the batch
 42  cudaDataType_t dataType, //in: data type (expecting CUDA_C_64F in this specific callback function)
 43  void * scalarStorage,    //inout: CPU-accessible storage for the returned coefficient value(s) of shape [0:batchSize-1]
 44  cudaStream_t stream)     //in: CUDA stream (default is 0x0)
 45{
 46  if (dataType == CUDA_C_64F) {
 47    auto * tdCoef = static_cast<cuDoubleComplex *>(scalarStorage); // casting to cuDoubleComplex because this callback function expects CUDA_C_64F data type
 48    for (int64_t i = 0; i < batchSize; ++i) {
 49      const auto omega = params[i * numParams + 0]; // params[0][i]: 0-th parameter for i-th instance of the batch
 50      tdCoef[i] = make_cuDoubleComplex(std::exp((-omega) * time), 0.0); // value of the i-th instance of the coefficients batch
 51    }
 52  } else {
 53    return 1; // error code (1: Error)
 54  }
 55  return 0; // error code (0: Success)
 56}
 57
 58
 59/** User-provided gradient callback function for the user-provided
 60 *  scalar callback function with respect to its single parameter Omega.
 61 *  It accumulates a partial derivative 2*Re(dCost/dOmega) = 2*Re(dCost/dCoef * dCoef/dOmega),
 62 *  where:
 63 *  - Cost is some user-defined real scalar cost function,
 64 *  - dCost/dCoef is the adjoint of the cost function with respect to the coefficient (or their batch) associated with the callback function,
 65 *  - dCoef/dOmega is the gradient of the coefficient (or their batch) with respect to the parameter Omega:
 66 *    dCoef/dOmega = -time * exp(-Omega * time)
 67 */
 68extern "C"
 69int32_t hCoefGradComplex64(
 70  double time,             //in: time point
 71  int64_t batchSize,       //in: user-defined batch size (number of coefficients in the batch)
 72  int32_t numParams,       //in: number of external user-provided Hamiltonian parameters (this function expects one parameter, Omega)
 73  const double * params,   //in: params[0:numParams-1][0:batchSize-1]: GPU-accessible F-ordered array of user-provided Hamiltonian parameters for all instances of the batch
 74  cudaDataType_t dataType, //in: data type (expecting CUDA_C_64F in this specific callback function)
 75  void * scalarGrad,       //inout: CPU-accessible storage for the adjoint(s) of the coefficient(s) of shape [0:batchSize-1]
 76  double * paramsGrad,     //inout: params[0:numParams-1][0:batchSize-1]: GPU-accessible F-ordered array of the returned gradient(s) of the parameter(s)
 77  cudaStream_t stream)     //in: CUDA stream (default is 0x0)
 78{
 79  if (dataType == CUDA_C_64F) {
 80    const auto * tdCoefAdjoint = static_cast<const cuDoubleComplex *>(scalarGrad); // casting to cuDoubleComplex because this callback function expects CUDA_C_64F data type
 81    for (int64_t i = 0; i < batchSize; ++i) {
 82      const auto omega = params[i * numParams + 0]; // params[0][i]: 0-th parameter for i-th instance of the batch
 83      paramsGrad[i * numParams + 0] += // IMPORTANT: Accumulate the partial derivative for the i-th instance of the batch, not overwrite it!
 84        2.0 * cuCreal(cuCmul(tdCoefAdjoint[i], make_cuDoubleComplex(std::exp((-omega) * time) * (-time), 0.0)));
 85    }
 86  } else {
 87    return 1; // error code (1: Error)
 88  }
 89  return 0; // error code (0: Success)
 90}
 91
 92
 93/** Example of a user-provided scalar CPU callback C function
 94 *  defining a time-dependent coefficient f(t) inside the Hamiltonian:
 95 *  f(t) = exp(i * Omega * t) = cos(Omega * t) + i * sin(Omega * t)
 96 */
 97extern "C"
 98int32_t fCoefComplex64(
 99  double time,             //in: time point
100  int64_t batchSize,       //in: user-defined batch size (number of coefficients in the batch)
101  int32_t numParams,       //in: number of external user-provided Hamiltonian parameters (this function expects one parameter, Omega)
102  const double * params,   //in: params[0:numParams-1][0:batchSize-1]: GPU-accessible F-ordered array of user-provided Hamiltonian parameters for all instances of the batch
103  cudaDataType_t dataType, //in: data type (expecting CUDA_C_64F in this specific callback function)
104  void * scalarStorage,    //inout: CPU-accessible storage for the returned coefficient value(s) of shape [0:batchSize-1]
105  cudaStream_t stream)     //in: CUDA stream (default is 0x0)
106{
107  if (dataType == CUDA_C_64F) {
108    auto * tdCoef = static_cast<cuDoubleComplex *>(scalarStorage); // casting to cuDoubleComplex because this callback function expects CUDA_C_64F data type
109    for (int64_t i = 0; i < batchSize; ++i) {
110      const auto omega = params[i * numParams + 0]; // params[0][i]: 0-th parameter for i-th instance of the batch
111      tdCoef[i] = make_cuDoubleComplex(std::cos(omega * time), std::sin(omega * time)); // value of the i-th instance of the coefficients batch
112    }
113  } else {
114    return 1; // error code (1: Error)
115  }
116  return 0; // error code (0: Success)
117}
118
119
120/** User-provided gradient callback function for the user-provided
121 *  scalar callback function with respect to its single parameter Omega.
122 *  It accumulates a partial derivative 2*Re(dCost/dOmega) = 2*Re(dCost/dCoef * dCoef/dOmega),
123 *  where:
124 *  - Cost is some user-defined real scalar cost function,
125 *  - dCost/dCoef is the adjoint of the cost function with respect to the coefficient associated with the callback function,
126 *  - dCoef/dOmega is the gradient of the coefficient with respect to the parameter Omega:
127 *    dCoef/dOmega = -i * time * exp(i * Omega * time)
128 *                 = -i * time * (cos(Omega * time) + i * sin(Omega * time)) =
129 *                 = -time * sin(Omega * time) + i * time * cos(Omega * time)
130 */
131extern "C"
132int32_t fCoefGradComplex64(
133  double time,             //in: time point
134  int64_t batchSize,       //in: user-defined batch size (number of coefficients in the batch)
135  int32_t numParams,       //in: number of external user-provided Hamiltonian parameters (this function expects one parameter, Omega)
136  const double * params,   //in: params[0:numParams-1][0:batchSize-1]: GPU-accessible F-ordered array of user-provided Hamiltonian parameters for all instances of the batch
137  cudaDataType_t dataType, //in: data type (expecting CUDA_C_64F in this specific callback function)
138  void * scalarGrad,       //inout: CPU-accessible storage for the adjoint(s) of the coefficient(s) of shape [0:batchSize-1]
139  double * paramsGrad,     //inout: params[0:numParams-1][0:batchSize-1]: GPU-accessible F-ordered array of the returned gradient(s) of the parameter(s)
140  cudaStream_t stream)     //in: CUDA stream (default is 0x0)
141{
142  if (dataType == CUDA_C_64F) {
143    const auto * tdCoefAdjoint = static_cast<const cuDoubleComplex *>(scalarGrad); // casting to cuDoubleComplex because this callback function expects CUDA_C_64F data type
144    for (int64_t i = 0; i < batchSize; ++i) {
145      const auto omega = params[i * numParams + 0]; // params[0][i]: 0-th parameter for i-th instance of the batch
146      paramsGrad[i * numParams + 0] += // IMPORTANT: Accumulate the partial derivative for the i-th instance of the batch, not overwrite it!
147        2.0 * cuCreal(cuCmul(tdCoefAdjoint[i], make_cuDoubleComplex(-std::sin(omega * time) * time, std::cos(omega * time) * time)));
148    }
149  } else {
150    return 1; // error code (1: Error)
151  }
152  return 0; // error code (0: Success)
153}
154
155
156/** Convenience class which encapsulates a user-defined Liouvillian operator (system Hamiltonian + dissipation terms):
157 *  - Constructor constructs the desired Liouvillian operator (`cudensitymatOperator_t`)
158 *  - Method `get()` returns a reference to the constructed Liouvillian operator
159 *  - Destructor releases all resources used by the Liouvillian operator
160 */
161class UserDefinedLiouvillian final
162{
163private:
164  // Data members
165  cudensitymatHandle_t handle;             // library context handle
166  int64_t stateBatchSize;                  // quantum state batch size
167  const std::vector<int64_t> spaceShape;   // Hilbert space shape (extents of the modes of the composite Hilbert space)
168  void * spinXelems {nullptr};             // elements of the X spin operator in GPU RAM (F-order storage)
169  void * spinYYelems {nullptr};            // elements of the fused YY two-spin operator in GPU RAM (F-order storage)
170  void * spinZZelems {nullptr};            // elements of the fused ZZ two-spin operator in GPU RAM (F-order storage)
171  cudensitymatElementaryOperator_t spinX;  // X spin operator (elementary tensor operator)
172  cudensitymatElementaryOperator_t spinYY; // fused YY two-spin operator (elementary tensor operator)
173  cudensitymatElementaryOperator_t spinZZ; // fused ZZ two-spin operator (elementary tensor operator)
174  cudensitymatOperatorTerm_t oneBodyTerm;  // operator term: H1 = sum_{i} {h_i(t) * X_i} (one-body term)
175  cudensitymatOperatorTerm_t twoBodyTerm;  // operator term: H2 = f(t) * sum_{i < j} {g_ij * ZZ_ij} (two-body term)
176  cudensitymatOperatorTerm_t noiseTerm;    // operator term: D1 = d * sum_{i} {YY_ii}  // Y_i operators act from different sides on the density matrix (two-body mixed term)
177  cudensitymatOperator_t liouvillian;      // full operator: (-i * (H1 + H2) * {..}) + (i * {..} * (H1 + H2)) + D1{..} (super-operator)
178
179public:
180
181  // Constructor constructs a user-defined Liouvillian operator
182  UserDefinedLiouvillian(cudensitymatHandle_t contextHandle,             // library context handle
183                         const std::vector<int64_t> & hilbertSpaceShape, // Hilbert space shape
184                         int64_t batchSize):                             // batch size for the quantum state
185    handle(contextHandle), stateBatchSize(batchSize), spaceShape(hilbertSpaceShape)
186  {
187    // Define the necessary operator tensors in GPU memory (F-order storage!)
188    spinXelems = createInitializeArrayGPU<NumericalType>(  // X[i0; j0]
189                  {{0.0, 0.0}, {1.0, 0.0},   // 1st column of matrix X
190                   {1.0, 0.0}, {0.0, 0.0}}); // 2nd column of matrix X
191
192    spinYYelems = createInitializeArrayGPU<NumericalType>(  // YY[i0, i1; j0, j1] := Y[i0; j0] * Y[i1; j1]
193                    {{0.0, 0.0},  {0.0, 0.0}, {0.0, 0.0}, {-1.0, 0.0},  // 1st column of matrix YY
194                     {0.0, 0.0},  {0.0, 0.0}, {1.0, 0.0}, {0.0, 0.0},   // 2nd column of matrix YY
195                     {0.0, 0.0},  {1.0, 0.0}, {0.0, 0.0}, {0.0, 0.0},   // 3rd column of matrix YY
196                     {-1.0, 0.0}, {0.0, 0.0}, {0.0, 0.0}, {0.0, 0.0}}); // 4th column of matrix YY
197
198    spinZZelems = createInitializeArrayGPU<NumericalType>(  // ZZ[i0, i1; j0, j1] := Z[i0; j0] * Z[i1; j1]
199                    {{1.0, 0.0}, {0.0, 0.0},  {0.0, 0.0},  {0.0, 0.0},   // 1st column of matrix ZZ
200                     {0.0, 0.0}, {-1.0, 0.0}, {0.0, 0.0},  {0.0, 0.0},   // 2nd column of matrix ZZ
201                     {0.0, 0.0}, {0.0, 0.0},  {-1.0, 0.0}, {0.0, 0.0},   // 3rd column of matrix ZZ
202                     {0.0, 0.0}, {0.0, 0.0},  {0.0, 0.0},  {1.0, 0.0}}); // 4th column of matrix ZZ
203
204    // Construct the necessary Elementary Tensor Operators
205    //  X_i operator
206    HANDLE_CUDM_ERROR(cudensitymatCreateElementaryOperator(handle,
207                        1,                                   // one-body operator
208                        std::vector<int64_t>({2}).data(),    // acts in tensor space of shape {2}
209                        CUDENSITYMAT_OPERATOR_SPARSITY_NONE, // dense tensor storage
210                        0,                                   // 0 for dense tensors
211                        nullptr,                             // nullptr for dense tensors
212                        dataType,                            // data type
213                        spinXelems,                          // tensor elements in GPU memory
214                        cudensitymatTensorCallbackNone,      // no tensor callback function (tensor is not time-dependent)
215                        cudensitymatTensorGradientCallbackNone, // no tensor gradient callback function
216                        &spinX));                            // the created elementary tensor operator
217    //  ZZ_ij = Z_i * Z_j fused operator
218    HANDLE_CUDM_ERROR(cudensitymatCreateElementaryOperator(handle,
219                        2,                                   // two-body operator
220                        std::vector<int64_t>({2,2}).data(),  // acts in tensor space of shape {2,2}
221                        CUDENSITYMAT_OPERATOR_SPARSITY_NONE, // dense tensor storage
222                        0,                                   // 0 for dense tensors
223                        nullptr,                             // nullptr for dense tensors
224                        dataType,                            // data type
225                        spinZZelems,                         // tensor elements in GPU memory
226                        cudensitymatTensorCallbackNone,      // no tensor callback function (tensor is not time-dependent)
227                        cudensitymatTensorGradientCallbackNone, // no tensor gradient callback function
228                        &spinZZ));                           // the created elementary tensor operator
229    //  YY_ii = Y_i * {..} * Y_i fused operator (note action from different sides)
230    HANDLE_CUDM_ERROR(cudensitymatCreateElementaryOperator(handle,
231                        2,                                   // two-body operator
232                        std::vector<int64_t>({2,2}).data(),  // acts in tensor space of shape {2,2}
233                        CUDENSITYMAT_OPERATOR_SPARSITY_NONE, // dense tensor storage
234                        0,                                   // 0 for dense tensors
235                        nullptr,                             // nullptr for dense tensors
236                        dataType,                            // data type
237                        spinYYelems,                         // tensor elements in GPU memory
238                        cudensitymatTensorCallbackNone,      // no tensor callback function (tensor is not time-dependent)
239                        cudensitymatTensorGradientCallbackNone, // no tensor gradient callback function
240                        &spinYY));                           // the created elementary tensor operator
241
242    // Construct the necessary Operator Terms from tensor products of Elementary Tensor Operators
243    //  Create an empty operator term
244    HANDLE_CUDM_ERROR(cudensitymatCreateOperatorTerm(handle,
245                        spaceShape.size(),                   // Hilbert space rank (number of modes)
246                        spaceShape.data(),                   // Hilbert space shape (mode extents)
247                        &oneBodyTerm));                      // the created empty operator term
248    //  Define the operator term: H1 = sum_{i} {h_i(t) * X_i}
249    for (int32_t i = 0; i < spaceShape.size(); ++i) {
250      HANDLE_CUDM_ERROR(cudensitymatOperatorTermAppendElementaryProduct(handle,
251                          oneBodyTerm,
252                          1,                                                             // number of elementary tensor operators in the product
253                          std::vector<cudensitymatElementaryOperator_t>({spinX}).data(), // elementary tensor operators forming the product
254                          std::vector<int32_t>({i}).data(),                              // space modes acted on by the operator product
255                          std::vector<int32_t>({0}).data(),                              // space mode action duality (0: from the left; 1: from the right)
256                          make_cuDoubleComplex(1.0, 0.0),                                // static coefficient part: Always 64-bit-precision complex number
257                          {hCoefComplex64, CUDENSITYMAT_CALLBACK_DEVICE_CPU, nullptr},   // CPU scalar callback function defining the time-dependent coefficient associated with this operator product
258                          {hCoefGradComplex64, CUDENSITYMAT_CALLBACK_DEVICE_CPU, nullptr,
259                           CUDENSITYMAT_DIFFERENTIATION_DIR_BACKWARD})); // CPU scalar gradient callback function defining the gradient of the coefficient with respect to the parameter Omega
260    }
261    //  Create an empty operator term
262    HANDLE_CUDM_ERROR(cudensitymatCreateOperatorTerm(handle,
263                        spaceShape.size(),                   // Hilbert space rank (number of modes)
264                        spaceShape.data(),                   // Hilbert space shape (mode extents)
265                        &twoBodyTerm));                      // the created empty operator term
266    //  Define the operator term: H2 = f(t) * sum_{i < j} {g_ij * ZZ_ij}
267    for (int32_t i = 0; i < spaceShape.size() - 1; ++i) {
268      for (int32_t j = (i + 1); j < spaceShape.size(); ++j) {
269        const double g_ij = -1.0 / static_cast<double>(i + j + 1); // assign some value to the time-independent g_ij coefficient
270        HANDLE_CUDM_ERROR(cudensitymatOperatorTermAppendElementaryProduct(handle,
271                            twoBodyTerm,
272                            1,                                                              // number of elementary tensor operators in the product
273                            std::vector<cudensitymatElementaryOperator_t>({spinZZ}).data(), // elementary tensor operators forming the product
274                            std::vector<int32_t>({i, j}).data(),                            // space modes acted on by the operator product
275                            std::vector<int32_t>({0, 0}).data(),                            // space mode action duality (0: from the left; 1: from the right)
276                            make_cuDoubleComplex(g_ij, 0.0),                                // g_ij static coefficient: Always 64-bit-precision complex number
277                            cudensitymatScalarCallbackNone,                                 // no time-dependent coefficient associated with this operator product
278                            cudensitymatScalarGradientCallbackNone));                       // no coefficient gradient associated with this operator product
279      }
280    }
281    //  Create an empty operator term
282    HANDLE_CUDM_ERROR(cudensitymatCreateOperatorTerm(handle,
283                        spaceShape.size(),                   // Hilbert space rank (number of modes)
284                        spaceShape.data(),                   // Hilbert space shape (mode extents)
285                        &noiseTerm));                        // the created empty operator term
286    //  Define the operator term: D1 = d * sum_{i} {YY_ii}
287    for (int32_t i = 0; i < spaceShape.size(); ++i) {
288      HANDLE_CUDM_ERROR(cudensitymatOperatorTermAppendElementaryProduct(handle,
289                          noiseTerm,
290                          1,                                                              // number of elementary tensor operators in the product
291                          std::vector<cudensitymatElementaryOperator_t>({spinYY}).data(), // elementary tensor operators forming the product
292                          std::vector<int32_t>({i, i}).data(),                            // space modes acted on by the operator product (from different sides)
293                          std::vector<int32_t>({0, 1}).data(),                            // space mode action duality (0: from the left; 1: from the right)
294                          make_cuDoubleComplex(1.0, 0.0),                                 // default coefficient: Always 64-bit-precision complex number
295                          cudensitymatScalarCallbackNone,                                 // no time-dependent coefficient associated with this operator product
296                          cudensitymatScalarGradientCallbackNone));                       // no coefficient gradient associated with this operator product
297    }
298
299    // Construct the full Liouvillian operator as a sum of the created operator terms
300    //  Create an empty operator (super-operator)
301    HANDLE_CUDM_ERROR(cudensitymatCreateOperator(handle,
302                        spaceShape.size(),                // Hilbert space rank (number of modes)
303                        spaceShape.data(),                // Hilbert space shape (modes extents)
304                        &liouvillian));                   // the created empty operator (super-operator)
305    //  Append an operator term to the operator (super-operator)
306    HANDLE_CUDM_ERROR(cudensitymatOperatorAppendTerm(handle,
307                        liouvillian,
308                        oneBodyTerm,                      // appended operator term
309                        0,                                // operator term action duality as a whole (0: acting from the left; 1: acting from the right)
310                        make_cuDoubleComplex(0.0, -1.0),  // -i constant
311                        cudensitymatScalarCallbackNone,   // no time-dependent coefficient associated with the operator term as a whole
312                        cudensitymatScalarGradientCallbackNone)); // no coefficient gradient associated with the operator term as a whole
313    //  Append an operator term to the operator (super-operator)
314    HANDLE_CUDM_ERROR(cudensitymatOperatorAppendTerm(handle,
315                        liouvillian,
316                        twoBodyTerm,                      // appended operator term
317                        0,                                // operator term action duality as a whole (0: acting from the left; 1: acting from the right)
318                        make_cuDoubleComplex(0.0, -1.0),  // -i constant
319                        {fCoefComplex64, CUDENSITYMAT_CALLBACK_DEVICE_CPU, nullptr}, // CPU scalar callback function defining the time-dependent coefficient associated with this operator term as a whole
320                        {fCoefGradComplex64, CUDENSITYMAT_CALLBACK_DEVICE_CPU, nullptr,
321                         CUDENSITYMAT_DIFFERENTIATION_DIR_BACKWARD})); // CPU scalar gradient callback function defining the gradient of the coefficient with respect to the parameter Omega
322    //  Append an operator term to the operator (super-operator)
323    HANDLE_CUDM_ERROR(cudensitymatOperatorAppendTerm(handle,
324                        liouvillian,
325                        oneBodyTerm,                      // appended operator term
326                        1,                                // operator term action duality as a whole (0: acting from the left; 1: acting from the right)
327                        make_cuDoubleComplex(0.0, +1.0),  // +i constant
328                        cudensitymatScalarCallbackNone,   // no time-dependent coefficient associated with the operator term as a whole
329                        cudensitymatScalarGradientCallbackNone)); // no coefficient gradient associated with the operator term as a whole
330    //  Append an operator term to the operator (super-operator)
331    HANDLE_CUDM_ERROR(cudensitymatOperatorAppendTerm(handle,
332                        liouvillian,
333                        twoBodyTerm,                      // appended operator term
334                        1,                                // operator term action duality as a whole (0: acting from the left; 1: acting from the right)
335                        make_cuDoubleComplex(0.0, 1.0),   // +i constant
336                        {fCoefComplex64, CUDENSITYMAT_CALLBACK_DEVICE_CPU, nullptr}, // CPU scalar callback function defining the time-dependent coefficient associated with this operator term as a whole
337                        {fCoefGradComplex64, CUDENSITYMAT_CALLBACK_DEVICE_CPU, nullptr,
338                         CUDENSITYMAT_DIFFERENTIATION_DIR_BACKWARD})); // CPU scalar gradient callback function defining the gradient of the coefficient with respect to the parameter Omega
339    //  Append an operator term to the operator (super-operator)
340    const double d = 1.0; // assign some value to the time-independent coefficient
341    HANDLE_CUDM_ERROR(cudensitymatOperatorAppendTerm(handle,
342                        liouvillian,
343                        noiseTerm,                        // appended operator term
344                        0,                                // operator term action duality as a whole (no duality reversing in this case)
345                        make_cuDoubleComplex(d, 0.0),     // static coefficient associated with the operator term as a whole
346                        cudensitymatScalarCallbackNone,   // no time-dependent coefficient associated with the operator term as a whole
347                        cudensitymatScalarGradientCallbackNone)); // no coefficient gradient associated with the operator term as a whole
348  }
349
350  // Destructor destructs the user-defined Liouvillian operator
351  ~UserDefinedLiouvillian()
352  {
353    // Destroy the Liouvillian operator
354    HANDLE_CUDM_ERROR(cudensitymatDestroyOperator(liouvillian));
355
356    // Destroy operator terms
357    HANDLE_CUDM_ERROR(cudensitymatDestroyOperatorTerm(noiseTerm));
358    HANDLE_CUDM_ERROR(cudensitymatDestroyOperatorTerm(twoBodyTerm));
359    HANDLE_CUDM_ERROR(cudensitymatDestroyOperatorTerm(oneBodyTerm));
360
361    // Destroy elementary tensor operators
362    HANDLE_CUDM_ERROR(cudensitymatDestroyElementaryOperator(spinYY));
363    HANDLE_CUDM_ERROR(cudensitymatDestroyElementaryOperator(spinZZ));
364    HANDLE_CUDM_ERROR(cudensitymatDestroyElementaryOperator(spinX));
365
366    // Destroy operator tensors
367    destroyArrayGPU(spinYYelems);
368    destroyArrayGPU(spinZZelems);
369    destroyArrayGPU(spinXelems);
370  }
371
372  // Disable copy constructor/assignment (GPU resources are private, no deep copy)
373  UserDefinedLiouvillian(const UserDefinedLiouvillian &) = delete;
374  UserDefinedLiouvillian & operator=(const UserDefinedLiouvillian &) = delete;
375  UserDefinedLiouvillian(UserDefinedLiouvillian &&) = delete;
376  UserDefinedLiouvillian & operator=(UserDefinedLiouvillian &&) = delete;
377
378  /** Returns the number of externally provided Hamiltonian parameters. */
379  int32_t getNumParameters() const
380  {
381    return 1; // one parameter Omega
382  }
383
384  /** Get access to the constructed Liouvillian operator. */
385  cudensitymatOperator_t & get()
386  {
387    return liouvillian;
388  }
389
390};

Now we can use the defined quantum many-body operator in our main code to compute its action on a mixed quantum state and then backward-differentiate it (compute gradients) with respect to the user-provided real parameter Omega. For the sake of simplicity, we pass a made-up adjoint of the output quantum state to the cudensitymatOperatorComputeActionBackwardDiff() call, which is just the output quantum state itself (in real scenarios, the adjoint of the output quantum state will depend on the user-chosen cost function and will be provided by the user). Upon completion of the cudensitymatOperatorComputeActionBackwardDiff() call, the paramsGrad output argument will contain the gradient of the user-defined cost function with respect to the user-provided real parameter Omega. Additionally, the backward-differentiation API call will also return the adjoint of the input quantum state for cases where the input quantum state implicitly depends on the user-provided real parameters (for example, cases where the input quantum state comes from a previous operator action step, which is typical for time-integration of quantum dynamics master equations). Note that both output arguments, namely paramsGrad and stateInAdj, are accumulative, i.e., they will be accumulated into (it is user’s responsibility to zero them out before the first call!).

  1/* Copyright (c) 2024-2026, NVIDIA CORPORATION & AFFILIATES.
  2 *
  3 * SPDX-License-Identifier: BSD-3-Clause
  4 */
  5
  6#include <cudensitymat.h>  // cuDensityMat library header
  7#include "helpers.h"       // helper functions
  8
  9
 10// Transverse Ising Hamiltonian with double summation ordering
 11// and spin-operator fusion, plus fused dissipation terms
 12#include "transverse_ising_full_fused_noisy_grad.h" // user-defined Liouvillian operator example
 13
 14#include <cmath>
 15#include <complex>
 16#include <vector>
 17#include <chrono>
 18#include <iostream>
 19#include <cassert>
 20
 21
 22// Number of times to perform operator action on a quantum state
 23constexpr int NUM_REPEATS = 2;
 24
 25// Logging verbosity
 26bool verbose = true;
 27
 28
 29// Example workflow
 30void exampleWorkflow(cudensitymatHandle_t handle)
 31{
 32  // Define the composite Hilbert space shape and
 33  // quantum state batch size (number of individual quantum states in a batched simulation)
 34  const std::vector<int64_t> spaceShape({2,2,2,2}); // dimensions of quantum degrees of freedom
 35  const int64_t batchSize = 1;                      // number of quantum states per batch
 36
 37  if (verbose) {
 38    std::cout << "Hilbert space rank = " << spaceShape.size() << "; Shape = (";
 39    for (const auto & dimsn: spaceShape)
 40      std::cout << dimsn << ",";
 41    std::cout << ")" << std::endl;
 42    std::cout << "Quantum state batch size = " << batchSize << std::endl;
 43  }
 44
 45  // Construct a user-defined Liouvillian operator using a convenience C++ class
 46  UserDefinedLiouvillian liouvillian(handle, spaceShape, batchSize);
 47  if (verbose)
 48    std::cout << "Constructed the Liouvillian operator\n";
 49
 50  // Set and place external user-provided Hamiltonian parameters in GPU memory
 51  const int32_t numParams = liouvillian.getNumParameters(); // number of external user-provided Hamiltonian parameters
 52  if (verbose)
 53    std::cout << "Number of external user-provided Hamiltonian parameters = " << numParams << std::endl;
 54  std::vector<double> cpuHamParams(numParams * batchSize);
 55  for (int64_t j = 0; j < batchSize; ++j) {
 56    for (int32_t i = 0; i < numParams; ++i) {
 57      cpuHamParams[j * numParams + i] = double(i+1) / double(j+1); // just setting some parameter values for each instance of the batch
 58    }
 59  }
 60  auto * hamiltonianParams = static_cast<double *>(createInitializeArrayGPU(cpuHamParams));
 61  if (verbose)
 62    std::cout << "Created an array of external user-provided Hamiltonian parameters in GPU memory\n";
 63
 64  // Create an array of gradients for the user-provided Hamiltonian parameters in GPU memory
 65  std::vector<double> cpuHamParamsGrad(numParams * batchSize, 0.0);
 66  auto * hamiltonianParamsGrad = static_cast<double *>(createInitializeArrayGPU(cpuHamParamsGrad));
 67  if (verbose)
 68    std::cout << "Created an array of gradients for the external user-provided Hamiltonian parameters in GPU memory\n";
 69
 70  // Declare the input quantum state
 71  cudensitymatState_t inputState;
 72  HANDLE_CUDM_ERROR(cudensitymatCreateState(handle,
 73                      CUDENSITYMAT_STATE_PURITY_MIXED,  // pure (state vector) or mixed (density matrix) state
 74                      spaceShape.size(),
 75                      spaceShape.data(),
 76                      batchSize,
 77                      dataType,
 78                      &inputState));
 79
 80  // Query the size of the quantum state storage
 81  std::size_t storageSize {0}; // only one storage component (tensor) is needed (no tensor factorization)
 82  HANDLE_CUDM_ERROR(cudensitymatStateGetComponentStorageSize(handle,
 83                      inputState,
 84                      1,               // only one storage component (tensor)
 85                      &storageSize));  // storage size in bytes
 86  const std::size_t stateVolume = storageSize / sizeof(NumericalType);  // quantum state tensor volume (number of elements)
 87  if (verbose)
 88    std::cout << "Quantum state storage size (bytes) = " << storageSize << std::endl;
 89
 90  // Prepare some initial value for the input quantum state batch
 91  std::vector<NumericalType> inputStateValue(stateVolume);
 92  if constexpr (std::is_same_v<NumericalType, float>) {
 93    for (std::size_t i = 0; i < stateVolume; ++i) {
 94      inputStateValue[i] = 1.0f / float(i+1); // just some value
 95    }
 96  } else if constexpr (std::is_same_v<NumericalType, double>) {
 97    for (std::size_t i = 0; i < stateVolume; ++i) {
 98      inputStateValue[i] = 1.0 / double(i+1); // just some value
 99    }
100  } else if constexpr (std::is_same_v<NumericalType, std::complex<float>>) {
101    for (std::size_t i = 0; i < stateVolume; ++i) {
102      inputStateValue[i] = NumericalType{1.0f / float(i+1), -1.0f / float(i+2)}; // just some value
103    }
104  } else if constexpr (std::is_same_v<NumericalType, std::complex<double>>) {
105    for (std::size_t i = 0; i < stateVolume; ++i) {
106      inputStateValue[i] = NumericalType{1.0 / double(i+1), -1.0 / double(i+2)}; // just some value
107    }
108  } else {
109    std::cerr << "Error: Unsupported data type!\n";
110    std::exit(1);
111  }
112  // Allocate initialized GPU storage for the input quantum state with prepared values
113  auto * inputStateElems = createInitializeArrayGPU(inputStateValue);
114  if (verbose)
115    std::cout << "Allocated input quantum state storage and initialized it to some value\n";
116
117  // Attach initialized GPU storage to the input quantum state
118  HANDLE_CUDM_ERROR(cudensitymatStateAttachComponentStorage(handle,
119                      inputState,
120                      1,                                                 // only one storage component (tensor)
121                      std::vector<void*>({inputStateElems}).data(),      // pointer to the GPU storage for the quantum state
122                      std::vector<std::size_t>({storageSize}).data()));  // size of the GPU storage for the quantum state
123  if (verbose)
124    std::cout << "Constructed input quantum state\n";
125
126  // Declare the output quantum state of the same shape
127  cudensitymatState_t outputState;
128  HANDLE_CUDM_ERROR(cudensitymatCreateState(handle,
129                      CUDENSITYMAT_STATE_PURITY_MIXED,  // pure (state vector) or mixed (density matrix) state
130                      spaceShape.size(),
131                      spaceShape.data(),
132                      batchSize,
133                      dataType,
134                      &outputState));
135
136  // Allocate GPU storage for the output quantum state
137  auto * outputStateElems = createArrayGPU<NumericalType>(stateVolume);
138  if (verbose)
139    std::cout << "Allocated output quantum state storage\n";
140
141  // Attach GPU storage to the output quantum state
142  HANDLE_CUDM_ERROR(cudensitymatStateAttachComponentStorage(handle,
143                      outputState,
144                      1,                                                 // only one storage component (tensor)
145                      std::vector<void*>({outputStateElems}).data(),     // pointer to the GPU storage for the quantum state
146                      std::vector<std::size_t>({storageSize}).data()));  // size of the GPU storage for the quantum state
147  if (verbose)
148    std::cout << "Constructed output quantum state\n";
149
150  // Declare the adjoint input quantum state of the same shape
151  cudensitymatState_t inputStateAdj;
152  HANDLE_CUDM_ERROR(cudensitymatCreateState(handle,
153                      CUDENSITYMAT_STATE_PURITY_MIXED,  // pure (state vector) or mixed (density matrix) state
154                      spaceShape.size(),
155                      spaceShape.data(),
156                      batchSize,
157                      dataType,  // data type must match
158                      &inputStateAdj));
159
160  // Allocate GPU storage for the adjoint input quantum state
161  auto * inputStateAdjElems = createArrayGPU<NumericalType>(stateVolume);
162  if (verbose)
163    std::cout << "Allocated adjoint input quantum state storage\n";
164
165  // Attach GPU storage to the adjoint input quantum state
166  HANDLE_CUDM_ERROR(cudensitymatStateAttachComponentStorage(handle,
167                      inputStateAdj,
168                      1,                                                 // only one storage component (tensor)
169                      std::vector<void*>({inputStateAdjElems}).data(),   // pointer to the GPU storage for the quantum state
170                      std::vector<std::size_t>({storageSize}).data()));  // size of the GPU storage for the quantum state
171  if (verbose)
172    std::cout << "Constructed adjoint input quantum state\n";
173
174  // Declare a workspace descriptor
175  cudensitymatWorkspaceDescriptor_t workspaceDescr;
176  HANDLE_CUDM_ERROR(cudensitymatCreateWorkspace(handle, &workspaceDescr));
177
178  // Query free GPU memory
179  std::size_t freeMem = 0, totalMem = 0;
180  HANDLE_CUDA_ERROR(cudaMemGetInfo(&freeMem, &totalMem));
181  freeMem = static_cast<std::size_t>(static_cast<double>(freeMem) * 0.95); // take 95% of the free memory as the workspace budget
182  if (verbose)
183    std::cout << "Max workspace buffer size (bytes) = " << freeMem << std::endl;
184
185  // Prepare the Liouvillian operator action on a quantum state (needs to be done only once)
186  auto startTime = std::chrono::high_resolution_clock::now();
187  HANDLE_CUDM_ERROR(cudensitymatOperatorPrepareAction(handle,
188                      liouvillian.get(),
189                      inputState,
190                      outputState,
191                      CUDENSITYMAT_COMPUTE_64F,  // GPU compute type
192                      freeMem,                   // max available GPU free memory for the workspace
193                      workspaceDescr,            // workspace descriptor
194                      0x0));                     // default CUDA stream
195  auto finishTime = std::chrono::high_resolution_clock::now();
196  std::chrono::duration<double> timeSec = finishTime - startTime;
197  if (verbose)
198    std::cout << "Operator action preparation time (sec) = " << timeSec.count() << std::endl;
199
200  // Query the required workspace buffer size (bytes)
201  std::size_t requiredBufferSize {0};
202  HANDLE_CUDM_ERROR(cudensitymatWorkspaceGetMemorySize(handle,
203                      workspaceDescr,
204                      CUDENSITYMAT_MEMSPACE_DEVICE,
205                      CUDENSITYMAT_WORKSPACE_SCRATCH,
206                      &requiredBufferSize));
207  if (verbose)
208    std::cout << "Required workspace buffer size (bytes) = " << requiredBufferSize << std::endl;
209
210  if (requiredBufferSize > freeMem) {
211    std::cerr << "Error: Required workspace buffer size is greater than the available GPU free memory!\n";
212    std::exit(1);
213  }
214
215  // Allocate GPU storage for the workspace buffer
216  std::size_t workspaceBufferSize = requiredBufferSize;
217  void * workspaceBuffer = createArrayGPU<NumericalType>(workspaceBufferSize / sizeof(NumericalType));
218  if (verbose)
219    std::cout << "Allocated workspace buffer of size (bytes) = " << workspaceBufferSize << std::endl;
220
221  // Attach the workspace buffer to the workspace descriptor
222  HANDLE_CUDM_ERROR(cudensitymatWorkspaceSetMemory(handle,
223                      workspaceDescr,
224                      CUDENSITYMAT_MEMSPACE_DEVICE,
225                      CUDENSITYMAT_WORKSPACE_SCRATCH,
226                      workspaceBuffer,
227                      workspaceBufferSize));
228  if (verbose)
229    std::cout << "Attached workspace buffer of size (bytes) = " << workspaceBufferSize << std::endl;
230
231  // Apply the Liouvillian operator to the input quatum state
232  // and accumulate its action into the output quantum state (note the accumulative += semantics)
233  for (int32_t repeat = 0; repeat < NUM_REPEATS; ++repeat) { // repeat multiple times for accurate timing
234    // Zero out the output quantum state
235    HANDLE_CUDM_ERROR(cudensitymatStateInitializeZero(handle,
236                        outputState,
237                        0x0));
238    if (verbose)
239      std::cout << "Initialized the output quantum state to zero\n";
240    HANDLE_CUDA_ERROR(cudaDeviceSynchronize());
241    startTime = std::chrono::high_resolution_clock::now();
242    HANDLE_CUDM_ERROR(cudensitymatOperatorComputeAction(handle,
243                        liouvillian.get(),
244                        0.3,                // time point (some value)
245                        batchSize,          // user-defined batch size
246                        numParams,          // number of external user-defined Hamiltonian parameters
247                        hamiltonianParams,  // external Hamiltonian parameters in GPU memory
248                        inputState,         // input quantum state
249                        outputState,        // output quantum state
250                        workspaceDescr,     // workspace descriptor
251                        0x0));              // default CUDA stream
252    HANDLE_CUDA_ERROR(cudaDeviceSynchronize());
253    finishTime = std::chrono::high_resolution_clock::now();
254    timeSec = finishTime - startTime;
255    if (verbose)
256      std::cout << "Operator action computation time (sec) = " << timeSec.count() << std::endl;
257  }
258
259  // Compute the squared norm of the output quantum state
260  void * norm2 = createInitializeArrayGPU(std::vector<double>(batchSize, 0.0));
261  HANDLE_CUDM_ERROR(cudensitymatStateComputeNorm(handle,
262                      outputState,
263                      norm2,
264                      0x0));
265  if (verbose) {
266    std::cout << "Computed the output quantum state norm:\n";
267    printArrayGPU<double>(norm2, batchSize);
268  }
269
270  HANDLE_CUDA_ERROR(cudaDeviceSynchronize());
271
272  // Prepare the Liouvillian operator action backward differentiation (needs to be done only once)
273  startTime = std::chrono::high_resolution_clock::now();
274  HANDLE_CUDM_ERROR(cudensitymatOperatorPrepareActionBackwardDiff(handle,
275                      liouvillian.get(),
276                      inputState,
277                      outputState,               // adjoint output quantum state is always congruent to the output quantum state
278                      CUDENSITYMAT_COMPUTE_64F,  // GPU compute type
279                      freeMem,                   // max available GPU free memory for the workspace buffer
280                      workspaceDescr,            // workspace descriptor
281                      0x0));                     // default CUDA stream
282  finishTime = std::chrono::high_resolution_clock::now();
283  timeSec = finishTime - startTime;
284  if (verbose)
285    std::cout << "Operator action backward differentiation preparation time (sec) = " << timeSec.count() << std::endl;
286
287  // Query the required workspace buffer size (bytes)
288  requiredBufferSize = 0;
289  HANDLE_CUDM_ERROR(cudensitymatWorkspaceGetMemorySize(handle,
290                      workspaceDescr,
291                      CUDENSITYMAT_MEMSPACE_DEVICE,
292                      CUDENSITYMAT_WORKSPACE_SCRATCH,
293                      &requiredBufferSize));
294  if (verbose)
295    std::cout << "Required workspace buffer size (bytes) = " << requiredBufferSize << std::endl;
296
297  if (requiredBufferSize > freeMem) {
298    std::cerr << "Error: Required workspace buffer size is greater than the available GPU free memory!\n";
299    std::exit(1);
300  }
301
302  // Reallocate the workspace buffer if the backward pass requires more memory
303  if (requiredBufferSize > workspaceBufferSize) {
304    destroyArrayGPU(workspaceBuffer);
305    workspaceBufferSize = requiredBufferSize;
306    workspaceBuffer = createArrayGPU<NumericalType>(workspaceBufferSize / sizeof(NumericalType));
307    if (verbose)
308      std::cout << "Re-allocated workspace buffer of size (bytes) = " << workspaceBufferSize << std::endl;
309  }
310
311  // Attach the workspace buffer to the workspace descriptor
312  HANDLE_CUDM_ERROR(cudensitymatWorkspaceSetMemory(handle,
313                      workspaceDescr,
314                      CUDENSITYMAT_MEMSPACE_DEVICE,
315                      CUDENSITYMAT_WORKSPACE_SCRATCH,
316                      workspaceBuffer,
317                      requiredBufferSize));
318  if (verbose)
319    std::cout << "Attached workspace buffer of size (bytes) = " << requiredBufferSize << std::endl;
320
321  // Liouvillian operator action backward differentiation:
322  // The adjoint output quantum state, which is always congruent to the output quantum state,
323  // depends on the user-defined cost function, so here we simply pass the previously computed output quantum state.
324  // In real-life applications, the user will pass their adjoint output quantum state, computed for their cost function.
325  for (int32_t repeat = 0; repeat < NUM_REPEATS; ++repeat) { // repeat multiple times for accurate timing
326    // Zero out the adjoint input quantum state and gradients
327    HANDLE_CUDM_ERROR(cudensitymatStateInitializeZero(handle,
328                        inputStateAdj,
329                        0x0));
330    initializeArrayGPU(std::vector<double>(numParams * batchSize, 0.0), hamiltonianParamsGrad);
331    if (verbose)
332      std::cout << "Initialized the adjoint input quantum state and gradients to zero\n";
333    HANDLE_CUDA_ERROR(cudaDeviceSynchronize());
334    startTime = std::chrono::high_resolution_clock::now();
335    HANDLE_CUDM_ERROR(cudensitymatOperatorComputeActionBackwardDiff(handle,
336                        liouvillian.get(),
337                        0.3,                    // time point (some value)
338                        batchSize,              // user-defined batch size
339                        numParams,              // number of external user-defined Hamiltonian parameters
340                        hamiltonianParams,      // external Hamiltonian parameters in GPU memory
341                        inputState,             // input quantum state
342                        outputState,            // adjoint output quantum state (here we just pass the previously computed output quantum state for simplicity)
343                        inputStateAdj,          // adjoint input quantum state
344                        hamiltonianParamsGrad,  // partial gradients with respect to the user-defined real parameters
345                        workspaceDescr,         // workspace descriptor
346                        0x0));                  // default CUDA stream
347    HANDLE_CUDA_ERROR(cudaDeviceSynchronize());
348    finishTime = std::chrono::high_resolution_clock::now();
349    timeSec = finishTime - startTime;
350    if (verbose)
351      std::cout << "Operator action backward differentiation computation time (sec) = " << timeSec.count() << std::endl;
352  }
353
354  // Compute the squared norm of the adjoint input quantum state
355  initializeArrayGPU(std::vector<double>(batchSize, 0.0), norm2);
356  HANDLE_CUDM_ERROR(cudensitymatStateComputeNorm(handle,
357                      inputStateAdj,
358                      norm2,
359                      0x0));
360  if (verbose) {
361    std::cout << "Computed the adjoint input quantum state norm:\n";
362    printArrayGPU<double>(norm2, batchSize);
363    std::cout << "Hamiltonian parameters gradients:\n";
364    printArrayGPU<double>(hamiltonianParamsGrad, numParams * batchSize);
365  }
366
367  HANDLE_CUDA_ERROR(cudaDeviceSynchronize());
368
369  // Destroy the norm2 array
370  destroyArrayGPU(norm2);
371
372  // Destroy workspace descriptor
373  HANDLE_CUDM_ERROR(cudensitymatDestroyWorkspace(workspaceDescr));
374
375  // Destroy workspace buffer storage
376  destroyArrayGPU(workspaceBuffer);
377
378  // Destroy quantum states
379  HANDLE_CUDM_ERROR(cudensitymatDestroyState(inputStateAdj));
380  HANDLE_CUDM_ERROR(cudensitymatDestroyState(outputState));
381  HANDLE_CUDM_ERROR(cudensitymatDestroyState(inputState));
382
383  // Destroy quantum state storage
384  destroyArrayGPU(inputStateAdjElems);
385  destroyArrayGPU(outputStateElems);
386  destroyArrayGPU(inputStateElems);
387
388  // Destroy external Hamiltonian parameters
389  destroyArrayGPU(static_cast<void *>(hamiltonianParamsGrad));
390  destroyArrayGPU(static_cast<void *>(hamiltonianParams));
391
392  if (verbose)
393    std::cout << "Destroyed resources\n" << std::flush;
394}
395
396
397int main(int argc, char ** argv)
398{
399  // Assign a GPU to the process
400  HANDLE_CUDA_ERROR(cudaSetDevice(0));
401  if (verbose)
402    std::cout << "Set active device\n";
403
404  // Create a library handle
405  cudensitymatHandle_t handle;
406  HANDLE_CUDM_ERROR(cudensitymatCreate(&handle));
407  if (verbose)
408    std::cout << "Created a library handle\n";
409
410  // Run the example
411  exampleWorkflow(handle);
412
413  // Destroy the library handle
414  HANDLE_CUDM_ERROR(cudensitymatDestroy(handle));
415  if (verbose)
416    std::cout << "Destroyed the library handle\n";
417
418  HANDLE_CUDA_ERROR(cudaDeviceReset());
419
420  // Done
421  return 0;
422}

Code example (serial batched execution with backward differentiation)#

The following code example extends backward differentiation to batched operators and quantum states. Here the Hamiltonian operator contains batched coefficients such that each instance of a batched quantum state is acted on by a different instance of the batched operator (with different coefficient values). The full sample code can be found in the NVIDIA/cuQuantum repository (main serial batched gradient code and operator definition for batched gradient as well as the utility code).

The Hamiltonian definition makes both the h(t) and f(t) scalar coefficients batched, requiring user-supplied vector storage for their static and dynamic (total) values. The corresponding scalar and scalar gradient callback functions also operate on a batch instead of a single instance. Furthermore, both params and paramsGrad arrays become truly two-dimensional arrays, with the first dimension corresponding to the number of user-provided real parameters and the second dimension corresponding to the batch size.

  1/* Copyright (c) 2026-2026, NVIDIA CORPORATION & AFFILIATES.
  2 *
  3 * SPDX-License-Identifier: BSD-3-Clause
  4 */
  5
  6#pragma once
  7
  8#include <cudensitymat.h> // cuDensityMat library header
  9#include "helpers.h"      // GPU helper functions
 10
 11#include <cmath>
 12#include <complex>
 13#include <vector>
 14#include <iostream>
 15#include <cassert>
 16
 17
 18/* DESCRIPTION:
 19   Batched time-dependent transverse-field Ising Hamiltonian operator
 20   with ordered and fused ZZ terms, plus fused unitary dissipation terms:
 21    H[k] = sum_{i} {h_i(t)[k] * X_i}          // transverse-field sum of X_i operators with batched time-dependent h_i(t)[k] coefficients 
 22      + f(t)[k] * sum_{i < j} {g_ij * ZZ_ij}  // batched modulated sum of the fused ordered {Z_i * Z_j} terms with static g_ij coefficients
 23      + d * sum_{i} {Y_i * {..} * Y_i}        // scaled sum of the dissipation terms {Y_i * {..} * Y_i} fused into the YY_ii super-operators
 24   where {..} is the placeholder for the density matrix to show that the Y_i operators act from different sides.
 25*/
 26
 27/** Define the numerical type and data type for the GPU computations (same) */
 28using NumericalType = std::complex<double>;      // do not change
 29constexpr cudaDataType_t dataType = CUDA_C_64F;  // do not change
 30
 31
 32/** User-provided batched scalar CPU callback C function
 33 *  defining a batched time-dependent coefficient h_i(t) for all instances
 34 *  of the batch inside the Hamiltonian:
 35 *  h_i(t)[k] = exp(-Omega[k] * t) for k = 0, ..., batchSize-1
 36 */
 37extern "C"
 38int32_t hCoefBatchComplex64(
 39  double time,             //in: time point
 40  int64_t batchSize,       //in: user-defined batch size (number of coefficients in the batch)
 41  int32_t numParams,       //in: number of external user-provided Hamiltonian parameters (this function expects one parameter, Omega)
 42  const double * params,   //in: params[0:numParams-1][0:batchSize-1]: GPU-accessible F-ordered array of user-provided Hamiltonian parameters for all instances of the batch
 43  cudaDataType_t dataType, //in: data type (expecting CUDA_C_64F in this specific callback function)
 44  void * scalarStorage,    //inout: CPU-accessible storage for the returned batched coefficient values of shape [0:batchSize-1]
 45  cudaStream_t stream)     //in: CUDA stream (default is 0x0)
 46{
 47  if (dataType == CUDA_C_64F) {
 48    auto * tdCoef = static_cast<cuDoubleComplex *>(scalarStorage); // casting to cuDoubleComplex because this callback function expects CUDA_C_64F data type
 49    for (int64_t k = 0; k < batchSize; ++k) { // for each instance of the batch
 50      const auto omega = params[k * numParams + 0]; // params[0][k]: 0-th parameter for k-th instance of the batch
 51      tdCoef[k] = make_cuDoubleComplex(std::exp((-omega) * time), 0.0); // value of the k-th instance of the batched coefficients
 52    }
 53  } else {
 54    return 1; // error code (1: Error)
 55  }
 56  return 0; // error code (0: Success)
 57}
 58
 59
 60/** User-provided batched scalar gradient callback function (CPU-side) for the user-provided
 61 *  batched scalar callback function hCoefBatchComplex64, defining the gradients with respect
 62 *  to its single (batched) parameter Omega. It accumulates a partial derivative:
 63 *    2*Re(dCost/dOmega[k]) = 2*Re(dCost/dCoef[k] * dCoef[k]/dOmega[k]),
 64 *  where:
 65 *  - Cost is some user-defined real scalar cost function,
 66 *  - dCost/dCoef[k] is the adjoint of the cost function with respect to the k-th instance of the batched coefficient associated with the callback function,
 67 *  - dCoef[k]/dOmega[k] is the gradient of the k-th instance of the batched coefficient with respect to the parameter Omega[k]:
 68 *    dCoef[k]/dOmega[k] = -t * exp(-Omega[k] * t) for k = 0, ..., batchSize-1
 69 */
 70extern "C"
 71int32_t hCoefBatchGradComplex64(
 72  double time,             //in: time point
 73  int64_t batchSize,       //in: user-defined batch size (number of coefficients in the batch)
 74  int32_t numParams,       //in: number of external user-provided Hamiltonian parameters (this function expects one parameter, Omega)
 75  const double * params,   //in: params[0:numParams-1][0:batchSize-1]: GPU-accessible F-ordered array of user-provided Hamiltonian parameters for all instances of the batch
 76  cudaDataType_t dataType, //in: data type (expecting CUDA_C_64F in this specific callback function)
 77  void * scalarGrad,       //in: CPU-accessible storage for the batched adjoint of the batched coefficient of shape [0:batchSize-1]
 78  double * paramsGrad,     //inout: params[0:numParams-1][0:batchSize-1]: GPU-accessible F-ordered array of the returned gradients of the parameter(s) for all instances of the batch
 79  cudaStream_t stream)     //in: CUDA stream (default is 0x0)
 80{
 81  if (dataType == CUDA_C_64F) {
 82    const auto * tdCoefAdjoint = static_cast<const cuDoubleComplex *>(scalarGrad); // casting to cuDoubleComplex because this callback function expects CUDA_C_64F data type
 83    for (int64_t k = 0; k < batchSize; ++k) { // for each instance of the batch
 84      const auto omega = params[k * numParams + 0]; // params[0][k]: 0-th parameter for k-th instance of the batch
 85      paramsGrad[k * numParams + 0] += // IMPORTANT: Accumulate the partial derivative for the k-th instance of the batch, not overwrite it!
 86        2.0 * cuCreal(cuCmul(tdCoefAdjoint[k], make_cuDoubleComplex(std::exp((-omega) * time) * (-time), 0.0)));
 87    }
 88  } else {
 89    return 1; // error code (1: Error)
 90  }
 91  return 0; // error code (0: Success)
 92}
 93
 94
 95/** User-provided batched scalar CPU callback C function
 96 *  defining a batched time-dependent coefficient f(t) for all instances
 97 *  of the batch inside the Hamiltonian:
 98 *  f(t)[k] = exp(i * Omega[k] * t)
 99 *          = cos(Omega[k] * t) + i * sin(Omega[k] * t) for k = 0, ..., batchSize-1
100 */
101extern "C"
102int32_t fCoefBatchComplex64(
103  double time,             //in: time point
104  int64_t batchSize,       //in: user-defined batch size (number of coefficients in the batch)
105  int32_t numParams,       //in: number of external user-provided Hamiltonian parameters (this function expects one parameter, Omega)
106  const double * params,   //in: params[0:numParams-1][0:batchSize-1]: GPU-accessible F-ordered array of user-provided Hamiltonian parameters for all instances of the batch
107  cudaDataType_t dataType, //in: data type (expecting CUDA_C_64F in this specific callback function)
108  void * scalarStorage,    //inout: CPU-accessible storage for the returned batched coefficient values of shape [0:batchSize-1]
109  cudaStream_t stream)     //in: CUDA stream (default is 0x0)
110{
111  if (dataType == CUDA_C_64F) {
112    auto * tdCoef = static_cast<cuDoubleComplex *>(scalarStorage); // casting to cuDoubleComplex because this callback function expects CUDA_C_64F data type
113    for (int64_t k = 0; k < batchSize; ++k) { // for each instance of the batch
114      const auto omega = params[k * numParams + 0]; // params[0][k]: 0-th parameter for k-th instance of the batch
115      tdCoef[k] = make_cuDoubleComplex(std::cos(omega * time), std::sin(omega * time)); // value of the k-th instance of the batched coefficients
116    }
117  } else {
118    return 1; // error code (1: Error)
119  }
120  return 0; // error code (0: Success)
121}
122
123
124/** User-provided batched scalar gradient callback function (CPU-side) for the user-provided
125 *  batched scalar callback function fCoefBatchComplex64, defining the gradients with respect
126 *  to its single (batched) parameter Omega. It accumulates a partial derivative:
127 *    2*Re(dCost/dOmega[k]) = 2*Re(dCost/dCoef[k] * dCoef[k]/dOmega[k]),
128 *  where:
129 *  - Cost is some user-defined real scalar cost function,
130 *  - dCost/dCoef[k] is the adjoint of the cost function with respect to the k-th instance of the batched coefficient associated with the callback function,
131 *  - dCoef[k]/dOmega[k] is the gradient of the k-th instance of the batched coefficient with respect to the parameter Omega[k]:
132 *    dCoef[k]/dOmega[k] = i * t * exp(i * Omega[k] * t)
133 *                       = i * t * cos(Omega[k] * t) - t * sin(Omega[k] * t) for k = 0, ..., batchSize-1
134 */
135extern "C"
136int32_t fCoefBatchGradComplex64(
137  double time,             //in: time point
138  int64_t batchSize,       //in: user-defined batch size (number of coefficients in the batch)
139  int32_t numParams,       //in: number of external user-provided Hamiltonian parameters (this function expects one parameter, Omega)
140  const double * params,   //in: params[0:numParams-1][0:batchSize-1]: GPU-accessible F-ordered array of user-provided Hamiltonian parameters for all instances of the batch
141  cudaDataType_t dataType, //in: data type (expecting CUDA_C_64F in this specific callback function)
142  void * scalarGrad,       //in: CPU-accessible storage for the batched adjoint of the batched coefficient of shape [0:batchSize-1]
143  double * paramsGrad,     //inout: params[0:numParams-1][0:batchSize-1]: GPU-accessible F-ordered array of the returned gradients of the parameter(s) for all instances of the batch
144  cudaStream_t stream)     //in: CUDA stream (default is 0x0)
145{
146  if (dataType == CUDA_C_64F) {
147    const auto * tdCoefAdjoint = static_cast<const cuDoubleComplex *>(scalarGrad); // casting to cuDoubleComplex because this callback function expects CUDA_C_64F data type
148    for (int64_t k = 0; k < batchSize; ++k) {
149      const auto omega = params[k * numParams + 0]; // params[0][k]: 0-th parameter for k-th instance of the batch
150      paramsGrad[k * numParams + 0] += // IMPORTANT: Accumulate the partial derivative for the k-th instance of the batch, not overwrite it!
151        2.0 * cuCreal(cuCmul(tdCoefAdjoint[k], make_cuDoubleComplex(-std::sin(omega * time) * time, std::cos(omega * time) * time)));
152    }
153  } else {
154    return 1; // error code (1: Error)
155  }
156  return 0; // error code (0: Success)
157}
158
159
160/** Convenience class which encapsulates a user-defined Liouvillian operator (system Hamiltonian + dissipation terms):
161 *  - Constructor constructs the desired Liouvillian operator (`cudensitymatOperator_t`)
162 *  - Method `get()` returns a reference to the constructed Liouvillian operator
163 *  - Destructor releases all resources used by the Liouvillian operator
164 */
165class UserDefinedLiouvillian final
166{
167private:
168  // Data members
169  cudensitymatHandle_t handle;             // library context handle
170  int64_t operBatchSize;                   // batch size for the super-operator
171  const std::vector<int64_t> spaceShape;   // Hilbert space shape (extents of the modes of the composite Hilbert space)
172  void * spinXelems {nullptr};             // elements of the X spin operator in GPU RAM (F-order storage)
173  void * spinYYelems {nullptr};            // elements of the fused YY two-spin operator in GPU RAM (F-order storage)
174  void * spinZZelems {nullptr};            // elements of the fused ZZ two-spin operator in GPU RAM (F-order storage)
175  cudensitymatElementaryOperator_t spinX;  // X spin operator (elementary tensor operator)
176  cudensitymatElementaryOperator_t spinYY; // fused YY two-spin operator (elementary tensor operator)
177  cudensitymatElementaryOperator_t spinZZ; // fused ZZ two-spin operator (elementary tensor operator)
178  cudensitymatOperatorTerm_t oneBodyTerm;  // operator term: H1 = sum_{i} {h_i(t) * X_i} (one-body term)
179  cudensitymatOperatorTerm_t twoBodyTerm;  // operator term: H2 = f(t) * sum_{i < j} {g_ij * ZZ_ij} (two-body term)
180  cudensitymatOperatorTerm_t noiseTerm;    // operator term: D1 = d * sum_{i} {YY_ii}  // Y_i operators act from different sides on the density matrix (two-body mixed term)
181  // Batched coefficients
182  cuDoubleComplex * hCoefsStatic {nullptr}; // static part of the h(t) batched coefficients in the one-body term (of length operBatchSize)
183  cuDoubleComplex * hCoefsTotal {nullptr};  // total h(t) batched coefficients in the one-body term (of length operBatchSize)
184  cuDoubleComplex * fCoefsStaticMinus {nullptr}; // static part of the f(t) batched coefficients in the two-body term (of length operBatchSize)
185  cuDoubleComplex * fCoefsTotalMinus {nullptr};  // total f(t) batched coefficients in the two-body term (of length operBatchSize)
186  cuDoubleComplex * fCoefsStaticPlus {nullptr};  // static part of the f(t) batched coefficients in the dual two-body term (of length operBatchSize)
187  cuDoubleComplex * fCoefsTotalPlus {nullptr};   // total f(t) batched coefficients in the dual two-body term (of length operBatchSize)
188  // Final Liouvillian operator
189  cudensitymatOperator_t liouvillian; // full operator: (-i * (H1 + H2) * {..}) + (i * {..} * (H1 + H2)) + D1{..} (super-operator)
190
191public:
192
193  // Constructor constructs a user-defined Liouvillian operator
194  UserDefinedLiouvillian(cudensitymatHandle_t contextHandle,             // library context handle
195                         const std::vector<int64_t> & hilbertSpaceShape, // Hilbert space shape
196                         int64_t batchSize):                             // batch size for the super-operator
197    handle(contextHandle), operBatchSize(batchSize), spaceShape(hilbertSpaceShape)
198  {
199    // Define the necessary operator tensors in GPU memory (F-order storage!)
200    spinXelems = createInitializeArrayGPU<NumericalType>(  // X[i0; j0]
201                  {{0.0, 0.0}, {1.0, 0.0},   // 1st column of matrix X
202                   {1.0, 0.0}, {0.0, 0.0}}); // 2nd column of matrix X
203
204    spinYYelems = createInitializeArrayGPU<NumericalType>(  // YY[i0, i1; j0, j1] := Y[i0; j0] * Y[i1; j1]
205                    {{0.0, 0.0},  {0.0, 0.0}, {0.0, 0.0}, {-1.0, 0.0},  // 1st column of matrix YY
206                     {0.0, 0.0},  {0.0, 0.0}, {1.0, 0.0}, {0.0, 0.0},   // 2nd column of matrix YY
207                     {0.0, 0.0},  {1.0, 0.0}, {0.0, 0.0}, {0.0, 0.0},   // 3rd column of matrix YY
208                     {-1.0, 0.0}, {0.0, 0.0}, {0.0, 0.0}, {0.0, 0.0}}); // 4th column of matrix YY
209
210    spinZZelems = createInitializeArrayGPU<NumericalType>(  // ZZ[i0, i1; j0, j1] := Z[i0; j0] * Z[i1; j1]
211                    {{1.0, 0.0}, {0.0, 0.0},  {0.0, 0.0},  {0.0, 0.0},   // 1st column of matrix ZZ
212                     {0.0, 0.0}, {-1.0, 0.0}, {0.0, 0.0},  {0.0, 0.0},   // 2nd column of matrix ZZ
213                     {0.0, 0.0}, {0.0, 0.0},  {-1.0, 0.0}, {0.0, 0.0},   // 3rd column of matrix ZZ
214                     {0.0, 0.0}, {0.0, 0.0},  {0.0, 0.0},  {1.0, 0.0}}); // 4th column of matrix ZZ
215
216    // Construct the necessary Elementary Tensor Operators
217    //  X_i operator
218    HANDLE_CUDM_ERROR(cudensitymatCreateElementaryOperator(handle,
219                        1,                                   // one-body operator
220                        std::vector<int64_t>({2}).data(),    // acts in tensor space of shape {2}
221                        CUDENSITYMAT_OPERATOR_SPARSITY_NONE, // dense tensor storage
222                        0,                                   // 0 for dense tensors
223                        nullptr,                             // nullptr for dense tensors
224                        dataType,                            // data type
225                        spinXelems,                          // tensor elements in GPU memory
226                        cudensitymatTensorCallbackNone,      // no tensor callback function (tensor is not time-dependent)
227                        cudensitymatTensorGradientCallbackNone, // no tensor gradient callback function
228                        &spinX));                            // the created elementary tensor operator
229    //  ZZ_ij = Z_i * Z_j fused operator
230    HANDLE_CUDM_ERROR(cudensitymatCreateElementaryOperator(handle,
231                        2,                                   // two-body operator
232                        std::vector<int64_t>({2,2}).data(),  // acts in tensor space of shape {2,2}
233                        CUDENSITYMAT_OPERATOR_SPARSITY_NONE, // dense tensor storage
234                        0,                                   // 0 for dense tensors
235                        nullptr,                             // nullptr for dense tensors
236                        dataType,                            // data type
237                        spinZZelems,                         // tensor elements in GPU memory
238                        cudensitymatTensorCallbackNone,      // no tensor callback function (tensor is not time-dependent)
239                        cudensitymatTensorGradientCallbackNone, // no tensor gradient callback function
240                        &spinZZ));                           // the created elementary tensor operator
241    //  YY_ii = Y_i * {..} * Y_i fused operator (note action from different sides)
242    HANDLE_CUDM_ERROR(cudensitymatCreateElementaryOperator(handle,
243                        2,                                   // two-body operator
244                        std::vector<int64_t>({2,2}).data(),  // acts in tensor space of shape {2,2}
245                        CUDENSITYMAT_OPERATOR_SPARSITY_NONE, // dense tensor storage
246                        0,                                   // 0 for dense tensors
247                        nullptr,                             // nullptr for dense tensors
248                        dataType,                            // data type
249                        spinYYelems,                         // tensor elements in GPU memory
250                        cudensitymatTensorCallbackNone,      // no tensor callback function (tensor is not time-dependent)
251                        cudensitymatTensorGradientCallbackNone, // no tensor gradient callback function
252                        &spinYY));                           // the created elementary tensor operator
253
254    // Construct the necessary Operator Terms from tensor products of Elementary Tensor Operators
255    //  Create an empty operator term
256    HANDLE_CUDM_ERROR(cudensitymatCreateOperatorTerm(handle,
257                        spaceShape.size(),                   // Hilbert space rank (number of modes)
258                        spaceShape.data(),                   // Hilbert space shape (mode extents)
259                        &oneBodyTerm));                      // the created empty operator term
260    //  Define the batched operator term: H1[k] = sum_{i} {h_i(t)[k] * X_i}
261    hCoefsStatic = static_cast<cuDoubleComplex *>(createInitializeArrayGPU(
262      std::vector<cuDoubleComplex>(operBatchSize, make_cuDoubleComplex(1.0, 0.0)))); // 1.0 constant for all coefficient instances in the batch
263    hCoefsTotal = static_cast<cuDoubleComplex *>(createInitializeArrayGPU(
264      std::vector<cuDoubleComplex>(operBatchSize, make_cuDoubleComplex(0.0, 0.0)))); // storage for the total coefficients for all instances of the batch
265    for (int32_t i = 0; i < spaceShape.size(); ++i) {
266      HANDLE_CUDM_ERROR(cudensitymatOperatorTermAppendElementaryProductBatch(handle,
267                          oneBodyTerm,
268                          1,                                                             // number of elementary tensor operators in the product
269                          std::vector<cudensitymatElementaryOperator_t>({spinX}).data(), // elementary tensor operators forming the product
270                          std::vector<int32_t>({i}).data(),                              // space modes acted on by the operator product
271                          std::vector<int32_t>({0}).data(),                              // space mode action duality (0: from the left; 1: from the right)
272                          operBatchSize,                                                 // batch size
273                          hCoefsStatic,                                                  // static part of the h(t) batched coefficients in the one-body term
274                          hCoefsTotal,                                                   // total h(t) batched coefficients in the one-body term
275                          {hCoefBatchComplex64, CUDENSITYMAT_CALLBACK_DEVICE_CPU, nullptr}, // CPU batched scalar callback function defining the time-dependent coefficient associated with this operator product
276                          {hCoefBatchGradComplex64, CUDENSITYMAT_CALLBACK_DEVICE_CPU, nullptr})); // CPU batched scalar gradient callback function defining the gradient of the coefficient with respect to the parameter Omega
277    }
278    //  Create an empty operator term
279    HANDLE_CUDM_ERROR(cudensitymatCreateOperatorTerm(handle,
280                        spaceShape.size(),                   // Hilbert space rank (number of modes)
281                        spaceShape.data(),                   // Hilbert space shape (mode extents)
282                        &twoBodyTerm));                      // the created empty operator term
283    //  Define the operator term: H2 = f(t) * sum_{i < j} {g_ij * ZZ_ij}
284    for (int32_t i = 0; i < spaceShape.size() - 1; ++i) {
285      for (int32_t j = (i + 1); j < spaceShape.size(); ++j) {
286        const double g_ij = -1.0 / static_cast<double>(i + j + 1); // assign some value to the time-independent g_ij coefficient
287        HANDLE_CUDM_ERROR(cudensitymatOperatorTermAppendElementaryProduct(handle,
288                            twoBodyTerm,
289                            1,                                                              // number of elementary tensor operators in the product
290                            std::vector<cudensitymatElementaryOperator_t>({spinZZ}).data(), // elementary tensor operators forming the product
291                            std::vector<int32_t>({i, j}).data(),                            // space modes acted on by the operator product
292                            std::vector<int32_t>({0, 0}).data(),                            // space mode action duality (0: from the left; 1: from the right)
293                            make_cuDoubleComplex(g_ij, 0.0),                                // g_ij static coefficient: Always 64-bit-precision complex number
294                            cudensitymatScalarCallbackNone,                                 // no time-dependent coefficient associated with this operator product
295                            cudensitymatScalarGradientCallbackNone));                       // no coefficient gradient associated with this operator product
296      }
297    }
298    //  Create an empty operator term
299    HANDLE_CUDM_ERROR(cudensitymatCreateOperatorTerm(handle,
300                        spaceShape.size(),                   // Hilbert space rank (number of modes)
301                        spaceShape.data(),                   // Hilbert space shape (mode extents)
302                        &noiseTerm));                        // the created empty operator term
303    //  Define the operator term: D1 = d * sum_{i} {YY_ii}
304    for (int32_t i = 0; i < spaceShape.size(); ++i) {
305      HANDLE_CUDM_ERROR(cudensitymatOperatorTermAppendElementaryProduct(handle,
306                          noiseTerm,
307                          1,                                                              // number of elementary tensor operators in the product
308                          std::vector<cudensitymatElementaryOperator_t>({spinYY}).data(), // elementary tensor operators forming the product
309                          std::vector<int32_t>({i, i}).data(),                            // space modes acted on by the operator product (from different sides)
310                          std::vector<int32_t>({0, 1}).data(),                            // space mode action duality (0: from the left; 1: from the right)
311                          make_cuDoubleComplex(1.0, 0.0),                                 // default coefficient: Always 64-bit-precision complex number
312                          cudensitymatScalarCallbackNone,                                 // no time-dependent coefficient associated with this operator product
313                          cudensitymatScalarGradientCallbackNone));                       // no coefficient gradient associated with this operator product
314    }
315
316    // Construct the full Liouvillian operator as a sum of the created operator terms
317    //  Create an empty operator (super-operator)
318    HANDLE_CUDM_ERROR(cudensitymatCreateOperator(handle,
319                        spaceShape.size(),                // Hilbert space rank (number of modes)
320                        spaceShape.data(),                // Hilbert space shape (modes extents)
321                        &liouvillian));                   // the created empty operator (super-operator)
322    //  Append an operator term to the operator (super-operator)
323    HANDLE_CUDM_ERROR(cudensitymatOperatorAppendTerm(handle,
324                        liouvillian,
325                        oneBodyTerm,                      // appended operator term
326                        0,                                // operator term action duality as a whole (0: acting from the left; 1: acting from the right)
327                        make_cuDoubleComplex(0.0, -1.0),  // -i constant
328                        cudensitymatScalarCallbackNone,   // no time-dependent coefficient associated with the operator term as a whole
329                        cudensitymatScalarGradientCallbackNone)); // no coefficient gradient associated with the operator term as a whole
330    //  Append an operator term to the operator (super-operator)
331    fCoefsStaticMinus = static_cast<cuDoubleComplex *>(createInitializeArrayGPU(
332      std::vector<cuDoubleComplex>(operBatchSize, make_cuDoubleComplex(0.0, -1.0)))); // -i constant for all coefficient instances in the batch
333    fCoefsTotalMinus = static_cast<cuDoubleComplex *>(createInitializeArrayGPU(
334      std::vector<cuDoubleComplex>(operBatchSize, make_cuDoubleComplex(0.0, 0.0)))); // storage for the total coefficients for all instances of the batch
335    HANDLE_CUDM_ERROR(cudensitymatOperatorAppendTermBatch(handle,
336                        liouvillian,
337                        twoBodyTerm,                      // appended operator term
338                        0,                                // operator term action duality as a whole (0: acting from the left; 1: acting from the right)
339                        operBatchSize,                    // number of instances of the operator term in the batch (they differ by the coefficient value)
340                        fCoefsStaticMinus,                // static part of the f(t) batched coefficients in the two-body term
341                        fCoefsTotalMinus,                 // total f(t) batched coefficients in the two-body term
342                        {fCoefBatchComplex64, CUDENSITYMAT_CALLBACK_DEVICE_CPU, nullptr}, // CPU batched scalar callback function defining the time-dependent coefficient associated with this operator term as a whole
343                        {fCoefBatchGradComplex64, CUDENSITYMAT_CALLBACK_DEVICE_CPU, nullptr})); // CPU batched scalar gradient callback function defining the gradient of the coefficient with respect to parameter Omega
344    //  Append an operator term to the operator (super-operator)
345    HANDLE_CUDM_ERROR(cudensitymatOperatorAppendTerm(handle,
346                        liouvillian,
347                        oneBodyTerm,                      // appended operator term
348                        1,                                // operator term action duality as a whole (0: acting from the left; 1: acting from the right)
349                        make_cuDoubleComplex(0.0, +1.0),  // +i constant
350                        cudensitymatScalarCallbackNone,   // no time-dependent coefficient associated with the operator term as a whole
351                        cudensitymatScalarGradientCallbackNone)); // no coefficient gradient associated with the operator term as a whole
352    //  Append an operator term to the operator (super-operator)
353    fCoefsStaticPlus = static_cast<cuDoubleComplex *>(createInitializeArrayGPU(
354      std::vector<cuDoubleComplex>(operBatchSize, make_cuDoubleComplex(0.0, +1.0)))); // +i constant for all coefficient instances in the batch
355    fCoefsTotalPlus = static_cast<cuDoubleComplex *>(createInitializeArrayGPU(
356      std::vector<cuDoubleComplex>(operBatchSize, make_cuDoubleComplex(0.0, 0.0)))); // storage for the total coefficients for all instances of the batch
357    HANDLE_CUDM_ERROR(cudensitymatOperatorAppendTermBatch(handle,
358                        liouvillian,
359                        twoBodyTerm,                      // appended operator term
360                        1,                                // operator term action duality as a whole (0: acting from the left; 1: acting from the right)
361                        operBatchSize,                    // number of instances of the operator term in the batch (they differ by the coefficient value)
362                        fCoefsStaticPlus,                 // static part of the f(t) batched coefficients in the dual two-body term
363                        fCoefsTotalPlus,                  // total f(t) batched coefficients in the dual two-body term
364                        {fCoefBatchComplex64, CUDENSITYMAT_CALLBACK_DEVICE_CPU, nullptr}, // CPU batched scalar callback function defining the time-dependent coefficient associated with this operator term as a whole
365                        {fCoefBatchGradComplex64, CUDENSITYMAT_CALLBACK_DEVICE_CPU, nullptr})); // CPU batched scalar gradient callback function defining the gradient of the coefficient with respect to parameter Omega
366    //  Append an operator term to the operator (super-operator)
367    const double d = 1.0; // assign some value to the time-independent coefficient
368    HANDLE_CUDM_ERROR(cudensitymatOperatorAppendTerm(handle,
369                        liouvillian,
370                        noiseTerm,                        // appended operator term
371                        0,                                // operator term action duality as a whole (no duality reversing in this case)
372                        make_cuDoubleComplex(d, 0.0),     // static coefficient associated with the operator term as a whole
373                        cudensitymatScalarCallbackNone,   // no time-dependent coefficient associated with the operator term as a whole
374                        cudensitymatScalarGradientCallbackNone)); // no coefficient gradient associated with the operator term as a whole
375  }
376
377  // Destructor destructs the user-defined Liouvillian operator
378  ~UserDefinedLiouvillian()
379  {
380    // Destroy the Liouvillian operator
381    HANDLE_CUDM_ERROR(cudensitymatDestroyOperator(liouvillian));
382
383    // Destroy operator terms
384    HANDLE_CUDM_ERROR(cudensitymatDestroyOperatorTerm(noiseTerm));
385    HANDLE_CUDM_ERROR(cudensitymatDestroyOperatorTerm(twoBodyTerm));
386    HANDLE_CUDM_ERROR(cudensitymatDestroyOperatorTerm(oneBodyTerm));
387
388    // Destroy elementary tensor operators
389    HANDLE_CUDM_ERROR(cudensitymatDestroyElementaryOperator(spinYY));
390    HANDLE_CUDM_ERROR(cudensitymatDestroyElementaryOperator(spinZZ));
391    HANDLE_CUDM_ERROR(cudensitymatDestroyElementaryOperator(spinX));
392
393    // Destroy the batched coefficients
394    destroyArrayGPU(fCoefsTotalPlus);
395    destroyArrayGPU(fCoefsStaticPlus);
396    destroyArrayGPU(fCoefsTotalMinus);
397    destroyArrayGPU(fCoefsStaticMinus);
398    destroyArrayGPU(hCoefsTotal);
399    destroyArrayGPU(hCoefsStatic);
400
401    // Destroy operator tensors
402    destroyArrayGPU(spinYYelems);
403    destroyArrayGPU(spinZZelems);
404    destroyArrayGPU(spinXelems);
405  }
406
407  // Disable copy constructor/assignment (GPU resources are private, no deep copy)
408  UserDefinedLiouvillian(const UserDefinedLiouvillian &) = delete;
409  UserDefinedLiouvillian & operator=(const UserDefinedLiouvillian &) = delete;
410  UserDefinedLiouvillian(UserDefinedLiouvillian &&) = delete;
411  UserDefinedLiouvillian & operator=(UserDefinedLiouvillian &&) = delete;
412
413  /** Returns the number of externally provided Hamiltonian parameters. */
414  int32_t getNumParameters() const
415  {
416    return 1; // one parameter Omega
417  }
418
419  /** Get access to the constructed Liouvillian operator. */
420  cudensitymatOperator_t & get()
421  {
422    return liouvillian;
423  }
424
425};

Once the batched Liouvillian operator has been defined, the rest of the code logic is largely identical to the non-batched case.

  1/* Copyright (c) 2026-2026, NVIDIA CORPORATION & AFFILIATES.
  2 *
  3 * SPDX-License-Identifier: BSD-3-Clause
  4 */
  5
  6#include <cudensitymat.h>  // cuDensityMat library header
  7#include "helpers.h"       // helper functions
  8
  9
 10// Batched time-dependent transverse-field Ising Hamiltonian operator
 11// with ordered and fused ZZ terms, plus fused unitary dissipation terms
 12#include "transverse_ising_full_fused_noisy_batch_grad.h" // user-defined batched Liouvillian operator example
 13
 14#include <cmath>
 15#include <complex>
 16#include <vector>
 17#include <chrono>
 18#include <iostream>
 19#include <cassert>
 20
 21
 22// Number of times to perform operator action on a quantum state
 23constexpr int NUM_REPEATS = 2;
 24
 25// Logging verbosity
 26bool verbose = true;
 27
 28
 29// Example workflow
 30void exampleWorkflow(cudensitymatHandle_t handle)
 31{
 32  // Define the composite Hilbert space shape and
 33  // quantum state batch size (number of individual quantum states in a batched simulation)
 34  const std::vector<int64_t> spaceShape({2,2,2,2}); // dimensions of quantum degrees of freedom
 35  const int64_t batchSize = 3;                      // number of quantum states per batch
 36
 37  if (verbose) {
 38    std::cout << "Hilbert space rank = " << spaceShape.size() << "; Shape = (";
 39    for (const auto & dimsn: spaceShape)
 40      std::cout << dimsn << ",";
 41    std::cout << ")" << std::endl;
 42    std::cout << "Quantum state batch size = " << batchSize << std::endl;
 43  }
 44
 45  // Construct a user-defined batched Liouvillian operator using a convenience C++ class
 46  // Note that the constructed Liouvillian operator has some batched coefficients
 47  UserDefinedLiouvillian liouvillian(handle, spaceShape, batchSize);
 48  if (verbose)
 49    std::cout << "Constructed the Liouvillian operator\n";
 50
 51  // Set and place external user-provided Hamiltonian parameters in GPU memory
 52  const int32_t numParams = liouvillian.getNumParameters(); // number of external user-provided Hamiltonian parameters
 53  if (verbose)
 54    std::cout << "Number of external user-provided Hamiltonian parameters = " << numParams << std::endl;
 55  std::vector<double> cpuHamParams(numParams * batchSize);
 56  for (int64_t j = 0; j < batchSize; ++j) {
 57    for (int32_t i = 0; i < numParams; ++i) {
 58      cpuHamParams[j * numParams + i] = double(i+1) / double(j+1); // just setting some parameter values for each instance of the batch
 59    }
 60  }
 61  auto * hamiltonianParams = static_cast<double *>(createInitializeArrayGPU(cpuHamParams));
 62  if (verbose)
 63    std::cout << "Created an array of external user-provided Hamiltonian parameters in GPU memory\n";
 64
 65  // Create an array of gradients for the user-provided Hamiltonian parameters in GPU memory
 66  std::vector<double> cpuHamParamsGrad(numParams * batchSize, 0.0);
 67  auto * hamiltonianParamsGrad = static_cast<double *>(createInitializeArrayGPU(cpuHamParamsGrad));
 68  if (verbose)
 69    std::cout << "Created an array of gradients for the external user-provided Hamiltonian parameters in GPU memory\n";
 70
 71  // Declare the input quantum state
 72  cudensitymatState_t inputState;
 73  HANDLE_CUDM_ERROR(cudensitymatCreateState(handle,
 74                      CUDENSITYMAT_STATE_PURITY_MIXED,  // pure (state vector) or mixed (density matrix) state
 75                      spaceShape.size(),
 76                      spaceShape.data(),
 77                      batchSize,
 78                      dataType,
 79                      &inputState));
 80
 81  // Query the size of the quantum state storage
 82  std::size_t storageSize {0}; // only one storage component (tensor) is needed (no tensor factorization)
 83  HANDLE_CUDM_ERROR(cudensitymatStateGetComponentStorageSize(handle,
 84                      inputState,
 85                      1,               // only one storage component (tensor)
 86                      &storageSize));  // storage size in bytes
 87  const std::size_t stateVolume = storageSize / sizeof(NumericalType);  // quantum state tensor volume (number of elements)
 88  if (verbose)
 89    std::cout << "Quantum state storage size (bytes) = " << storageSize << std::endl;
 90
 91  // Prepare some initial value for the input quantum state batch
 92  std::vector<NumericalType> inputStateValue(stateVolume);
 93  if constexpr (std::is_same_v<NumericalType, float>) {
 94    for (std::size_t i = 0; i < stateVolume; ++i) {
 95      inputStateValue[i] = 1.0f / float(i+1); // just some value
 96    }
 97  } else if constexpr (std::is_same_v<NumericalType, double>) {
 98    for (std::size_t i = 0; i < stateVolume; ++i) {
 99      inputStateValue[i] = 1.0 / double(i+1); // just some value
100    }
101  } else if constexpr (std::is_same_v<NumericalType, std::complex<float>>) {
102    for (std::size_t i = 0; i < stateVolume; ++i) {
103      inputStateValue[i] = NumericalType{1.0f / float(i+1), -1.0f / float(i+2)}; // just some value
104    }
105  } else if constexpr (std::is_same_v<NumericalType, std::complex<double>>) {
106    for (std::size_t i = 0; i < stateVolume; ++i) {
107      inputStateValue[i] = NumericalType{1.0 / double(i+1), -1.0 / double(i+2)}; // just some value
108    }
109  } else {
110    std::cerr << "Error: Unsupported data type!\n";
111    std::exit(1);
112  }
113  // Allocate initialized GPU storage for the input quantum state with prepared values
114  auto * inputStateElems = createInitializeArrayGPU(inputStateValue);
115  if (verbose)
116    std::cout << "Allocated input quantum state storage and initialized it to some value\n";
117
118  // Attach initialized GPU storage to the input quantum state
119  HANDLE_CUDM_ERROR(cudensitymatStateAttachComponentStorage(handle,
120                      inputState,
121                      1,                                                 // only one storage component (tensor)
122                      std::vector<void*>({inputStateElems}).data(),      // pointer to the GPU storage for the quantum state
123                      std::vector<std::size_t>({storageSize}).data()));  // size of the GPU storage for the quantum state
124  if (verbose)
125    std::cout << "Constructed input quantum state\n";
126
127  // Declare the output quantum state of the same shape
128  cudensitymatState_t outputState;
129  HANDLE_CUDM_ERROR(cudensitymatCreateState(handle,
130                      CUDENSITYMAT_STATE_PURITY_MIXED,  // pure (state vector) or mixed (density matrix) state
131                      spaceShape.size(),
132                      spaceShape.data(),
133                      batchSize,
134                      dataType,
135                      &outputState));
136
137  // Allocate GPU storage for the output quantum state
138  auto * outputStateElems = createArrayGPU<NumericalType>(stateVolume);
139  if (verbose)
140    std::cout << "Allocated output quantum state storage\n";
141
142  // Attach GPU storage to the output quantum state
143  HANDLE_CUDM_ERROR(cudensitymatStateAttachComponentStorage(handle,
144                      outputState,
145                      1,                                                 // only one storage component (tensor)
146                      std::vector<void*>({outputStateElems}).data(),     // pointer to the GPU storage for the quantum state
147                      std::vector<std::size_t>({storageSize}).data()));  // size of the GPU storage for the quantum state
148  if (verbose)
149    std::cout << "Constructed output quantum state\n";
150
151  // Declare the adjoint input quantum state of the same shape
152  cudensitymatState_t inputStateAdj;
153  HANDLE_CUDM_ERROR(cudensitymatCreateState(handle,
154                      CUDENSITYMAT_STATE_PURITY_MIXED,  // pure (state vector) or mixed (density matrix) state
155                      spaceShape.size(),
156                      spaceShape.data(),
157                      batchSize,
158                      dataType,  // data type must match
159                      &inputStateAdj));
160
161  // Allocate GPU storage for the adjoint input quantum state
162  auto * inputStateAdjElems = createArrayGPU<NumericalType>(stateVolume);
163  if (verbose)
164    std::cout << "Allocated adjoint input quantum state storage\n";
165
166  // Attach GPU storage to the adjoint input quantum state
167  HANDLE_CUDM_ERROR(cudensitymatStateAttachComponentStorage(handle,
168                      inputStateAdj,
169                      1,                                                 // only one storage component (tensor)
170                      std::vector<void*>({inputStateAdjElems}).data(),   // pointer to the GPU storage for the quantum state
171                      std::vector<std::size_t>({storageSize}).data()));  // size of the GPU storage for the quantum state
172  if (verbose)
173    std::cout << "Constructed adjoint input quantum state\n";
174
175  // Declare a workspace descriptor
176  cudensitymatWorkspaceDescriptor_t workspaceDescr;
177  HANDLE_CUDM_ERROR(cudensitymatCreateWorkspace(handle, &workspaceDescr));
178
179  // Query free GPU memory
180  std::size_t freeMem = 0, totalMem = 0;
181  HANDLE_CUDA_ERROR(cudaMemGetInfo(&freeMem, &totalMem));
182  freeMem = static_cast<std::size_t>(static_cast<double>(freeMem) * 0.45); // take 45% of the free memory for the workspace buffer
183  if (verbose)
184    std::cout << "Max workspace buffer size (bytes) = " << freeMem << std::endl;
185
186  // Allocate GPU storage for the workspace buffer
187  const std::size_t bufferVolume = freeMem / sizeof(NumericalType);
188  auto * workspaceBuffer = createArrayGPU<NumericalType>(bufferVolume);
189  if (verbose)
190    std::cout << "Allocated workspace buffer of size (bytes) = " << freeMem << std::endl;
191
192  // Prepare the Liouvillian operator action on a quantum state (needs to be done only once)
193  auto startTime = std::chrono::high_resolution_clock::now();
194  HANDLE_CUDM_ERROR(cudensitymatOperatorPrepareAction(handle,
195                      liouvillian.get(),
196                      inputState,
197                      outputState,
198                      CUDENSITYMAT_COMPUTE_64F,  // GPU compute type
199                      freeMem,                   // max available GPU free memory for the workspace
200                      workspaceDescr,            // workspace descriptor
201                      0x0));                     // default CUDA stream
202  auto finishTime = std::chrono::high_resolution_clock::now();
203  std::chrono::duration<double> timeSec = finishTime - startTime;
204  if (verbose)
205    std::cout << "Operator action preparation time (sec) = " << timeSec.count() << std::endl;
206
207  // Query the required workspace buffer size (bytes)
208  std::size_t requiredBufferSize {0};
209  HANDLE_CUDM_ERROR(cudensitymatWorkspaceGetMemorySize(handle,
210                      workspaceDescr,
211                      CUDENSITYMAT_MEMSPACE_DEVICE,
212                      CUDENSITYMAT_WORKSPACE_SCRATCH,
213                      &requiredBufferSize));
214  if (verbose)
215    std::cout << "Required workspace buffer size (bytes) = " << requiredBufferSize << std::endl;
216
217  if (requiredBufferSize > freeMem) {
218    std::cerr << "Error: Required workspace buffer size is greater than the available GPU free memory!\n";
219    std::exit(1);
220  }
221
222  // Attach the workspace buffer to the workspace descriptor
223  HANDLE_CUDM_ERROR(cudensitymatWorkspaceSetMemory(handle,
224                      workspaceDescr,
225                      CUDENSITYMAT_MEMSPACE_DEVICE,
226                      CUDENSITYMAT_WORKSPACE_SCRATCH,
227                      workspaceBuffer,
228                      requiredBufferSize));
229  if (verbose)
230    std::cout << "Attached workspace buffer of size (bytes) = " << requiredBufferSize << std::endl;
231
232  // Apply the Liouvillian operator to the input quatum state
233  // and accumulate its action into the output quantum state (note the accumulative += semantics)
234  for (int32_t repeat = 0; repeat < NUM_REPEATS; ++repeat) { // repeat multiple times for accurate timing
235    // Zero out the output quantum state
236    HANDLE_CUDM_ERROR(cudensitymatStateInitializeZero(handle,
237                        outputState,
238                        0x0));
239    if (verbose)
240      std::cout << "Initialized the output quantum state to zero\n";
241    HANDLE_CUDA_ERROR(cudaDeviceSynchronize());
242    startTime = std::chrono::high_resolution_clock::now();
243    HANDLE_CUDM_ERROR(cudensitymatOperatorComputeAction(handle,
244                        liouvillian.get(),
245                        0.3,                // time point (some value)
246                        batchSize,          // user-defined batch size
247                        numParams,          // number of external user-defined Hamiltonian parameters
248                        hamiltonianParams,  // external Hamiltonian parameters in GPU memory
249                        inputState,         // input quantum state
250                        outputState,        // output quantum state
251                        workspaceDescr,     // workspace descriptor
252                        0x0));              // default CUDA stream
253    HANDLE_CUDA_ERROR(cudaDeviceSynchronize());
254    finishTime = std::chrono::high_resolution_clock::now();
255    timeSec = finishTime - startTime;
256    if (verbose)
257      std::cout << "Operator action computation time (sec) = " << timeSec.count() << std::endl;
258  }
259
260  // Compute the squared norm of the output quantum state
261  void * norm2 = createInitializeArrayGPU(std::vector<double>(batchSize, 0.0));
262  HANDLE_CUDM_ERROR(cudensitymatStateComputeNorm(handle,
263                      outputState,
264                      norm2,
265                      0x0));
266  if (verbose) {
267    std::cout << "Computed the output quantum state norm:\n";
268    printArrayGPU<double>(norm2, batchSize);
269  }
270
271  HANDLE_CUDA_ERROR(cudaDeviceSynchronize());
272
273  // Prepare the Liouvillian operator action backward differentiation (needs to be done only once)
274  startTime = std::chrono::high_resolution_clock::now();
275  HANDLE_CUDM_ERROR(cudensitymatOperatorPrepareActionBackwardDiff(handle,
276                      liouvillian.get(),
277                      inputState,
278                      outputState,               // adjoint output quantum state is always congruent to the output quantum state
279                      CUDENSITYMAT_COMPUTE_64F,  // GPU compute type
280                      freeMem,                   // max available GPU free memory for the workspace buffer
281                      workspaceDescr,            // workspace descriptor
282                      0x0));                     // default CUDA stream
283  finishTime = std::chrono::high_resolution_clock::now();
284  timeSec = finishTime - startTime;
285  if (verbose)
286    std::cout << "Operator action backward differentiation preparation time (sec) = " << timeSec.count() << std::endl;
287
288  // Query the required workspace buffer size (bytes)
289  requiredBufferSize = 0;
290  HANDLE_CUDM_ERROR(cudensitymatWorkspaceGetMemorySize(handle,
291                      workspaceDescr,
292                      CUDENSITYMAT_MEMSPACE_DEVICE,
293                      CUDENSITYMAT_WORKSPACE_SCRATCH,
294                      &requiredBufferSize));
295  if (verbose)
296    std::cout << "Required workspace buffer size (bytes) = " << requiredBufferSize << std::endl;
297
298  if (requiredBufferSize > freeMem) {
299    std::cerr << "Error: Required workspace buffer size is greater than the available GPU free memory!\n";
300    std::exit(1);
301  }
302
303  // Attach the workspace buffer to the workspace descriptor
304  HANDLE_CUDM_ERROR(cudensitymatWorkspaceSetMemory(handle,
305                      workspaceDescr,
306                      CUDENSITYMAT_MEMSPACE_DEVICE,
307                      CUDENSITYMAT_WORKSPACE_SCRATCH,
308                      workspaceBuffer,
309                      requiredBufferSize));
310  if (verbose)
311    std::cout << "Attached workspace buffer of size (bytes) = " << requiredBufferSize << std::endl;
312
313  // Liouvillian operator action backward differentiation:
314  // The adjoint output quantum state, which is always congruent to the output quantum state,
315  // depends on the user-defined cost function, so here we simply pass the previously computed output quantum state.
316  // In real-life applications, the user will pass their adjoint output quantum state, computed for their cost function.
317  for (int32_t repeat = 0; repeat < NUM_REPEATS; ++repeat) { // repeat multiple times for accurate timing
318    // Zero out the adjoint input quantum state and gradients
319    HANDLE_CUDM_ERROR(cudensitymatStateInitializeZero(handle,
320                        inputStateAdj,
321                        0x0));
322    initializeArrayGPU(std::vector<double>(numParams * batchSize, 0.0), hamiltonianParamsGrad);
323    if (verbose)
324      std::cout << "Initialized the adjoint input quantum state and gradients to zero\n";
325    HANDLE_CUDA_ERROR(cudaDeviceSynchronize());
326    startTime = std::chrono::high_resolution_clock::now();
327    HANDLE_CUDM_ERROR(cudensitymatOperatorComputeActionBackwardDiff(handle,
328                        liouvillian.get(),
329                        0.3,                    // time point (some value)
330                        batchSize,              // user-defined batch size
331                        numParams,              // number of external user-defined Hamiltonian parameters
332                        hamiltonianParams,      // external Hamiltonian parameters in GPU memory
333                        inputState,             // input quantum state
334                        outputState,            // adjoint output quantum state (here we just pass the previously computed output quantum state for simplicity)
335                        inputStateAdj,          // adjoint input quantum state
336                        hamiltonianParamsGrad,  // partial gradients with respect to the user-defined real parameters
337                        workspaceDescr,         // workspace descriptor
338                        0x0));                  // default CUDA stream
339    HANDLE_CUDA_ERROR(cudaDeviceSynchronize());
340    finishTime = std::chrono::high_resolution_clock::now();
341    timeSec = finishTime - startTime;
342    if (verbose)
343      std::cout << "Operator action backward differentiation computation time (sec) = " << timeSec.count() << std::endl;
344  }
345
346  // Compute the squared norm of the adjoint input quantum state
347  initializeArrayGPU(std::vector<double>(batchSize, 0.0), norm2);
348  HANDLE_CUDM_ERROR(cudensitymatStateComputeNorm(handle,
349                      inputStateAdj,
350                      norm2,
351                      0x0));
352  if (verbose) {
353    std::cout << "Computed the adjoint input quantum state norm:\n";
354    printArrayGPU<double>(norm2, batchSize);
355    std::cout << "Hamiltonian parameters gradients:\n";
356    printArrayGPU<double>(hamiltonianParamsGrad, numParams * batchSize);
357  }
358
359  HANDLE_CUDA_ERROR(cudaDeviceSynchronize());
360
361  // Destroy the norm2 array
362  destroyArrayGPU(norm2);
363
364  // Destroy workspace descriptor
365  HANDLE_CUDM_ERROR(cudensitymatDestroyWorkspace(workspaceDescr));
366
367  // Destroy workspace buffer storage
368  destroyArrayGPU(workspaceBuffer);
369
370  // Destroy quantum states
371  HANDLE_CUDM_ERROR(cudensitymatDestroyState(inputStateAdj));
372  HANDLE_CUDM_ERROR(cudensitymatDestroyState(outputState));
373  HANDLE_CUDM_ERROR(cudensitymatDestroyState(inputState));
374
375  // Destroy quantum state storage
376  destroyArrayGPU(inputStateAdjElems);
377  destroyArrayGPU(outputStateElems);
378  destroyArrayGPU(inputStateElems);
379
380  // Destroy external Hamiltonian parameters
381  destroyArrayGPU(static_cast<void *>(hamiltonianParamsGrad));
382  destroyArrayGPU(static_cast<void *>(hamiltonianParams));
383
384  if (verbose)
385    std::cout << "Destroyed resources\n" << std::flush;
386}
387
388
389int main(int argc, char ** argv)
390{
391  // Assign a GPU to the process
392  HANDLE_CUDA_ERROR(cudaSetDevice(0));
393  if (verbose)
394    std::cout << "Set active device\n";
395
396  // Create a library handle
397  cudensitymatHandle_t handle;
398  HANDLE_CUDM_ERROR(cudensitymatCreate(&handle));
399  if (verbose)
400    std::cout << "Created a library handle\n";
401
402  // Run the example
403  exampleWorkflow(handle);
404
405  // Destroy the library handle
406  HANDLE_CUDM_ERROR(cudensitymatDestroy(handle));
407  if (verbose)
408    std::cout << "Destroyed the library handle\n";
409
410  HANDLE_CUDA_ERROR(cudaDeviceReset());
411
412  // Done
413  return 0;
414}

Code example (serial execution of MPS-TDVP time propagation)#

The following code example illustrates how to use the cuDensityMat library to propagate a pure MPS state in time under a transverse-field Ising Hamiltonian encoded as an MPO, using the split-scope TDVP method with Krylov subspace exponentiation. The example builds the Hamiltonian MPO, creates input and output MPS states, initializes the input MPS to a Neel state, configures the TDVP and Krylov solver parameters, and then runs a time-stepping loop. The full sample code can be found in the NVIDIA/cuQuantum repository (main serial MPS-TDVP code as well as the utility code).

  1/* Copyright (c) 2024-2026, NVIDIA CORPORATION & AFFILIATES.
  2 *
  3 * SPDX-License-Identifier: BSD-3-Clause
  4 */
  5
  6// MPS-TDVP time propagation example.
  7//
  8// Demonstrates propagating a pure MPS state under a transverse-field Ising
  9// Hamiltonian encoded as an MPO, using the split-scope TDVP method with
 10// Krylov subspace exponentiation.
 11//
 12// Workflow:
 13//  1. Build Hamiltonian as MPO (nearest-neighbor ZZ + transverse X field)
 14//  2. Create input and output MPS states (cudensitymatCreateStateMPS)
 15//  3. Initialize input MPS to a Neel state |0101...>
 16//  4. Create TDVP time propagation object
 17//  5. Configure TDVP / Krylov parameters
 18//  6. Prepare propagation and allocate workspace
 19//  7. Time-stepping loop
 20//  8. Clean up all resources
 21
 22#include <cudensitymat.h>
 23#include "helpers.h"
 24
 25#include <algorithm>
 26#include <cmath>
 27#include <complex>
 28#include <vector>
 29#include <numeric>
 30#include <chrono>
 31#include <iostream>
 32#include <cassert>
 33
 34
 35using Complex = std::complex<double>;
 36constexpr cudaDataType_t DATA_TYPE = CUDA_C_64F;
 37
 38constexpr bool verbose = true;
 39
 40// --- Simulation parameters ---
 41constexpr int32_t  NUM_SITES    = 40;
 42constexpr int64_t  PHYS_DIM     = 2;
 43constexpr int64_t  MAX_BOND_DIM = 128;
 44constexpr int64_t  MPO_BOND_DIM = 3;
 45constexpr int32_t  NUM_STEPS    = 3;
 46constexpr double   DT           = 0.01;
 47constexpr double   J_COUPLING   = 1.0;
 48constexpr double   H_FIELD      = 0.5;
 49
 50
 51// ============================================================================
 52// Transverse-field Ising MPO builder
 53// ============================================================================
 54//
 55// H = -J * sum_{i} Z_i Z_{i+1}  +  h * sum_{i} X_i
 56//
 57// Standard MPO representation with bond dimension 3.
 58// Site tensor mode ordering follows cuDensityMat convention (column-major):
 59//   Left boundary (site 0):     [phys_ket(d), right_bond(bR), phys_bra(d)]
 60//   Interior sites:             [left_bond(bL), phys_ket(d), right_bond(bR), phys_bra(d)]
 61//   Right boundary (site N-1):  [left_bond(bL), phys_ket(d), phys_bra(d)]
 62//
 63// The bulk MPO matrix (indexed by bond dimensions aL, aR) is:
 64//
 65//        |  I      0     0  |
 66//   W =  |  Z      0     0  |
 67//        | h*X   -J*Z    I  |
 68//
 69// Left boundary row-vector  (d x 3 x d):  W_L = [ h*X  -J*Z  I ]
 70// Right boundary col-vector (3 x d x d):  W_R = [ I; Z; h*X ]^T
 71//
 72// Pauli matrices: I = diag(1,1),  X = [[0,1],[1,0]],  Z = diag(1,-1)
 73
 74struct IsingMPO {
 75
 76  std::vector<std::vector<Complex>> hostTensors;
 77  std::vector<void*> gpuPtrs;
 78
 79  void build() {
 80    const Complex zero{0.0, 0.0}, one{1.0, 0.0};
 81    const Complex mone{-1.0, 0.0};
 82    const Complex jc{-J_COUPLING, 0.0};
 83    const Complex hc{H_FIELD, 0.0};
 84
 85    // Pauli matrices stored column-major: mat[col*2 + row]
 86    auto I_mat = [&](int r, int c) -> Complex { return (r == c) ? one : zero; };
 87    auto X_mat = [&](int r, int c) -> Complex { return (r != c) ? one : zero; };
 88    auto Z_mat = [&](int r, int c) -> Complex { return (r == c) ? ((r == 0) ? one : mone) : zero; };
 89
 90    hostTensors.resize(NUM_SITES);
 91    gpuPtrs.resize(NUM_SITES, nullptr);
 92
 93    for (int32_t site = 0; site < NUM_SITES; ++site) {
 94      const int64_t bL = (site == 0) ? 1 : MPO_BOND_DIM;
 95      const int64_t bR = (site == NUM_SITES - 1) ? 1 : MPO_BOND_DIM;
 96      const int64_t vol = bL * PHYS_DIM * PHYS_DIM * bR;
 97      hostTensors[site].assign(vol, zero);
 98
 99      // Library mode ordering (column-major):
100      //   [left_bond, phys_ket, right_bond, phys_bra]
101      // Boundary sites omit the missing bond; bL=1 or bR=1 makes
102      // the degenerate dimension transparent in the flat index.
103      auto idx = [&](int64_t aL, int64_t ket, int64_t aR, int64_t bra) -> int64_t {
104        return aL + bL * (ket + PHYS_DIM * (aR + bR * bra));
105      };
106
107      if (NUM_SITES == 1) {
108        for (int bra = 0; bra < PHYS_DIM; ++bra)
109          for (int ket = 0; ket < PHYS_DIM; ++ket)
110            hostTensors[site][idx(0, ket, 0, bra)] = hc * X_mat(bra, ket);
111      } else if (site == 0) {
112        // Left boundary: row-vector [h*X, -J*Z, I]
113        for (int bra = 0; bra < PHYS_DIM; ++bra)
114          for (int ket = 0; ket < PHYS_DIM; ++ket) {
115            hostTensors[site][idx(0, ket, 0, bra)] = hc * X_mat(bra, ket);
116            hostTensors[site][idx(0, ket, 1, bra)] = jc * Z_mat(bra, ket);
117            hostTensors[site][idx(0, ket, 2, bra)] = I_mat(bra, ket);
118          }
119      } else if (site == NUM_SITES - 1) {
120        // Right boundary: col-vector [I; Z; h*X]^T
121        for (int bra = 0; bra < PHYS_DIM; ++bra)
122          for (int ket = 0; ket < PHYS_DIM; ++ket) {
123            hostTensors[site][idx(0, ket, 0, bra)] = I_mat(bra, ket);
124            hostTensors[site][idx(1, ket, 0, bra)] = Z_mat(bra, ket);
125            hostTensors[site][idx(2, ket, 0, bra)] = hc * X_mat(bra, ket);
126          }
127      } else {
128        // Bulk:
129        //   row 0: [I, 0, 0]
130        //   row 1: [Z, 0, 0]
131        //   row 2: [h*X, -J*Z, I]
132        for (int bra = 0; bra < PHYS_DIM; ++bra)
133          for (int ket = 0; ket < PHYS_DIM; ++ket) {
134            hostTensors[site][idx(0, ket, 0, bra)] = I_mat(bra, ket);
135            hostTensors[site][idx(1, ket, 0, bra)] = Z_mat(bra, ket);
136            hostTensors[site][idx(2, ket, 0, bra)] = hc * X_mat(bra, ket);
137            hostTensors[site][idx(2, ket, 1, bra)] = jc * Z_mat(bra, ket);
138            hostTensors[site][idx(2, ket, 2, bra)] = I_mat(bra, ket);
139          }
140      }
141
142      gpuPtrs[site] = createInitializeArrayGPU(hostTensors[site]);
143    }
144  }
145
146  void destroy() {
147    for (auto & ptr : gpuPtrs) {
148      if (ptr) { destroyArrayGPU(ptr); ptr = nullptr; }
149    }
150  }
151};
152
153
154// ============================================================================
155// Build a Neel-state MPS  |0,1,0,1,...>  with given bond dimensions
156// ============================================================================
157// Each MPS tensor A[site] has shape (bondL, phys, bondR) in column-major.
158// For a product state, only the (0,sigma,0) slice is nonzero:
159//   A[0,sigma,0] = delta(sigma, site%2)
160// Remaining bond indices are zero-padded.
161
162struct NeelMPS {
163
164  std::vector<std::vector<Complex>> hostTensors;
165  std::vector<void*> gpuPtrs;
166
167  void build(const std::vector<int64_t>& bondDims) {
168    const Complex zero{0.0, 0.0}, one{1.0, 0.0};
169
170    hostTensors.resize(NUM_SITES);
171    gpuPtrs.resize(NUM_SITES, nullptr);
172
173    for (int32_t site = 0; site < NUM_SITES; ++site) {
174      const int64_t bL = (site == 0) ? 1 : bondDims[site - 1];
175      const int64_t bR = (site == NUM_SITES - 1) ? 1 : bondDims[site];
176      const int64_t vol = bL * PHYS_DIM * bR;
177      hostTensors[site].assign(vol, zero);
178
179      // T[aL, sigma, aR]  column-major
180      auto idx = [&](int64_t aL, int64_t sigma, int64_t aR) -> int64_t {
181        return aL + bL * (sigma + PHYS_DIM * aR);
182      };
183
184      const int64_t neelSpin = site % 2;  // 0 for even sites, 1 for odd
185      hostTensors[site][idx(0, neelSpin, 0)] = one;
186
187      gpuPtrs[site] = createInitializeArrayGPU(hostTensors[site]);
188    }
189  }
190
191  void destroy() {
192    for (auto & ptr : gpuPtrs) {
193      if (ptr) { destroyArrayGPU(ptr); ptr = nullptr; }
194    }
195  }
196};
197
198
199// ============================================================================
200// Example workflow
201// ============================================================================
202
203void exampleWorkflow(cudensitymatHandle_t handle)
204{
205  // --- 1. Build the transverse-field Ising MPO ---
206  IsingMPO mpo;
207  mpo.build();
208  if (verbose)
209    std::cout << "Built transverse-field Ising MPO (bond dim " << MPO_BOND_DIM << ")\n";
210
211  const std::vector<int64_t> spaceShape(NUM_SITES, PHYS_DIM);
212  std::vector<int64_t> mpoBondDims(NUM_SITES - 1, MPO_BOND_DIM);
213
214  cudensitymatMatrixProductOperator_t mpoHandle;
215  std::vector<cudensitymatWrappedTensorCallback_t> mpoCallbacks(NUM_SITES, cudensitymatTensorCallbackNone);
216  std::vector<cudensitymatWrappedTensorGradientCallback_t> mpoGradCallbacks(NUM_SITES, cudensitymatTensorGradientCallbackNone);
217  HANDLE_CUDM_ERROR(cudensitymatCreateMatrixProductOperator(handle,
218                      NUM_SITES,
219                      spaceShape.data(),
220                      CUDENSITYMAT_BOUNDARY_CONDITION_OPEN,
221                      mpoBondDims.data(),
222                      DATA_TYPE,
223                      mpo.gpuPtrs.data(),
224                      mpoCallbacks.data(),
225                      mpoGradCallbacks.data(),
226                      &mpoHandle));
227  if (verbose)
228    std::cout << "Created MPO handle\n";
229
230  // --- 2. Build the Operator from the MPO ---
231  cudensitymatOperatorTerm_t operatorTerm;
232  HANDLE_CUDM_ERROR(cudensitymatCreateOperatorTerm(handle,
233                      NUM_SITES,
234                      spaceShape.data(),
235                      &operatorTerm));
236
237  std::vector<int32_t> modesActedOn(NUM_SITES);
238  std::iota(modesActedOn.begin(), modesActedOn.end(), 0);
239  std::vector<int32_t> modeDuality(NUM_SITES, 0);
240  std::vector<int32_t> mpoConjugation = {0};
241
242  HANDLE_CUDM_ERROR(cudensitymatOperatorTermAppendMPOProduct(handle,
243                      operatorTerm,
244                      1,
245                      &mpoHandle,
246                      mpoConjugation.data(),
247                      modesActedOn.data(),
248                      modeDuality.data(),
249                      make_cuDoubleComplex(1.0, 0.0),
250                      cudensitymatScalarCallbackNone,
251                      cudensitymatScalarGradientCallbackNone));
252
253  cudensitymatOperator_t hamiltonian;
254  HANDLE_CUDM_ERROR(cudensitymatCreateOperator(handle,
255                      NUM_SITES,
256                      spaceShape.data(),
257                      &hamiltonian));
258  HANDLE_CUDM_ERROR(cudensitymatOperatorAppendTerm(handle,
259                      hamiltonian,
260                      operatorTerm,
261                      0,
262                      make_cuDoubleComplex(1.0, 0.0),
263                      cudensitymatScalarCallbackNone,
264                      cudensitymatScalarGradientCallbackNone));
265  if (verbose)
266    std::cout << "Constructed Hamiltonian operator from MPO\n";
267
268  // --- 3. Create input and output MPS states ---
269  const int64_t batchSize = 1;
270
271  // For OBC, cap bond dimensions by the exact Hilbert space dimension on each side.
272  std::vector<int64_t> mpsBondDims(NUM_SITES - 1);
273  for (int32_t i = 0; i < NUM_SITES - 1; ++i) {
274    int64_t leftDim = 1;
275    for (int32_t j = 0; j <= i; ++j) leftDim *= spaceShape[j];
276    int64_t rightDim = 1;
277    for (int32_t j = i + 1; j < NUM_SITES; ++j) rightDim *= spaceShape[j];
278    mpsBondDims[i] = std::min({MAX_BOND_DIM, leftDim, rightDim});
279  }
280
281  cudensitymatState_t stateIn, stateOut;
282  HANDLE_CUDM_ERROR(cudensitymatCreateStateMPS(handle,
283                      CUDENSITYMAT_STATE_PURITY_PURE,
284                      NUM_SITES,
285                      spaceShape.data(),
286                      CUDENSITYMAT_BOUNDARY_CONDITION_OPEN,
287                      mpsBondDims.data(),
288                      DATA_TYPE,
289                      batchSize,
290                      &stateIn));
291  HANDLE_CUDM_ERROR(cudensitymatCreateStateMPS(handle,
292                      CUDENSITYMAT_STATE_PURITY_PURE,
293                      NUM_SITES,
294                      spaceShape.data(),
295                      CUDENSITYMAT_BOUNDARY_CONDITION_OPEN,
296                      mpsBondDims.data(),
297                      DATA_TYPE,
298                      batchSize,
299                      &stateOut));
300
301  // Query number of MPS components (= NUM_SITES tensors)
302  int32_t numComponents = 0;
303  HANDLE_CUDM_ERROR(cudensitymatStateGetNumComponents(handle, stateIn, &numComponents));
304  assert(numComponents == NUM_SITES);
305
306  // Query storage sizes for each MPS tensor
307  std::vector<std::size_t> componentSizes(numComponents);
308  HANDLE_CUDM_ERROR(cudensitymatStateGetComponentStorageSize(handle,
309                      stateIn, numComponents, componentSizes.data()));
310
311  if (verbose) {
312    std::cout << "MPS state has " << numComponents << " components, sizes (bytes):";
313    for (auto s : componentSizes) std::cout << " " << s;
314    std::cout << "\n";
315  }
316
317  // --- 4. Allocate GPU storage and initialise Neel MPS for stateIn ---
318  NeelMPS neelMps;
319  neelMps.build(mpsBondDims);
320
321  // Allocate empty storage for stateOut
322  std::vector<void*> stateOutBufs(numComponents, nullptr);
323  for (int32_t c = 0; c < numComponents; ++c)
324    stateOutBufs[c] = createArrayGPU<Complex>(componentSizes[c] / sizeof(Complex));
325
326  HANDLE_CUDM_ERROR(cudensitymatStateAttachComponentStorage(handle,
327                      stateIn, numComponents,
328                      neelMps.gpuPtrs.data(), componentSizes.data()));
329  HANDLE_CUDM_ERROR(cudensitymatStateAttachComponentStorage(handle,
330                      stateOut, numComponents,
331                      stateOutBufs.data(), componentSizes.data()));
332
333  if (verbose)
334    std::cout << "Initialized MPS states (Neel state |0101...>)\n";
335
336  // --- 5. Create TDVP time propagation object ---
337  cudensitymatTimePropagation_t timeProp;
338  HANDLE_CUDM_ERROR(cudensitymatCreateTimePropagation(handle,
339                      hamiltonian,
340                      1,  // Hermitian
341                      CUDENSITYMAT_PROPAGATION_SCOPE_SPLIT,
342                      CUDENSITYMAT_PROPAGATION_APPROACH_KRYLOV,
343                      &timeProp));
344  if (verbose)
345    std::cout << "Created TDVP time propagation object\n";
346
347  // --- 6. Configure TDVP and Krylov parameters ---
348  // TDVP config: 2nd-order, 1-site
349  cudensitymatTimePropagationScopeSplitTDVPConfig_t tdvpConfig;
350  HANDLE_CUDM_ERROR(cudensitymatCreateTimePropagationScopeSplitTDVPConfig(handle, &tdvpConfig));
351  {
352    const int32_t order = 2;
353    HANDLE_CUDM_ERROR(cudensitymatTimePropagationScopeSplitTDVPConfigSetAttribute(handle,
354                        tdvpConfig,
355                        CUDENSITYMAT_PROPAGATION_SPLIT_SCOPE_TDVP_ORDER,
356                        &order, sizeof(order)));
357  }
358  HANDLE_CUDM_ERROR(cudensitymatTimePropagationConfigure(handle,
359                      timeProp,
360                      CUDENSITYMAT_PROPAGATION_SPLIT_SCOPE_TDVP_CONFIG,
361                      &tdvpConfig, sizeof(tdvpConfig)));
362
363  // Krylov config
364  cudensitymatTimePropagationApproachKrylovConfig_t krylovConfig;
365  HANDLE_CUDM_ERROR(cudensitymatCreateTimePropagationApproachKrylovConfig(handle, &krylovConfig));
366  {
367    const int32_t maxDim = 10;
368    HANDLE_CUDM_ERROR(cudensitymatTimePropagationApproachKrylovConfigSetAttribute(handle,
369                        krylovConfig,
370                        CUDENSITYMAT_PROPAGATION_APPROACH_KRYLOV_MAX_DIM,
371                        &maxDim, sizeof(maxDim)));
372    const double tol = 1e-8;
373    HANDLE_CUDM_ERROR(cudensitymatTimePropagationApproachKrylovConfigSetAttribute(handle,
374                        krylovConfig,
375                        CUDENSITYMAT_PROPAGATION_APPROACH_KRYLOV_TOLERANCE,
376                        &tol, sizeof(tol)));
377  }
378  HANDLE_CUDM_ERROR(cudensitymatTimePropagationConfigure(handle,
379                      timeProp,
380                      CUDENSITYMAT_PROPAGATION_APPROACH_KRYLOV_CONFIG,
381                      &krylovConfig, sizeof(krylovConfig)));
382  if (verbose)
383    std::cout << "Configured TDVP (order=2, 1-site) with Krylov (max_dim=10, tol=1e-8)\n";
384
385  // --- 7. Prepare propagation and allocate workspace ---
386  cudensitymatWorkspaceDescriptor_t workspaceDescr;
387  HANDLE_CUDM_ERROR(cudensitymatCreateWorkspace(handle, &workspaceDescr));
388
389  std::size_t freeMem = 0, totalMem = 0;
390  HANDLE_CUDA_ERROR(cudaMemGetInfo(&freeMem, &totalMem));
391  freeMem = static_cast<std::size_t>(static_cast<double>(freeMem) * 0.95);
392  if (verbose)
393    std::cout << "Available workspace memory (bytes) = " << freeMem << "\n";
394
395  HANDLE_CUDM_ERROR(cudensitymatTimePropagationPrepare(handle,
396                      timeProp,
397                      stateIn,
398                      stateOut,
399                      CUDENSITYMAT_COMPUTE_64F,
400                      freeMem,
401                      workspaceDescr,
402                      0x0));
403  if (verbose)
404    std::cout << "Prepared time propagation\n";
405
406  // Query and allocate scratch workspace
407  std::size_t scratchSize = 0;
408  HANDLE_CUDM_ERROR(cudensitymatWorkspaceGetMemorySize(handle,
409                      workspaceDescr,
410                      CUDENSITYMAT_MEMSPACE_DEVICE,
411                      CUDENSITYMAT_WORKSPACE_SCRATCH,
412                      &scratchSize));
413  void * scratchBuf = nullptr;
414  if (scratchSize > 0) {
415    HANDLE_CUDA_ERROR(cudaMalloc(&scratchBuf, scratchSize));
416    HANDLE_CUDM_ERROR(cudensitymatWorkspaceSetMemory(handle,
417                        workspaceDescr,
418                        CUDENSITYMAT_MEMSPACE_DEVICE,
419                        CUDENSITYMAT_WORKSPACE_SCRATCH,
420                        scratchBuf, scratchSize));
421  }
422  if (verbose)
423    std::cout << "Scratch workspace (bytes) = " << scratchSize << "\n";
424
425  // --- 8. Time-stepping loop ---
426  if (verbose) {
427    std::cout << "\nStarting TDVP propagation: " << NUM_STEPS << " steps, dt = " << DT << "\n";
428    std::cout << "Hamiltonian: H = -" << J_COUPLING << " sum Z_i Z_{i+1} + "
429              << H_FIELD << " sum X_i\n\n";
430  }
431
432  HANDLE_CUDA_ERROR(cudaDeviceSynchronize());
433  const auto wallStart = std::chrono::high_resolution_clock::now();
434
435  for (int32_t step = 0; step < NUM_STEPS; ++step) {
436    const double currentTime = step * DT;
437
438    HANDLE_CUDM_ERROR(cudensitymatTimePropagationCompute(handle,
439                        timeProp,
440                        DT,      // timeStepReal
441                        0.0,     // timeStepImag
442                        currentTime,
443                        batchSize,
444                        0,       // numParams
445                        nullptr, // params
446                        stateIn,
447                        stateOut,
448                        workspaceDescr,
449                        0x0));
450
451    // Swap input/output for next step (double-buffering)
452    std::swap(stateIn, stateOut);
453  }
454
455  HANDLE_CUDA_ERROR(cudaDeviceSynchronize());
456  const auto wallEnd = std::chrono::high_resolution_clock::now();
457  const std::chrono::duration<double> elapsed = wallEnd - wallStart;
458  if (verbose)
459    std::cout << "\nTotal propagation wall time (sec) = " << elapsed.count() << "\n";
460
461  // --- 9. Clean up ---
462  if (scratchBuf)
463    HANDLE_CUDA_ERROR(cudaFree(scratchBuf));
464  HANDLE_CUDM_ERROR(cudensitymatDestroyWorkspace(workspaceDescr));
465  HANDLE_CUDM_ERROR(cudensitymatDestroyTimePropagation(timeProp));
466  HANDLE_CUDM_ERROR(cudensitymatDestroyTimePropagationApproachKrylovConfig(krylovConfig));
467  HANDLE_CUDM_ERROR(cudensitymatDestroyTimePropagationScopeSplitTDVPConfig(tdvpConfig));
468  HANDLE_CUDM_ERROR(cudensitymatDestroyOperator(hamiltonian));
469  HANDLE_CUDM_ERROR(cudensitymatDestroyOperatorTerm(operatorTerm));
470  HANDLE_CUDM_ERROR(cudensitymatDestroyMatrixProductOperator(mpoHandle));
471  HANDLE_CUDM_ERROR(cudensitymatDestroyState(stateOut));
472  HANDLE_CUDM_ERROR(cudensitymatDestroyState(stateIn));
473
474  for (auto buf : stateOutBufs)
475    destroyArrayGPU(buf);
476  neelMps.destroy();
477  mpo.destroy();
478
479  if (verbose)
480    std::cout << "Destroyed all resources\n";
481}
482
483
484int main(int argc, char ** argv)
485{
486  HANDLE_CUDA_ERROR(cudaSetDevice(0));
487  if (verbose)
488    std::cout << "Set active device\n";
489
490  cudensitymatHandle_t handle;
491  HANDLE_CUDM_ERROR(cudensitymatCreate(&handle));
492  if (verbose)
493    std::cout << "Created library handle\n";
494
495  exampleWorkflow(handle);
496
497  HANDLE_CUDM_ERROR(cudensitymatDestroy(handle));
498  if (verbose)
499    std::cout << "Destroyed library handle\n";
500
501  HANDLE_CUDA_ERROR(cudaDeviceReset());
502  return 0;
503}

Code example (serial execution of two-site MPS-TDVP time propagation)#

The following code example illustrates the two-site variant of the split-scope TDVP method, selected via the CUDENSITYMAT_PROPAGATION_SPLIT_SCOPE_TDVP_NUM_SITES attribute set to 2. Unlike the single-site update, the two-site update can grow the MPS bond extents dynamically: each evolved two-site block is re-split via a truncated SVD whose policy is supplied through an attached cudensitymatSVDConfig_t (here with a global cap via CUDENSITYMAT_SVD_CONFIG_MAX_EXTENT). The example starts from a polarized product state with small initial current bond extents recorded via cudensitymatStateMPSSetCurrentBondExtents(), and after each step queries cudensitymatStateMPSGetCurrentBondExtents() and compares the current extents against the maximum (buffer) extents to monitor bond growth and truncation. The full sample code can be found in the NVIDIA/cuQuantum repository (main serial two-site MPS-TDVP code as well as the utility code).

  1/* Copyright (c) 2024-2026, NVIDIA CORPORATION & AFFILIATES.
  2 *
  3 * SPDX-License-Identifier: BSD-3-Clause
  4 */
  5
  6// Two-site MPS-TDVP time propagation example.
  7//
  8// Demonstrates propagating a pure MPS state under a transverse-field Ising
  9// Hamiltonian encoded as an MPO, using the split-scope TDVP method with the
 10// TWO-site local update (CUDENSITYMAT_PROPAGATION_SPLIT_SCOPE_TDVP_NUM_SITES = 2)
 11// and Krylov subspace exponentiation.
 12//
 13// Unlike the single-site companion (mps_tdvp_example.cpp), the two-site sweep
 14// can dynamically grow the MPS bond dimensions: each two-site block is evolved
 15// and then re-split via a truncated SVD. This example therefore additionally:
 16//   * starts from small current bond extents (> 1, below the buffer maximum)
 17//     via cudensitymatStateMPSSetCurrentBondExtents;
 18//   * attaches a TDVP SVD configuration with a maximum-extent cap
 19//     (CUDENSITYMAT_SVD_CONFIG_MAX_EXTENT); and
 20//   * after each step queries cudensitymatStateMPSGetCurrentBondExtents and
 21//     compares against the maximum extents to monitor bond growth.
 22//
 23// Workflow:
 24//  1. Build Hamiltonian as MPO (nearest-neighbor ZZ + transverse X field)
 25//  2. Create input and output MPS states (cudensitymatCreateStateMPS)
 26//  3. Initialize input MPS to a polarized product state |000...0> and set small
 27//     current bond extents
 28//  4. Create TDVP time propagation object
 29//  5. Configure TDVP (num_sites = 2 + SVD config) / Krylov parameters
 30//  6. Prepare propagation and allocate workspace
 31//  7. Time-stepping loop; after each step monitor current vs. maximum extents
 32//  8. Clean up all resources
 33
 34#include <cudensitymat.h>
 35#include "helpers.h"
 36
 37#include <algorithm>
 38#include <cmath>
 39#include <complex>
 40#include <vector>
 41#include <numeric>
 42#include <chrono>
 43#include <iostream>
 44#include <cassert>
 45
 46
 47using Complex = std::complex<double>;
 48constexpr cudaDataType_t DATA_TYPE = CUDA_C_64F;
 49
 50constexpr bool verbose = true;
 51
 52// --- Simulation parameters ---
 53constexpr int32_t  NUM_SITES        = 8;
 54constexpr int64_t  PHYS_DIM         = 2;
 55constexpr int64_t  MAX_BOND_DIM     = 8;    // maximum (buffer) bond dimension cap
 56constexpr int64_t  MPO_BOND_DIM     = 3;
 57constexpr int64_t  INITIAL_BOND_DIM = 2;    // small initial current bond dimension (> 1)
 58constexpr int32_t  NUM_STEPS        = 5;
 59constexpr double   DT               = 0.05;
 60constexpr double   J_COUPLING       = 1.0;
 61constexpr double   H_FIELD          = 0.5;
 62
 63
 64// ============================================================================
 65// Transverse-field Ising MPO builder
 66// ============================================================================
 67//
 68// H = -J * sum_{i} Z_i Z_{i+1}  +  h * sum_{i} X_i
 69//
 70// Standard MPO representation with bond dimension 3.
 71// Site tensor mode ordering follows cuDensityMat convention (column-major):
 72//   Left boundary (site 0):     [phys_ket(d), right_bond(bR), phys_bra(d)]
 73//   Interior sites:             [left_bond(bL), phys_ket(d), right_bond(bR), phys_bra(d)]
 74//   Right boundary (site N-1):  [left_bond(bL), phys_ket(d), phys_bra(d)]
 75//
 76// The bulk MPO matrix (indexed by bond dimensions aL, aR) is:
 77//
 78//        |  I      0     0  |
 79//   W =  |  Z      0     0  |
 80//        | h*X   -J*Z    I  |
 81//
 82// Pauli matrices: I = diag(1,1),  X = [[0,1],[1,0]],  Z = diag(1,-1)
 83
 84struct IsingMPO {
 85
 86  std::vector<std::vector<Complex>> hostTensors;
 87  std::vector<void*> gpuPtrs;
 88
 89  void build() {
 90    const Complex zero{0.0, 0.0}, one{1.0, 0.0};
 91    const Complex mone{-1.0, 0.0};
 92    const Complex jc{-J_COUPLING, 0.0};
 93    const Complex hc{H_FIELD, 0.0};
 94
 95    auto I_mat = [&](int r, int c) -> Complex { return (r == c) ? one : zero; };
 96    auto X_mat = [&](int r, int c) -> Complex { return (r != c) ? one : zero; };
 97    auto Z_mat = [&](int r, int c) -> Complex { return (r == c) ? ((r == 0) ? one : mone) : zero; };
 98
 99    hostTensors.resize(NUM_SITES);
100    gpuPtrs.resize(NUM_SITES, nullptr);
101
102    for (int32_t site = 0; site < NUM_SITES; ++site) {
103      const int64_t bL = (site == 0) ? 1 : MPO_BOND_DIM;
104      const int64_t bR = (site == NUM_SITES - 1) ? 1 : MPO_BOND_DIM;
105      const int64_t vol = bL * PHYS_DIM * PHYS_DIM * bR;
106      hostTensors[site].assign(vol, zero);
107
108      // Library mode ordering (column-major): [left_bond, phys_ket, right_bond, phys_bra]
109      auto idx = [&](int64_t aL, int64_t ket, int64_t aR, int64_t bra) -> int64_t {
110        return aL + bL * (ket + PHYS_DIM * (aR + bR * bra));
111      };
112
113      if (site == 0) {
114        // Left boundary: row-vector [h*X, -J*Z, I]
115        for (int bra = 0; bra < PHYS_DIM; ++bra)
116          for (int ket = 0; ket < PHYS_DIM; ++ket) {
117            hostTensors[site][idx(0, ket, 0, bra)] = hc * X_mat(bra, ket);
118            hostTensors[site][idx(0, ket, 1, bra)] = jc * Z_mat(bra, ket);
119            hostTensors[site][idx(0, ket, 2, bra)] = I_mat(bra, ket);
120          }
121      } else if (site == NUM_SITES - 1) {
122        // Right boundary: col-vector [I; Z; h*X]^T
123        for (int bra = 0; bra < PHYS_DIM; ++bra)
124          for (int ket = 0; ket < PHYS_DIM; ++ket) {
125            hostTensors[site][idx(0, ket, 0, bra)] = I_mat(bra, ket);
126            hostTensors[site][idx(1, ket, 0, bra)] = Z_mat(bra, ket);
127            hostTensors[site][idx(2, ket, 0, bra)] = hc * X_mat(bra, ket);
128          }
129      } else {
130        // Bulk:
131        //   row 0: [I, 0, 0]
132        //   row 1: [Z, 0, 0]
133        //   row 2: [h*X, -J*Z, I]
134        for (int bra = 0; bra < PHYS_DIM; ++bra)
135          for (int ket = 0; ket < PHYS_DIM; ++ket) {
136            hostTensors[site][idx(0, ket, 0, bra)] = I_mat(bra, ket);
137            hostTensors[site][idx(1, ket, 0, bra)] = Z_mat(bra, ket);
138            hostTensors[site][idx(2, ket, 0, bra)] = hc * X_mat(bra, ket);
139            hostTensors[site][idx(2, ket, 1, bra)] = jc * Z_mat(bra, ket);
140            hostTensors[site][idx(2, ket, 2, bra)] = I_mat(bra, ket);
141          }
142      }
143
144      gpuPtrs[site] = createInitializeArrayGPU(hostTensors[site]);
145    }
146  }
147
148  void destroy() {
149    for (auto & ptr : gpuPtrs) {
150      if (ptr) { destroyArrayGPU(ptr); ptr = nullptr; }
151    }
152  }
153};
154
155
156// ============================================================================
157// Build a polarized product-state MPS  |000...0>  with given (maximum/buffer)
158// bond extents
159// ============================================================================
160// Each MPS tensor A[site] has shape (bondL, phys, bondR) in column-major over
161// the MAXIMUM (buffer) extents. For a product state only the (0,sigma,0) slice
162// is nonzero; the smaller current bond extents (set separately) leave the rest
163// of the buffer zero-padded.
164
165struct ProductMPS {
166
167  std::vector<std::vector<Complex>> hostTensors;
168  std::vector<void*> gpuPtrs;
169
170  void build(const std::vector<int64_t>& maxBondDims) {
171    const Complex zero{0.0, 0.0}, one{1.0, 0.0};
172
173    hostTensors.resize(NUM_SITES);
174    gpuPtrs.resize(NUM_SITES, nullptr);
175
176    for (int32_t site = 0; site < NUM_SITES; ++site) {
177      const int64_t bL = (site == 0) ? 1 : maxBondDims[site - 1];
178      const int64_t bR = (site == NUM_SITES - 1) ? 1 : maxBondDims[site];
179      const int64_t vol = bL * PHYS_DIM * bR;
180      hostTensors[site].assign(vol, zero);
181
182      // T[aL, sigma, aR]  column-major over the maximum extents.
183      auto idx = [&](int64_t aL, int64_t sigma, int64_t aR) -> int64_t {
184        return aL + bL * (sigma + PHYS_DIM * aR);
185      };
186
187      hostTensors[site][idx(0, 0, 0)] = one;  // |0> on each site (product state)
188
189      gpuPtrs[site] = createInitializeArrayGPU(hostTensors[site]);
190    }
191  }
192
193  void destroy() {
194    for (auto & ptr : gpuPtrs) {
195      if (ptr) { destroyArrayGPU(ptr); ptr = nullptr; }
196    }
197  }
198};
199
200
201// ============================================================================
202// Example workflow
203// ============================================================================
204
205void exampleWorkflow(cudensitymatHandle_t handle)
206{
207  // --- 1. Build the transverse-field Ising MPO ---
208  IsingMPO mpo;
209  mpo.build();
210  if (verbose)
211    std::cout << "Built transverse-field Ising MPO (bond dim " << MPO_BOND_DIM << ")\n";
212
213  const std::vector<int64_t> spaceShape(NUM_SITES, PHYS_DIM);
214  std::vector<int64_t> mpoBondDims(NUM_SITES - 1, MPO_BOND_DIM);
215
216  cudensitymatMatrixProductOperator_t mpoHandle;
217  std::vector<cudensitymatWrappedTensorCallback_t> mpoCallbacks(NUM_SITES, cudensitymatTensorCallbackNone);
218  std::vector<cudensitymatWrappedTensorGradientCallback_t> mpoGradCallbacks(NUM_SITES, cudensitymatTensorGradientCallbackNone);
219  HANDLE_CUDM_ERROR(cudensitymatCreateMatrixProductOperator(handle,
220                      NUM_SITES,
221                      spaceShape.data(),
222                      CUDENSITYMAT_BOUNDARY_CONDITION_OPEN,
223                      mpoBondDims.data(),
224                      DATA_TYPE,
225                      mpo.gpuPtrs.data(),
226                      mpoCallbacks.data(),
227                      mpoGradCallbacks.data(),
228                      &mpoHandle));
229  if (verbose)
230    std::cout << "Created MPO handle\n";
231
232  // --- 2. Build the Operator from the MPO ---
233  cudensitymatOperatorTerm_t operatorTerm;
234  HANDLE_CUDM_ERROR(cudensitymatCreateOperatorTerm(handle,
235                      NUM_SITES,
236                      spaceShape.data(),
237                      &operatorTerm));
238
239  std::vector<int32_t> modesActedOn(NUM_SITES);
240  std::iota(modesActedOn.begin(), modesActedOn.end(), 0);
241  std::vector<int32_t> modeDuality(NUM_SITES, 0);
242  std::vector<int32_t> mpoConjugation = {0};
243
244  HANDLE_CUDM_ERROR(cudensitymatOperatorTermAppendMPOProduct(handle,
245                      operatorTerm,
246                      1,
247                      &mpoHandle,
248                      mpoConjugation.data(),
249                      modesActedOn.data(),
250                      modeDuality.data(),
251                      make_cuDoubleComplex(1.0, 0.0),
252                      cudensitymatScalarCallbackNone,
253                      cudensitymatScalarGradientCallbackNone));
254
255  cudensitymatOperator_t hamiltonian;
256  HANDLE_CUDM_ERROR(cudensitymatCreateOperator(handle,
257                      NUM_SITES,
258                      spaceShape.data(),
259                      &hamiltonian));
260  HANDLE_CUDM_ERROR(cudensitymatOperatorAppendTerm(handle,
261                      hamiltonian,
262                      operatorTerm,
263                      0,
264                      make_cuDoubleComplex(1.0, 0.0),
265                      cudensitymatScalarCallbackNone,
266                      cudensitymatScalarGradientCallbackNone));
267  if (verbose)
268    std::cout << "Constructed Hamiltonian operator from MPO\n";
269
270  // --- 3. Create input and output MPS states ---
271  const int64_t batchSize = 1;
272
273  // Maximum (buffer) bond extents, fixed at state creation: capped by the exact
274  // Hilbert space dimension on each side and by MAX_BOND_DIM.
275  std::vector<int64_t> maxBondDims(NUM_SITES - 1);
276  for (int32_t i = 0; i < NUM_SITES - 1; ++i) {
277    int64_t leftDim = 1;
278    for (int32_t j = 0; j <= i; ++j) leftDim *= spaceShape[j];
279    int64_t rightDim = 1;
280    for (int32_t j = i + 1; j < NUM_SITES; ++j) rightDim *= spaceShape[j];
281    maxBondDims[i] = std::min({MAX_BOND_DIM, leftDim, rightDim});
282  }
283
284  // Initial current bond extents: > 1, clamped to the maximum, so the two-site
285  // sweep starts below the buffer maximum and can grow the bonds.
286  std::vector<int64_t> initialBondDims(NUM_SITES - 1);
287  for (int32_t i = 0; i < NUM_SITES - 1; ++i)
288    initialBondDims[i] = std::min<int64_t>(INITIAL_BOND_DIM, maxBondDims[i]);
289
290  cudensitymatState_t stateIn, stateOut;
291  HANDLE_CUDM_ERROR(cudensitymatCreateStateMPS(handle,
292                      CUDENSITYMAT_STATE_PURITY_PURE,
293                      NUM_SITES,
294                      spaceShape.data(),
295                      CUDENSITYMAT_BOUNDARY_CONDITION_OPEN,
296                      maxBondDims.data(),
297                      DATA_TYPE,
298                      batchSize,
299                      &stateIn));
300  HANDLE_CUDM_ERROR(cudensitymatCreateStateMPS(handle,
301                      CUDENSITYMAT_STATE_PURITY_PURE,
302                      NUM_SITES,
303                      spaceShape.data(),
304                      CUDENSITYMAT_BOUNDARY_CONDITION_OPEN,
305                      maxBondDims.data(),
306                      DATA_TYPE,
307                      batchSize,
308                      &stateOut));
309
310  // Query number of MPS components (= NUM_SITES tensors)
311  int32_t numComponents = 0;
312  HANDLE_CUDM_ERROR(cudensitymatStateGetNumComponents(handle, stateIn, &numComponents));
313  assert(numComponents == NUM_SITES);
314
315  // Query storage sizes for each MPS tensor (sized for the MAXIMUM extents)
316  std::vector<std::size_t> componentSizes(numComponents);
317  HANDLE_CUDM_ERROR(cudensitymatStateGetComponentStorageSize(handle,
318                      stateIn, numComponents, componentSizes.data()));
319
320  if (verbose) {
321    std::cout << "MPS state has " << numComponents << " components, sizes (bytes):";
322    for (auto s : componentSizes) std::cout << " " << s;
323    std::cout << "\n";
324  }
325
326  // --- 4. Allocate GPU storage and initialise the product-state MPS for stateIn ---
327  ProductMPS productMps;
328  productMps.build(maxBondDims);
329
330  // Allocate empty storage for stateOut
331  std::vector<void*> stateOutBufs(numComponents, nullptr);
332  for (int32_t c = 0; c < numComponents; ++c)
333    stateOutBufs[c] = createArrayGPU<Complex>(componentSizes[c] / sizeof(Complex));
334
335  HANDLE_CUDM_ERROR(cudensitymatStateAttachComponentStorage(handle,
336                      stateIn, numComponents,
337                      productMps.gpuPtrs.data(), componentSizes.data()));
338  HANDLE_CUDM_ERROR(cudensitymatStateAttachComponentStorage(handle,
339                      stateOut, numComponents,
340                      stateOutBufs.data(), componentSizes.data()));
341
342  // Record the small initial CURRENT (valid) bond extents (metadata only).
343  HANDLE_CUDM_ERROR(cudensitymatStateMPSSetCurrentBondExtents(handle,
344                      stateIn, initialBondDims.data()));
345  HANDLE_CUDM_ERROR(cudensitymatStateMPSSetCurrentBondExtents(handle,
346                      stateOut, initialBondDims.data()));
347
348  if (verbose) {
349    std::cout << "Initialized MPS states (polarized product state |000...0>)\n";
350    std::cout << "Maximum (buffer) bond extents:";
351    for (auto b : maxBondDims) std::cout << " " << b;
352    std::cout << "\nInitial current bond extents:";
353    for (auto b : initialBondDims) std::cout << " " << b;
354    std::cout << "\n";
355  }
356
357  // --- 5. Create TDVP time propagation object ---
358  cudensitymatTimePropagation_t timeProp;
359  HANDLE_CUDM_ERROR(cudensitymatCreateTimePropagation(handle,
360                      hamiltonian,
361                      1,  // Hermitian
362                      CUDENSITYMAT_PROPAGATION_SCOPE_SPLIT,
363                      CUDENSITYMAT_PROPAGATION_APPROACH_KRYLOV,
364                      &timeProp));
365  if (verbose)
366    std::cout << "Created TDVP time propagation object\n";
367
368  // --- 6. Configure TDVP and Krylov parameters ---
369  // TDVP config: 2nd-order, TWO-site, with an attached SVD truncation policy.
370  cudensitymatTimePropagationScopeSplitTDVPConfig_t tdvpConfig;
371  HANDLE_CUDM_ERROR(cudensitymatCreateTimePropagationScopeSplitTDVPConfig(handle, &tdvpConfig));
372  {
373    const int32_t order = 2;
374    HANDLE_CUDM_ERROR(cudensitymatTimePropagationScopeSplitTDVPConfigSetAttribute(handle,
375                        tdvpConfig,
376                        CUDENSITYMAT_PROPAGATION_SPLIT_SCOPE_TDVP_ORDER,
377                        &order, sizeof(order)));
378    const int32_t numSites = 2;
379    HANDLE_CUDM_ERROR(cudensitymatTimePropagationScopeSplitTDVPConfigSetAttribute(handle,
380                        tdvpConfig,
381                        CUDENSITYMAT_PROPAGATION_SPLIT_SCOPE_TDVP_NUM_SITES,
382                        &numSites, sizeof(numSites)));
383  }
384
385  // SVD truncation policy for the two-site splits (captured by value at Configure
386  // time, so it can be destroyed right after). Here just a global bond cap.
387  cudensitymatSVDConfig_t svdConfig;
388  HANDLE_CUDM_ERROR(cudensitymatCreateSVDConfig(handle, &svdConfig));
389  {
390    const int64_t svdMaxExtent = MAX_BOND_DIM;
391    HANDLE_CUDM_ERROR(cudensitymatSVDConfigSetAttribute(handle, svdConfig,
392                        CUDENSITYMAT_SVD_CONFIG_MAX_EXTENT,
393                        &svdMaxExtent, sizeof(svdMaxExtent)));
394  }
395  HANDLE_CUDM_ERROR(cudensitymatTimePropagationScopeSplitTDVPConfigSetAttribute(handle,
396                      tdvpConfig,
397                      CUDENSITYMAT_PROPAGATION_SPLIT_SCOPE_TDVP_SVD_CONFIG,
398                      &svdConfig, sizeof(svdConfig)));
399
400  HANDLE_CUDM_ERROR(cudensitymatTimePropagationConfigure(handle,
401                      timeProp,
402                      CUDENSITYMAT_PROPAGATION_SPLIT_SCOPE_TDVP_CONFIG,
403                      &tdvpConfig, sizeof(tdvpConfig)));
404
405  HANDLE_CUDM_ERROR(cudensitymatDestroySVDConfig(svdConfig));
406
407  // Krylov config
408  cudensitymatTimePropagationApproachKrylovConfig_t krylovConfig;
409  HANDLE_CUDM_ERROR(cudensitymatCreateTimePropagationApproachKrylovConfig(handle, &krylovConfig));
410  {
411    const int32_t maxDim = 10;
412    HANDLE_CUDM_ERROR(cudensitymatTimePropagationApproachKrylovConfigSetAttribute(handle,
413                        krylovConfig,
414                        CUDENSITYMAT_PROPAGATION_APPROACH_KRYLOV_MAX_DIM,
415                        &maxDim, sizeof(maxDim)));
416    const double tol = 1e-8;
417    HANDLE_CUDM_ERROR(cudensitymatTimePropagationApproachKrylovConfigSetAttribute(handle,
418                        krylovConfig,
419                        CUDENSITYMAT_PROPAGATION_APPROACH_KRYLOV_TOLERANCE,
420                        &tol, sizeof(tol)));
421  }
422  HANDLE_CUDM_ERROR(cudensitymatTimePropagationConfigure(handle,
423                      timeProp,
424                      CUDENSITYMAT_PROPAGATION_APPROACH_KRYLOV_CONFIG,
425                      &krylovConfig, sizeof(krylovConfig)));
426  if (verbose)
427    std::cout << "Configured TDVP (order=2, num_sites=2, SVD max_extent=" << MAX_BOND_DIM
428              << ") with Krylov (max_dim=10, tol=1e-8)\n";
429
430  // --- 7. Prepare propagation and allocate workspace ---
431  cudensitymatWorkspaceDescriptor_t workspaceDescr;
432  HANDLE_CUDM_ERROR(cudensitymatCreateWorkspace(handle, &workspaceDescr));
433
434  std::size_t freeMem = 0, totalMem = 0;
435  HANDLE_CUDA_ERROR(cudaMemGetInfo(&freeMem, &totalMem));
436  freeMem = static_cast<std::size_t>(static_cast<double>(freeMem) * 0.95);
437  if (verbose)
438    std::cout << "Available workspace memory (bytes) = " << freeMem << "\n";
439
440  HANDLE_CUDM_ERROR(cudensitymatTimePropagationPrepare(handle,
441                      timeProp,
442                      stateIn,
443                      stateOut,
444                      CUDENSITYMAT_COMPUTE_64F,
445                      freeMem,
446                      workspaceDescr,
447                      0x0));
448  if (verbose)
449    std::cout << "Prepared time propagation\n";
450
451  // Query and allocate scratch workspace
452  std::size_t scratchSize = 0;
453  HANDLE_CUDM_ERROR(cudensitymatWorkspaceGetMemorySize(handle,
454                      workspaceDescr,
455                      CUDENSITYMAT_MEMSPACE_DEVICE,
456                      CUDENSITYMAT_WORKSPACE_SCRATCH,
457                      &scratchSize));
458  void * scratchBuf = nullptr;
459  if (scratchSize > 0) {
460    HANDLE_CUDA_ERROR(cudaMalloc(&scratchBuf, scratchSize));
461    HANDLE_CUDM_ERROR(cudensitymatWorkspaceSetMemory(handle,
462                        workspaceDescr,
463                        CUDENSITYMAT_MEMSPACE_DEVICE,
464                        CUDENSITYMAT_WORKSPACE_SCRATCH,
465                        scratchBuf, scratchSize));
466  }
467  if (verbose)
468    std::cout << "Scratch workspace (bytes) = " << scratchSize << "\n";
469
470  // --- 8. Time-stepping loop ---
471  if (verbose) {
472    std::cout << "\nStarting 2-site TDVP propagation: " << NUM_STEPS << " steps, dt = " << DT << "\n";
473    std::cout << "Hamiltonian: H = -" << J_COUPLING << " sum Z_i Z_{i+1} + "
474              << H_FIELD << " sum X_i\n\n";
475  }
476
477  HANDLE_CUDA_ERROR(cudaDeviceSynchronize());
478  const auto wallStart = std::chrono::high_resolution_clock::now();
479
480  const int32_t numBonds = NUM_SITES - 1;
481  std::vector<int64_t> currentBonds(numBonds, 0);
482
483  for (int32_t step = 0; step < NUM_STEPS; ++step) {
484    const double currentTime = step * DT;
485
486    HANDLE_CUDM_ERROR(cudensitymatTimePropagationCompute(handle,
487                        timeProp,
488                        DT,      // timeStepReal
489                        0.0,     // timeStepImag
490                        currentTime,
491                        batchSize,
492                        0,       // numParams
493                        nullptr, // params
494                        stateIn,
495                        stateOut,
496                        workspaceDescr,
497                        0x0));
498
499    // Compare the evolved state's current bond extents against the maximum
500    // (buffer) extents to see how the bonds grow / get truncated.
501    HANDLE_CUDM_ERROR(cudensitymatStateMPSGetCurrentBondExtents(handle,
502                        stateOut, currentBonds.data()));
503    if (verbose) {
504      std::cout << "Step " << (step + 1) << "/" << NUM_STEPS
505                << "  current/maximum bond extents:";
506      bool anyTruncated = false;
507      for (int32_t b = 0; b < numBonds; ++b) {
508        std::cout << " " << currentBonds[b] << "/" << maxBondDims[b];
509        if (currentBonds[b] < maxBondDims[b]) anyTruncated = true;
510      }
511      std::cout << (anyTruncated ? "  (some bonds below maximum)" : "  (all bonds at maximum)") << "\n";
512    }
513
514    // Swap input/output for next step (double-buffering)
515    std::swap(stateIn, stateOut);
516  }
517
518  HANDLE_CUDA_ERROR(cudaDeviceSynchronize());
519  const auto wallEnd = std::chrono::high_resolution_clock::now();
520  const std::chrono::duration<double> elapsed = wallEnd - wallStart;
521  if (verbose)
522    std::cout << "\nTotal propagation wall time (sec) = " << elapsed.count() << "\n";
523
524  // --- 9. Clean up ---
525  if (scratchBuf)
526    HANDLE_CUDA_ERROR(cudaFree(scratchBuf));
527  HANDLE_CUDM_ERROR(cudensitymatDestroyWorkspace(workspaceDescr));
528  HANDLE_CUDM_ERROR(cudensitymatDestroyTimePropagation(timeProp));
529  HANDLE_CUDM_ERROR(cudensitymatDestroyTimePropagationApproachKrylovConfig(krylovConfig));
530  HANDLE_CUDM_ERROR(cudensitymatDestroyTimePropagationScopeSplitTDVPConfig(tdvpConfig));
531  HANDLE_CUDM_ERROR(cudensitymatDestroyOperator(hamiltonian));
532  HANDLE_CUDM_ERROR(cudensitymatDestroyOperatorTerm(operatorTerm));
533  HANDLE_CUDM_ERROR(cudensitymatDestroyMatrixProductOperator(mpoHandle));
534  HANDLE_CUDM_ERROR(cudensitymatDestroyState(stateOut));
535  HANDLE_CUDM_ERROR(cudensitymatDestroyState(stateIn));
536
537  for (auto buf : stateOutBufs)
538    destroyArrayGPU(buf);
539  productMps.destroy();
540  mpo.destroy();
541
542  if (verbose)
543    std::cout << "Destroyed all resources\n";
544}
545
546
547int main(int argc, char ** argv)
548{
549  HANDLE_CUDA_ERROR(cudaSetDevice(0));
550  if (verbose)
551    std::cout << "Set active device\n";
552
553  cudensitymatHandle_t handle;
554  HANDLE_CUDM_ERROR(cudensitymatCreate(&handle));
555  if (verbose)
556    std::cout << "Created library handle\n";
557
558  exampleWorkflow(handle);
559
560  HANDLE_CUDM_ERROR(cudensitymatDestroy(handle));
561  if (verbose)
562    std::cout << "Destroyed library handle\n";
563
564  HANDLE_CUDA_ERROR(cudaDeviceReset());
565  return 0;
566}

Code example (serial execution of MPS-MPO operator action via ALS)#

The following code example illustrates how to use the cuDensityMat library to compute the action of a transverse-field Ising Hamiltonian encoded as an MPO on a pure MPS state, fitted via the split-scope variational ALS path (CUDENSITYMAT_FITTING_SCOPE_SPLIT + CUDENSITYMAT_FITTING_APPROACH_LINSOLVE). The example builds the Hamiltonian MPO, creates input and output MPS states, initializes the input MPS to a Néel state, zero-initializes the output MPS via cudensitymatStateInitializeZero (which puts the ALS fitter into the assignment-mode starting-point path), configures the ALS sub-config (max_sweeps, tolerance), prepares the action and computes one step, then reports the output MPS bond extents. The full sample code can be found in the NVIDIA/cuQuantum repository (main serial MPS-MPO action code as well as the utility code).

  1/* Copyright (c) 2024-2026, NVIDIA CORPORATION & AFFILIATES.
  2 *
  3 * SPDX-License-Identifier: BSD-3-Clause
  4 */
  5
  6// MPS-MPO operator-action example.
  7//
  8// Demonstrates applying a 1-D transverse-field Ising MPO to a pure MPS state
  9// via the split-scope variational ALS fitting path:
 10//
 11//   stateOut += alpha * H * stateIn
 12//
 13// All quantum-state inputs are matrix product states (MPS); the Hamiltonian
 14// is encoded as a matrix product operator (MPO). The output MPS is fitted
 15// via 1-site alternating least squares (ALS), so the user controls the
 16// truncation budget by choosing the output MPS bond dimensions.
 17//
 18// Workflow:
 19//  1. Build Hamiltonian as MPO (nearest-neighbor ZZ + transverse X field)
 20//  2. Build single-term Operator wrapping the MPO
 21//  3. Create input and output MPS states (cudensitymatCreateStateMPS)
 22//  4. Initialize input MPS to a Neel state |0101...> and zero-initialise
 23//     stateOut via cudensitymatStateInitializeZero
 24//  5. Create OperatorAction (FITTING_SCOPE_SPLIT + FITTING_APPROACH_LINSOLVE)
 25//  6. (Optional) Configure ALS sub-config (num_sites, max_sweeps, tolerance)
 26//  7. Prepare action and allocate workspace
 27//  8. Compute one operator action at t = 0 (stateOut <- alpha * H * stateIn)
 28//  9. Report the output MPS bond extents
 29// 10. Clean up all resources
 30//
 31// Assignment semantics. The split-scope operator action fits stateOut to
 32// alpha * H * stateIn accumulated on top of the prior contents of stateOut.
 33// Zero-initialising stateOut with cudensitymatStateInitializeZero before the
 34// Compute call therefore makes the result a plain assignment
 35// stateOut = alpha * H * stateIn (up to FP64 fitting accuracy). The example
 36// reports the allocated output MPS component shapes; a schedule-agnostic norm
 37// check should use the public MPS norm API once it is available.
 38
 39#include <cudensitymat.h>
 40#include "helpers.h"
 41
 42#include <algorithm>
 43#include <cmath>
 44#include <complex>
 45#include <vector>
 46#include <numeric>
 47#include <iostream>
 48#include <cassert>
 49
 50
 51using Complex = std::complex<double>;
 52constexpr cudaDataType_t DATA_TYPE = CUDA_C_64F;
 53
 54constexpr bool verbose = true;
 55
 56// --- Simulation parameters ---
 57// 8 qubits, TFIM (J = 1.0, h = 0.5). Output MPS capped at bond dim 16 which
 58// saturates the natural max bond ([2,4,8,16,8,4,2]) for an 8-site qubit chain
 59// and is comfortably above chi_MPO * chi_MPS_in = 3 * 1 = 3.
 60constexpr int32_t  NUM_SITES        = 8;
 61constexpr int64_t  PHYS_DIM         = 2;
 62constexpr int64_t  MAX_OUT_BOND_DIM = 16;
 63constexpr int64_t  MPO_BOND_DIM     = 3;
 64constexpr double   J_COUPLING       = 1.0;
 65constexpr double   H_FIELD          = 0.5;
 66
 67
 68// ============================================================================
 69// Transverse-field Ising MPO builder (bond dim 3)
 70// ============================================================================
 71//
 72// H = -J * sum_{i} Z_i Z_{i+1}  +  h * sum_{i} X_i
 73//
 74// The bulk MPO matrix (indexed by bond dimensions aL, aR) is:
 75//
 76//        |  I      0     0  |
 77//   W =  |  Z      0     0  |
 78//        | h*X   -J*Z    I  |
 79//
 80// Library mode ordering (column-major):
 81//   Left boundary (site 0):     [phys_ket(d), right_bond(bR), phys_bra(d)]   (bL omitted, treated as 1)
 82//   Interior sites:             [left_bond(bL), phys_ket(d), right_bond(bR), phys_bra(d)]
 83//   Right boundary (site N-1):  [left_bond(bL), phys_ket(d), phys_bra(d)]    (bR omitted, treated as 1)
 84
 85struct IsingMPO {
 86
 87  std::vector<std::vector<Complex>> hostTensors;
 88  std::vector<void*> gpuPtrs;
 89
 90  void build() {
 91    const Complex zero{0.0, 0.0}, one{1.0, 0.0}, mone{-1.0, 0.0};
 92    const Complex jc{-J_COUPLING, 0.0};
 93    const Complex hc{H_FIELD, 0.0};
 94
 95    auto I_mat = [&](int r, int c) -> Complex { return (r == c) ? one : zero; };
 96    auto X_mat = [&](int r, int c) -> Complex { return (r != c) ? one : zero; };
 97    auto Z_mat = [&](int r, int c) -> Complex { return (r == c) ? ((r == 0) ? one : mone) : zero; };
 98
 99    hostTensors.resize(NUM_SITES);
100    gpuPtrs.resize(NUM_SITES, nullptr);
101
102    for (int32_t site = 0; site < NUM_SITES; ++site) {
103      const int64_t bL = (site == 0) ? 1 : MPO_BOND_DIM;
104      const int64_t bR = (site == NUM_SITES - 1) ? 1 : MPO_BOND_DIM;
105      const int64_t vol = bL * PHYS_DIM * PHYS_DIM * bR;
106      hostTensors[site].assign(vol, zero);
107
108      auto idx = [&](int64_t aL, int64_t ket, int64_t aR, int64_t bra) -> int64_t {
109        return aL + bL * (ket + PHYS_DIM * (aR + bR * bra));
110      };
111
112      if (NUM_SITES == 1) {
113        for (int bra = 0; bra < PHYS_DIM; ++bra)
114          for (int ket = 0; ket < PHYS_DIM; ++ket)
115            hostTensors[site][idx(0, ket, 0, bra)] = hc * X_mat(bra, ket);
116      } else if (site == 0) {
117        for (int bra = 0; bra < PHYS_DIM; ++bra)
118          for (int ket = 0; ket < PHYS_DIM; ++ket) {
119            hostTensors[site][idx(0, ket, 0, bra)] = hc * X_mat(bra, ket);
120            hostTensors[site][idx(0, ket, 1, bra)] = jc * Z_mat(bra, ket);
121            hostTensors[site][idx(0, ket, 2, bra)] = I_mat(bra, ket);
122          }
123      } else if (site == NUM_SITES - 1) {
124        for (int bra = 0; bra < PHYS_DIM; ++bra)
125          for (int ket = 0; ket < PHYS_DIM; ++ket) {
126            hostTensors[site][idx(0, ket, 0, bra)] = I_mat(bra, ket);
127            hostTensors[site][idx(1, ket, 0, bra)] = Z_mat(bra, ket);
128            hostTensors[site][idx(2, ket, 0, bra)] = hc * X_mat(bra, ket);
129          }
130      } else {
131        for (int bra = 0; bra < PHYS_DIM; ++bra)
132          for (int ket = 0; ket < PHYS_DIM; ++ket) {
133            hostTensors[site][idx(0, ket, 0, bra)] = I_mat(bra, ket);
134            hostTensors[site][idx(1, ket, 0, bra)] = Z_mat(bra, ket);
135            hostTensors[site][idx(2, ket, 0, bra)] = hc * X_mat(bra, ket);
136            hostTensors[site][idx(2, ket, 1, bra)] = jc * Z_mat(bra, ket);
137            hostTensors[site][idx(2, ket, 2, bra)] = I_mat(bra, ket);
138          }
139      }
140
141      gpuPtrs[site] = createInitializeArrayGPU(hostTensors[site]);
142    }
143  }
144
145  void destroy() {
146    for (auto & ptr : gpuPtrs) {
147      if (ptr) { destroyArrayGPU(ptr); ptr = nullptr; }
148    }
149  }
150};
151
152
153// ============================================================================
154// Build a Neel-state MPS  |0,1,0,1,...>  with given bond dimensions
155// ============================================================================
156// Each MPS tensor A[site] has shape (bondL, phys, bondR) in column-major.
157// For a product state, only the (0, site%2, 0) slice is nonzero; remaining
158// bond indices are zero-padded.
159
160struct NeelMPS {
161
162  std::vector<std::vector<Complex>> hostTensors;
163  std::vector<void*> gpuPtrs;
164
165  void build(const std::vector<int64_t>& bondDims) {
166    const Complex zero{0.0, 0.0}, one{1.0, 0.0};
167
168    hostTensors.resize(NUM_SITES);
169    gpuPtrs.resize(NUM_SITES, nullptr);
170
171    for (int32_t site = 0; site < NUM_SITES; ++site) {
172      const int64_t bL = (site == 0) ? 1 : bondDims[site - 1];
173      const int64_t bR = (site == NUM_SITES - 1) ? 1 : bondDims[site];
174      const int64_t vol = bL * PHYS_DIM * bR;
175      hostTensors[site].assign(vol, zero);
176
177      auto idx = [&](int64_t aL, int64_t sigma, int64_t aR) -> int64_t {
178        return aL + bL * (sigma + PHYS_DIM * aR);
179      };
180
181      const int64_t neelSpin = site % 2;
182      hostTensors[site][idx(0, neelSpin, 0)] = one;
183
184      gpuPtrs[site] = createInitializeArrayGPU(hostTensors[site]);
185    }
186  }
187
188  void destroy() {
189    for (auto & ptr : gpuPtrs) {
190      if (ptr) { destroyArrayGPU(ptr); ptr = nullptr; }
191    }
192  }
193};
194
195
196// ============================================================================
197// Helper: cap MPS bond dimensions by the exact Hilbert-space dimension on
198// either side. For an OBC qubit chain, the max admissible bond at site i is
199// min(2^(i+1), 2^(N-i-1)).
200// ============================================================================
201static std::vector<int64_t> makeMaxBondDims(int64_t maxBondCap)
202{
203  std::vector<int64_t> dims(NUM_SITES - 1);
204  for (int32_t i = 0; i < NUM_SITES - 1; ++i) {
205    int64_t leftDim = 1;
206    for (int32_t j = 0; j <= i; ++j) leftDim *= PHYS_DIM;
207    int64_t rightDim = 1;
208    for (int32_t j = i + 1; j < NUM_SITES; ++j) rightDim *= PHYS_DIM;
209    dims[i] = std::min({maxBondCap, leftDim, rightDim});
210  }
211  return dims;
212}
213
214
215// ============================================================================
216// Example workflow
217// ============================================================================
218
219void exampleWorkflow(cudensitymatHandle_t handle)
220{
221  // --- 1. Build the transverse-field Ising MPO ---
222  IsingMPO mpo;
223  mpo.build();
224  if (verbose)
225    std::cout << "Built transverse-field Ising MPO (bond dim " << MPO_BOND_DIM << ")\n";
226
227  const std::vector<int64_t> spaceShape(NUM_SITES, PHYS_DIM);
228  std::vector<int64_t> mpoBondDims(NUM_SITES - 1, MPO_BOND_DIM);
229
230  cudensitymatMatrixProductOperator_t mpoHandle;
231  std::vector<cudensitymatWrappedTensorCallback_t> mpoCallbacks(NUM_SITES, cudensitymatTensorCallbackNone);
232  std::vector<cudensitymatWrappedTensorGradientCallback_t> mpoGradCallbacks(NUM_SITES, cudensitymatTensorGradientCallbackNone);
233  HANDLE_CUDM_ERROR(cudensitymatCreateMatrixProductOperator(handle,
234                      NUM_SITES,
235                      spaceShape.data(),
236                      CUDENSITYMAT_BOUNDARY_CONDITION_OPEN,
237                      mpoBondDims.data(),
238                      DATA_TYPE,
239                      mpo.gpuPtrs.data(),
240                      mpoCallbacks.data(),
241                      mpoGradCallbacks.data(),
242                      &mpoHandle));
243  if (verbose)
244    std::cout << "Created MPO handle\n";
245
246  // --- 2. Wrap the MPO in a single-term Operator ---
247  // Split-scope operator actions require the operator to wrap exactly one
248  // term containing a single MPO product.
249  cudensitymatOperatorTerm_t operatorTerm;
250  HANDLE_CUDM_ERROR(cudensitymatCreateOperatorTerm(handle,
251                      NUM_SITES,
252                      spaceShape.data(),
253                      &operatorTerm));
254
255  std::vector<int32_t> modesActedOn(NUM_SITES);
256  std::iota(modesActedOn.begin(), modesActedOn.end(), 0);
257  std::vector<int32_t> modeDuality(NUM_SITES, 0);
258  std::vector<int32_t> mpoConjugation = {0};
259
260  HANDLE_CUDM_ERROR(cudensitymatOperatorTermAppendMPOProduct(handle,
261                      operatorTerm,
262                      1,
263                      &mpoHandle,
264                      mpoConjugation.data(),
265                      modesActedOn.data(),
266                      modeDuality.data(),
267                      make_cuDoubleComplex(1.0, 0.0),
268                      cudensitymatScalarCallbackNone,
269                      cudensitymatScalarGradientCallbackNone));
270
271  cudensitymatOperator_t hamiltonian;
272  HANDLE_CUDM_ERROR(cudensitymatCreateOperator(handle,
273                      NUM_SITES,
274                      spaceShape.data(),
275                      &hamiltonian));
276  HANDLE_CUDM_ERROR(cudensitymatOperatorAppendTerm(handle,
277                      hamiltonian,
278                      operatorTerm,
279                      0,
280                      make_cuDoubleComplex(1.0, 0.0),
281                      cudensitymatScalarCallbackNone,
282                      cudensitymatScalarGradientCallbackNone));
283  if (verbose)
284    std::cout << "Constructed Hamiltonian operator from MPO\n";
285
286  // --- 3. Create input and output MPS states ---
287  const int64_t batchSize = 1;
288
289  // Both stateIn and stateOut are sized to the maximum admissible bond
290  // dimension for the given physical dimensions. The Neel input is
291  // mathematically a bond-1 state; we zero-pad it into the maximum-bond
292  // tensors. The output is sized to MAX_OUT_BOND_DIM = 16, which is well
293  // above the bond dimension needed to represent H * stateIn exactly
294  // (chi_MPO * chi_MPS_in = 3).
295  std::vector<int64_t> mpsBondDims = makeMaxBondDims(MAX_OUT_BOND_DIM);
296
297  cudensitymatState_t stateIn, stateOut;
298  HANDLE_CUDM_ERROR(cudensitymatCreateStateMPS(handle,
299                      CUDENSITYMAT_STATE_PURITY_PURE,
300                      NUM_SITES,
301                      spaceShape.data(),
302                      CUDENSITYMAT_BOUNDARY_CONDITION_OPEN,
303                      mpsBondDims.data(),
304                      DATA_TYPE,
305                      batchSize,
306                      &stateIn));
307  HANDLE_CUDM_ERROR(cudensitymatCreateStateMPS(handle,
308                      CUDENSITYMAT_STATE_PURITY_PURE,
309                      NUM_SITES,
310                      spaceShape.data(),
311                      CUDENSITYMAT_BOUNDARY_CONDITION_OPEN,
312                      mpsBondDims.data(),
313                      DATA_TYPE,
314                      batchSize,
315                      &stateOut));
316
317  int32_t numComponents = 0;
318  HANDLE_CUDM_ERROR(cudensitymatStateGetNumComponents(handle, stateIn, &numComponents));
319  assert(numComponents == NUM_SITES);
320
321  std::vector<std::size_t> componentSizes(numComponents);
322  HANDLE_CUDM_ERROR(cudensitymatStateGetComponentStorageSize(handle,
323                      stateIn, numComponents, componentSizes.data()));
324
325  if (verbose) {
326    std::cout << "MPS state has " << numComponents << " components, sizes (bytes):";
327    for (auto s : componentSizes) std::cout << " " << s;
328    std::cout << "\n";
329    std::cout << "MPS bond dims:";
330    for (auto b : mpsBondDims) std::cout << " " << b;
331    std::cout << "\n";
332  }
333
334  // --- 4. Allocate GPU storage; initialise stateIn to Neel, stateOut to 0 ---
335  //
336  // The per-site GPU buffers for stateIn must be allocated AND populated with
337  // their initial data before attaching them to the state, so we upload the
338  // Neel data on the host first via NeelMPS::build. For stateOut a plain
339  // cudaMalloc is enough; cudensitymatStateInitializeZero (called below)
340  // writes zeros into the attached buffers so the operator action behaves as
341  // an assignment.
342  NeelMPS neelMps;
343  neelMps.build(mpsBondDims);
344
345  std::vector<void *> stateOutBuffers(numComponents, nullptr);
346  for (int32_t c = 0; c < numComponents; ++c) {
347    HANDLE_CUDA_ERROR(cudaMalloc(&stateOutBuffers[c], componentSizes[c]));
348  }
349
350  HANDLE_CUDM_ERROR(cudensitymatStateAttachComponentStorage(handle,
351                      stateIn, numComponents,
352                      neelMps.gpuPtrs.data(), componentSizes.data()));
353  HANDLE_CUDM_ERROR(cudensitymatStateAttachComponentStorage(handle,
354                      stateOut, numComponents,
355                      stateOutBuffers.data(), componentSizes.data()));
356
357  // Zero-initialise stateOut so that the subsequent operator action assigns
358  // alpha * H * stateIn into it (rather than accumulating onto prior data).
359  HANDLE_CUDM_ERROR(cudensitymatStateInitializeZero(handle, stateOut, /*stream=*/0));
360
361  if (verbose)
362    std::cout << "Initialized MPS states (input = Neel |0101...>, "
363                 "output = zero MPS via cudensitymatStateInitializeZero)\n";
364
365  // --- 5. Create the OperatorAction (split scope + LinSolve approach) ---
366  cudensitymatOperator_t operators[] = {hamiltonian};
367  cudensitymatOperatorAction_t operatorAction;
368  HANDLE_CUDM_ERROR(cudensitymatCreateOperatorAction(handle,
369                      /*numOperators=*/1,
370                      operators,
371                      CUDENSITYMAT_FITTING_SCOPE_SPLIT,
372                      CUDENSITYMAT_FITTING_APPROACH_LINSOLVE,
373                      &operatorAction));
374  if (verbose)
375    std::cout << "Created OperatorAction (scope = SPLIT, approach = LINSOLVE)\n";
376
377  // --- 6. (Optional) Configure ALS sub-config ---
378  // The defaults (num_sites = 1, max_sweeps = 20, tolerance = 1e-10) already
379  // suffice for a TFIM demo. We pass an explicit configuration here only to
380  // show the wiring; tightening max_sweeps below 20 may degrade convergence.
381  cudensitymatStateFittingScopeSplitALSConfig_t alsConfig{nullptr};
382  HANDLE_CUDM_ERROR(cudensitymatCreateStateFittingScopeSplitALSConfig(handle, &alsConfig));
383  {
384    const int32_t numSites = 1;
385    HANDLE_CUDM_ERROR(cudensitymatStateFittingScopeSplitALSConfigSetAttribute(handle,
386                        alsConfig,
387                        CUDENSITYMAT_FITTING_SPLIT_SCOPE_ALS_NUM_SITES,
388                        &numSites, sizeof(numSites)));
389    const int32_t maxSweeps = 20;
390    HANDLE_CUDM_ERROR(cudensitymatStateFittingScopeSplitALSConfigSetAttribute(handle,
391                        alsConfig,
392                        CUDENSITYMAT_FITTING_SPLIT_SCOPE_ALS_MAX_SWEEPS,
393                        &maxSweeps, sizeof(maxSweeps)));
394    const double tolerance = 1e-10;
395    HANDLE_CUDM_ERROR(cudensitymatStateFittingScopeSplitALSConfigSetAttribute(handle,
396                        alsConfig,
397                        CUDENSITYMAT_FITTING_SPLIT_SCOPE_ALS_TOLERANCE,
398                        &tolerance, sizeof(tolerance)));
399  }
400  HANDLE_CUDM_ERROR(cudensitymatOperatorActionConfigure(handle,
401                      operatorAction,
402                      CUDENSITYMAT_FITTING_SPLIT_SCOPE_ALS_CONFIG,
403                      &alsConfig, sizeof(alsConfig)));
404  // The sub-config is deep-copied at Configure; it is safe to destroy now.
405  HANDLE_CUDM_ERROR(cudensitymatDestroyStateFittingScopeSplitALSConfig(alsConfig));
406  if (verbose)
407    std::cout << "Configured ALS (num_sites = 1, max_sweeps = 20, tolerance = 1e-10)\n";
408
409  // --- 7. Prepare action and allocate workspace ---
410  cudensitymatWorkspaceDescriptor_t workspaceDescr;
411  HANDLE_CUDM_ERROR(cudensitymatCreateWorkspace(handle, &workspaceDescr));
412
413  std::size_t freeMem = 0, totalMem = 0;
414  HANDLE_CUDA_ERROR(cudaMemGetInfo(&freeMem, &totalMem));
415  freeMem = static_cast<std::size_t>(static_cast<double>(freeMem) * 0.95);
416  if (verbose)
417    std::cout << "Available workspace memory (bytes) = " << freeMem << "\n";
418
419  HANDLE_CUDM_ERROR(cudensitymatOperatorActionPrepare(handle,
420                      operatorAction,
421                      &stateIn,
422                      stateOut,
423                      CUDENSITYMAT_COMPUTE_64F,
424                      freeMem,
425                      workspaceDescr,
426                      /*stream=*/0));
427  if (verbose)
428    std::cout << "Prepared OperatorAction\n";
429
430  std::size_t scratchSize = 0;
431  HANDLE_CUDM_ERROR(cudensitymatWorkspaceGetMemorySize(handle,
432                      workspaceDescr,
433                      CUDENSITYMAT_MEMSPACE_DEVICE,
434                      CUDENSITYMAT_WORKSPACE_SCRATCH,
435                      &scratchSize));
436  void * scratchBuf = nullptr;
437  if (scratchSize > 0) {
438    HANDLE_CUDA_ERROR(cudaMalloc(&scratchBuf, scratchSize));
439    HANDLE_CUDM_ERROR(cudensitymatWorkspaceSetMemory(handle,
440                        workspaceDescr,
441                        CUDENSITYMAT_MEMSPACE_DEVICE,
442                        CUDENSITYMAT_WORKSPACE_SCRATCH,
443                        scratchBuf, scratchSize));
444  }
445  if (verbose)
446    std::cout << "Scratch workspace (bytes) = " << scratchSize << "\n";
447
448  // --- 8. Compute one operator action (stateOut <- alpha * H * stateIn) ---
449  // Because stateOut was zero-initialised above, the split-scope fit assigns
450  // alpha * H * stateIn into stateOut rather than accumulating onto prior
451  // contents, so the result equals H |Neel> for this fixture (alpha = 1).
452  if (verbose)
453    std::cout << "\nApplying H to Neel state via OperatorActionCompute (t = 0)\n";
454
455  HANDLE_CUDM_ERROR(cudensitymatOperatorActionCompute(handle,
456                      operatorAction,
457                      /*time=*/0.0,
458                      batchSize,
459                      /*numParams=*/0,
460                      /*params=*/nullptr,
461                      &stateIn,
462                      stateOut,
463                      workspaceDescr,
464                      /*stream=*/0));
465  HANDLE_CUDA_ERROR(cudaStreamSynchronize(0));
466  if (verbose)
467    std::cout << "OperatorActionCompute completed\n";
468
469  // --- 9. Report the output MPS bond extents ---
470  // Bond extents reported here are the as-allocated ones; the ALS fit cannot
471  // exceed them but may use a subset internally.
472  if (verbose) {
473    std::cout << "\nOutput MPS component shapes (column-major modes):\n";
474    for (int32_t c = 0; c < numComponents; ++c) {
475      int32_t globalId = 0;
476      int32_t numModes = 0;
477      int32_t batchModeLocation = 0;
478      HANDLE_CUDM_ERROR(cudensitymatStateGetComponentNumModes(handle,
479                          stateOut, c, &globalId, &numModes, &batchModeLocation));
480      std::vector<int64_t> extents(numModes), offsets(numModes);
481      HANDLE_CUDM_ERROR(cudensitymatStateGetComponentInfo(handle,
482                          stateOut, c, &globalId,
483                          &numModes,
484                          extents.data(),
485                          offsets.data()));
486      std::cout << "  site " << c << " : [";
487      for (int32_t m = 0; m < numModes; ++m) {
488        std::cout << extents[m] << (m + 1 < numModes ? ", " : "");
489      }
490      std::cout << "]\n";
491    }
492  }
493
494  // --- 10. Clean up ---
495  if (scratchBuf)
496    HANDLE_CUDA_ERROR(cudaFree(scratchBuf));
497  HANDLE_CUDM_ERROR(cudensitymatDestroyWorkspace(workspaceDescr));
498  HANDLE_CUDM_ERROR(cudensitymatDestroyOperatorAction(operatorAction));
499  HANDLE_CUDM_ERROR(cudensitymatDestroyOperator(hamiltonian));
500  HANDLE_CUDM_ERROR(cudensitymatDestroyOperatorTerm(operatorTerm));
501  HANDLE_CUDM_ERROR(cudensitymatDestroyMatrixProductOperator(mpoHandle));
502  HANDLE_CUDM_ERROR(cudensitymatDestroyState(stateOut));
503  HANDLE_CUDM_ERROR(cudensitymatDestroyState(stateIn));
504
505  for (auto * buf : stateOutBuffers) {
506    if (buf) HANDLE_CUDA_ERROR(cudaFree(buf));
507  }
508  neelMps.destroy();
509  mpo.destroy();
510
511  if (verbose)
512    std::cout << "\nDestroyed all resources\n";
513}
514
515
516int main(int argc, char ** argv)
517{
518  HANDLE_CUDA_ERROR(cudaSetDevice(0));
519  if (verbose)
520    std::cout << "Set active device\n";
521
522  cudensitymatHandle_t handle;
523  HANDLE_CUDM_ERROR(cudensitymatCreate(&handle));
524  if (verbose)
525    std::cout << "Created library handle\n";
526
527  exampleWorkflow(handle);
528
529  HANDLE_CUDM_ERROR(cudensitymatDestroy(handle));
530  if (verbose)
531    std::cout << "Destroyed library handle\n";
532
533  HANDLE_CUDA_ERROR(cudaDeviceReset());
534  return 0;
535}

Code example (serial execution of operator eigenspectrum computation)#

The following code example illustrates how to use the cuDensityMat library for computing the extreme eigenspectrum of a given operator. The full sample code can be found in the NVIDIA/cuQuantum repository (main serial eigenspectrum code and operator definition as well as the utility code).

First, similarly to the above examples, we define a transverse-field Ising Hamiltonian with fused ZZ terms the eigenspectrum of which we want to compute. Specifically, in this case, we want to compute a number of the smallest real eigenvalues and their corresponding eigenvectors (pure quantum states). For simplicity, we made our operator time-independent (static).

  1/* Copyright (c) 2024-2025, NVIDIA CORPORATION & AFFILIATES.
  2 *
  3 * SPDX-License-Identifier: BSD-3-Clause
  4 */
  5
  6#pragma once
  7
  8#include <cudensitymat.h> // cuDensityMat library header
  9#include "helpers.h"      // GPU helper functions
 10
 11#include <cmath>
 12#include <complex>
 13#include <vector>
 14#include <iostream>
 15#include <cassert>
 16
 17
 18/* DESCRIPTION:
 19   Transverse-field Ising Hamiltonian operator with ordered and fused ZZ terms:
 20    H = sum_{i} {h_i * X_i}         // transverse field sum of X_i operators with static h_i coefficients 
 21      + sum_{i < j} {g_ij * ZZ_ij}  // sum of the fused ordered {Z_i * Z_j} terms with static g_ij coefficients
 22*/
 23
 24/** Define the numerical type and data type for the GPU computations (same) */
 25using NumericalType = std::complex<double>;      // do not change
 26constexpr cudaDataType_t dataType = CUDA_C_64F;  // do not change
 27
 28
 29/** Convenience class which encapsulates a user-defined Liouvillian operator (system Hamiltonian + dissipation terms):
 30 *  - Constructor constructs the desired Liouvillian operator (`cudensitymatOperator_t`)
 31 *  - Method `get()` returns a reference to the constructed Liouvillian operator
 32 *  - Destructor releases all resources used by the Liouvillian operator
 33 */
 34class UserDefinedLiouvillian final
 35{
 36private:
 37  // Data members
 38  cudensitymatHandle_t handle;             // library context handle
 39  int64_t stateBatchSize;                  // quantum state batch size
 40  const std::vector<int64_t> spaceShape;   // Hilbert space shape (extents of the modes of the composite Hilbert space)
 41  void * spinXelems {nullptr};             // elements of the X spin operator in GPU RAM (F-order storage)
 42  void * spinZZelems {nullptr};            // elements of the fused ZZ two-spin operator in GPU RAM (F-order storage)
 43  cudensitymatElementaryOperator_t spinX;  // X spin operator (elementary tensor operator)
 44  cudensitymatElementaryOperator_t spinZZ; // fused ZZ two-spin operator (elementary tensor operator)
 45  cudensitymatOperatorTerm_t oneBodyTerm;  // operator term: H1 = sum_{i} {h_i * X_i} (one-body term)
 46  cudensitymatOperatorTerm_t twoBodyTerm;  // operator term: H2 = sum_{i < j} {g_ij * ZZ_ij} (two-body term)
 47  cudensitymatOperator_t liouvillian;      // full operator: H = H1 + H2
 48
 49public:
 50
 51  // Constructor constructs a user-defined Liouvillian operator
 52  UserDefinedLiouvillian(cudensitymatHandle_t contextHandle,             // library context handle
 53                         const std::vector<int64_t> & hilbertSpaceShape, // Hilbert space shape
 54                         int64_t batchSize):                             // batch size for the quantum state
 55    handle(contextHandle), stateBatchSize(batchSize), spaceShape(hilbertSpaceShape)
 56  {
 57    // Define the necessary operator tensors in GPU memory (F-order storage!)
 58    spinXelems = createInitializeArrayGPU<NumericalType>(  // X[i0; j0]
 59                  {{0.0, 0.0}, {1.0, 0.0},   // 1st column of matrix X
 60                   {1.0, 0.0}, {0.0, 0.0}}); // 2nd column of matrix X
 61
 62    spinZZelems = createInitializeArrayGPU<NumericalType>(  // ZZ[i0, i1; j0, j1] := Z[i0; j0] * Z[i1; j1]
 63                    {{1.0, 0.0}, {0.0, 0.0},  {0.0, 0.0},  {0.0, 0.0},   // 1st column of matrix ZZ
 64                     {0.0, 0.0}, {-1.0, 0.0}, {0.0, 0.0},  {0.0, 0.0},   // 2nd column of matrix ZZ
 65                     {0.0, 0.0}, {0.0, 0.0},  {-1.0, 0.0}, {0.0, 0.0},   // 3rd column of matrix ZZ
 66                     {0.0, 0.0}, {0.0, 0.0},  {0.0, 0.0},  {1.0, 0.0}}); // 4th column of matrix ZZ
 67
 68    // Construct the necessary Elementary Tensor Operators
 69    //  X_i operator
 70    HANDLE_CUDM_ERROR(cudensitymatCreateElementaryOperator(handle,
 71                        1,                                   // one-body operator
 72                        std::vector<int64_t>({2}).data(),    // acts in tensor space of shape {2}
 73                        CUDENSITYMAT_OPERATOR_SPARSITY_NONE, // dense tensor storage
 74                        0,                                   // 0 for dense tensors
 75                        nullptr,                             // nullptr for dense tensors
 76                        dataType,                            // data type
 77                        spinXelems,                          // tensor elements in GPU memory
 78                        cudensitymatTensorCallbackNone,      // no tensor callback function (tensor is not time-dependent)
 79                        cudensitymatTensorGradientCallbackNone, // no tensor gradient callback function
 80                        &spinX));                            // the created elementary tensor operator
 81    //  ZZ_ij = Z_i * Z_j fused operator
 82    HANDLE_CUDM_ERROR(cudensitymatCreateElementaryOperator(handle,
 83                        2,                                   // two-body operator
 84                        std::vector<int64_t>({2,2}).data(),  // acts in tensor space of shape {2,2}
 85                        CUDENSITYMAT_OPERATOR_SPARSITY_NONE, // dense tensor storage
 86                        0,                                   // 0 for dense tensors
 87                        nullptr,                             // nullptr for dense tensors
 88                        dataType,                            // data type
 89                        spinZZelems,                         // tensor elements in GPU memory
 90                        cudensitymatTensorCallbackNone,      // no tensor callback function (tensor is not time-dependent)
 91                        cudensitymatTensorGradientCallbackNone, // no tensor gradient callback function
 92                        &spinZZ));                           // the created elementary tensor operator
 93
 94    // Construct the necessary Operator Terms from tensor products of Elementary Tensor Operators
 95    //  Create an empty operator term
 96    HANDLE_CUDM_ERROR(cudensitymatCreateOperatorTerm(handle,
 97                        spaceShape.size(),                   // Hilbert space rank (number of modes)
 98                        spaceShape.data(),                   // Hilbert space shape (mode extents)
 99                        &oneBodyTerm));                      // the created empty operator term
100    //  Define the operator term: H1 = sum_{i} {h_i * X_i}
101    for (int32_t i = 0; i < spaceShape.size(); ++i) {
102      const double h_i = 1.0 / static_cast<double>(i+1); // assign some value to the time-independent h_i coefficient
103      HANDLE_CUDM_ERROR(cudensitymatOperatorTermAppendElementaryProduct(handle,
104                          oneBodyTerm,
105                          1,                                                             // number of elementary tensor operators in the product
106                          std::vector<cudensitymatElementaryOperator_t>({spinX}).data(), // elementary tensor operators forming the product
107                          std::vector<int32_t>({i}).data(),                              // space modes acted on by the operator product
108                          std::vector<int32_t>({0}).data(),                              // space mode action duality (0: from the left; 1: from the right)
109                          make_cuDoubleComplex(h_i, 0.0),                                // h_i constant coefficient: Always 64-bit-precision complex number
110                          cudensitymatScalarCallbackNone,                                // no time-dependent coefficient associated with this operator product
111                          cudensitymatScalarGradientCallbackNone));                      // no coefficient gradient associated with this operator product
112    }
113    //  Create an empty operator term
114    HANDLE_CUDM_ERROR(cudensitymatCreateOperatorTerm(handle,
115                        spaceShape.size(),                   // Hilbert space rank (number of modes)
116                        spaceShape.data(),                   // Hilbert space shape (mode extents)
117                        &twoBodyTerm));                      // the created empty operator term
118    //  Define the operator term: H2 = sum_{i < j} {g_ij * ZZ_ij}
119    for (int32_t i = 0; i < spaceShape.size() - 1; ++i) {
120      for (int32_t j = (i + 1); j < spaceShape.size(); ++j) {
121        const double g_ij = -1.0 / static_cast<double>(i + j + 1); // assign some value to the time-independent g_ij coefficient
122        HANDLE_CUDM_ERROR(cudensitymatOperatorTermAppendElementaryProduct(handle,
123                            twoBodyTerm,
124                            1,                                                              // number of elementary tensor operators in the product
125                            std::vector<cudensitymatElementaryOperator_t>({spinZZ}).data(), // elementary tensor operators forming the product
126                            std::vector<int32_t>({i, j}).data(),                            // space modes acted on by the operator product
127                            std::vector<int32_t>({0, 0}).data(),                            // space mode action duality (0: from the left; 1: from the right)
128                            make_cuDoubleComplex(g_ij, 0.0),                                // g_ij constant coefficient: Always 64-bit-precision complex number
129                            cudensitymatScalarCallbackNone,                                 // no time-dependent coefficient associated with this operator product
130                            cudensitymatScalarGradientCallbackNone));                       // no coefficient gradient associated with this operator product
131      }
132    }
133
134    // Construct the full Liouvillian operator as a sum of the operator terms
135    //  Create an empty operator
136    HANDLE_CUDM_ERROR(cudensitymatCreateOperator(handle,
137                        spaceShape.size(),                // Hilbert space rank (number of modes)
138                        spaceShape.data(),                // Hilbert space shape (modes extents)
139                        &liouvillian));                   // the created empty operator
140    //  Append an operator term to the operator
141    HANDLE_CUDM_ERROR(cudensitymatOperatorAppendTerm(handle,
142                        liouvillian,
143                        oneBodyTerm,                      // appended operator term
144                        0,                                // operator term action duality as a whole (0: acting from the left; 1: acting from the right)
145                        make_cuDoubleComplex(1.0, 0.0),   // constant coefficient associated with the operator term as a whole
146                        cudensitymatScalarCallbackNone,   // no time-dependent coefficient associated with the operator term as a whole
147                        cudensitymatScalarGradientCallbackNone)); // no coefficient gradient associated with the operator term as a whole
148    //  Append an operator term to the operator (super-operator)
149    HANDLE_CUDM_ERROR(cudensitymatOperatorAppendTerm(handle,
150                        liouvillian,
151                        twoBodyTerm,                      // appended operator term
152                        0,                                // operator term action duality as a whole (0: acting from the left; 1: acting from the right)
153                        make_cuDoubleComplex(1.0, 0.0),   // constant coefficient associated with the operator term as a whole
154                        cudensitymatScalarCallbackNone,   // no time-dependent coefficient associated with the operator term as a whole
155                        cudensitymatScalarGradientCallbackNone)); // no coefficient gradient associated with this operator term as a whole
156  }
157
158  // Destructor destructs the user-defined Liouvillian operator
159  ~UserDefinedLiouvillian()
160  {
161    // Destroy the Liouvillian operator
162    HANDLE_CUDM_ERROR(cudensitymatDestroyOperator(liouvillian));
163
164    // Destroy operator terms
165    HANDLE_CUDM_ERROR(cudensitymatDestroyOperatorTerm(twoBodyTerm));
166    HANDLE_CUDM_ERROR(cudensitymatDestroyOperatorTerm(oneBodyTerm));
167
168    // Destroy elementary tensor operators
169    HANDLE_CUDM_ERROR(cudensitymatDestroyElementaryOperator(spinZZ));
170    HANDLE_CUDM_ERROR(cudensitymatDestroyElementaryOperator(spinX));
171
172    // Destroy operator tensors
173    destroyArrayGPU(spinZZelems);
174    destroyArrayGPU(spinXelems);
175  }
176
177  // Disable copy constructor/assignment (GPU resources are private, no deep copy)
178  UserDefinedLiouvillian(const UserDefinedLiouvillian &) = delete;
179  UserDefinedLiouvillian & operator=(const UserDefinedLiouvillian &) = delete;
180  UserDefinedLiouvillian(UserDefinedLiouvillian &&) = delete;
181  UserDefinedLiouvillian & operator=(UserDefinedLiouvillian &&) = delete;
182
183  /** Returns the number of externally provided Hamiltonian parameters. */
184  int32_t getNumParameters() const
185  {
186    return 0; // no free parameters
187  }
188
189  /** Get access to the constructed Liouvillian operator. */
190  cudensitymatOperator_t & get()
191  {
192    return liouvillian;
193  }
194
195};

Once the operator has been defined, we can follow standard steps to create necessary quantum states which will store the eigenstates of the defined operator. Then we can prepare the operator eigenspectrum computation, and, finally, compute the eigenspectrum. Note that the leading subset of the quantum states passed to the eigenspectrum compute call to store the computed eigenstates will also be used as the initial guesses for the first Krylov subspace block (if the block size is smaller than the number of requested eigenstates, only the leading quantum states will be used).

  1/* Copyright (c) 2024-2025, NVIDIA CORPORATION & AFFILIATES.
  2 *
  3 * SPDX-License-Identifier: BSD-3-Clause
  4 */
  5
  6#include <cudensitymat.h>  // cuDensityMat library header
  7#include "helpers.h"       // helper functions
  8
  9
 10// Transverse Ising Hamiltonian with double summation ordering and spin-operator fusion
 11#include "transverse_ising_full_fused.h"  // user-defined Liouvillian operator example
 12
 13#include <cmath>
 14#include <complex>
 15#include <vector>
 16#include <chrono>
 17#include <iostream>
 18#include <cassert>
 19
 20
 21// Logging verbosity
 22bool verbose = true;
 23
 24
 25// Example workflow
 26void exampleWorkflow(cudensitymatHandle_t handle)
 27{
 28  // Define the composite Hilbert space shape and
 29  // quantum state batch size (number of individual quantum states in a batched simulation)
 30  const std::vector<int64_t> spaceShape({2,2,2,2,2,2,2,2,2,2}); // dimensions of quantum degrees of freedom
 31  const int64_t batchSize = 1;        // number of quantum states per batch (currently only 1 state per batch)
 32  const int32_t numEigenStates = 4;   // number of eigenstates to compute
 33
 34  if (verbose) {
 35    std::cout << "Hilbert space rank = " << spaceShape.size() << "; Shape = (";
 36    for (const auto & dimsn: spaceShape)
 37      std::cout << dimsn << ",";
 38    std::cout << ")" << std::endl;
 39    std::cout << "Quantum state batch size = " << batchSize << std::endl;
 40  }
 41
 42  // Construct a user-defined Liouvillian operator using a convenience C++ class
 43  UserDefinedLiouvillian liouvillian(handle, spaceShape, batchSize);
 44  if (verbose)
 45    std::cout << "Constructed the Liouvillian operator\n";
 46
 47  // Create quantum states to store the eigenstates
 48  std::size_t stateVolume {0};
 49  std::vector<cudensitymatState_t> eigenStates(numEigenStates);
 50  std::vector<void *> eigenStatesElems(numEigenStates);
 51  for (int32_t id = 0; id < numEigenStates; ++id) {
 52
 53    // Declare the quantum state
 54    HANDLE_CUDM_ERROR(cudensitymatCreateState(handle,
 55                        CUDENSITYMAT_STATE_PURITY_PURE,  // pure (state vector)
 56                        spaceShape.size(),
 57                        spaceShape.data(),
 58                        batchSize,
 59                        dataType,
 60                        &eigenStates[id]));
 61
 62    // Query the size of the quantum state storage
 63    std::size_t storageSize {0}; // only one storage component (tensor) is needed (no tensor factorization)
 64    HANDLE_CUDM_ERROR(cudensitymatStateGetComponentStorageSize(handle,
 65                        eigenStates[id],
 66                        1,               // only one storage component (tensor)
 67                        &storageSize));  // storage size in bytes
 68    stateVolume = storageSize / sizeof(NumericalType);  // quantum state tensor volume (number of elements)
 69    if (verbose)
 70      std::cout << "Quantum state storage size (bytes) = " << storageSize << std::endl;
 71
 72    // Prepare some initial value for the quantum state
 73    std::vector<NumericalType> stateValue(stateVolume);
 74    if constexpr (std::is_same_v<NumericalType, double>) {
 75      for (std::size_t i = 0; i < stateVolume; ++i) {
 76        stateValue[i] = 1.0 / double(id*5 + i+1); // just some value
 77      }
 78    } else if constexpr (std::is_same_v<NumericalType, std::complex<double>>) {
 79      for (std::size_t i = 0; i < stateVolume; ++i) {
 80        stateValue[i] = NumericalType{1.0 / double(id*5 + i+1), -1.0 / double(id*3 + i+2)}; // just some value
 81      }
 82    } else {
 83      std::cerr << "Error: Unsupported data type!\n";
 84      std::exit(1);
 85    }
 86    // Allocate initialized GPU storage for the quantum state with prepared values
 87    eigenStatesElems[id] = createInitializeArrayGPU(stateValue);
 88    if (verbose)
 89      std::cout << "Allocated quantum state storage and initialized it to some value\n";
 90
 91    // Attach initialized GPU storage to the quantum state
 92    HANDLE_CUDM_ERROR(cudensitymatStateAttachComponentStorage(handle,
 93                        eigenStates[id],
 94                        1,                                                 // only one storage component (tensor)
 95                        std::vector<void*>({eigenStatesElems[id]}).data(), // pointer to the GPU storage for the quantum state
 96                        std::vector<std::size_t>({storageSize}).data()));  // size of the GPU storage for the quantum state
 97    if (verbose)
 98      std::cout << "Constructed quantum state\n";
 99  }
100
101  // Allocate storage for the eigenvalues and convergence tolerances
102  void * eigenvalues = createArrayGPU<NumericalType>(numEigenStates * batchSize);
103  std::vector<double> tolerances(numEigenStates * batchSize, 1e-6);
104
105  // Declare a workspace descriptor
106  cudensitymatWorkspaceDescriptor_t workspaceDescr;
107  HANDLE_CUDM_ERROR(cudensitymatCreateWorkspace(handle, &workspaceDescr));
108
109  // Query free GPU memory
110  std::size_t freeMem = 0, totalMem = 0;
111  HANDLE_CUDA_ERROR(cudaMemGetInfo(&freeMem, &totalMem));
112  freeMem = static_cast<std::size_t>(static_cast<double>(freeMem) * 0.95); // take 95% of the free memory for the workspace buffer
113  if (verbose)
114    std::cout << "Max workspace buffer size (bytes) = " << freeMem << std::endl;
115
116  // Create the operator eigenspectrum computation object
117  cudensitymatOperatorSpectrum_t spectrum;
118  HANDLE_CUDM_ERROR(cudensitymatCreateOperatorSpectrum(handle,
119                      liouvillian.get(),
120                      1,  // Hermitian operator
121                      CUDENSITYMAT_OPERATOR_SPECTRUM_SMALLEST_REAL,
122                      &spectrum));
123
124  // Prepare the operator eigenspectrum computation (needs to be done only once)
125  auto startTime = std::chrono::high_resolution_clock::now();
126  HANDLE_CUDM_ERROR(cudensitymatOperatorSpectrumPrepare(handle,
127                      spectrum,
128                      numEigenStates,
129                      eigenStates[0],
130                      CUDENSITYMAT_COMPUTE_64F,
131                      freeMem,
132                      workspaceDescr,
133                      0x0));
134  auto finishTime = std::chrono::high_resolution_clock::now();
135  std::chrono::duration<double> timeSec = finishTime - startTime;
136  if (verbose)
137    std::cout << "Operator eigenspectrum preparation time (sec) = " << timeSec.count() << std::endl;
138
139  // Query the required workspace buffer size (bytes)
140  std::size_t requiredBufferSize {0};
141  HANDLE_CUDM_ERROR(cudensitymatWorkspaceGetMemorySize(handle,
142                      workspaceDescr,
143                      CUDENSITYMAT_MEMSPACE_DEVICE,
144                      CUDENSITYMAT_WORKSPACE_SCRATCH,
145                      &requiredBufferSize));
146  if (verbose)
147    std::cout << "Required workspace buffer size (bytes) = " << requiredBufferSize << std::endl;
148
149  // Allocate GPU storage for the workspace buffer
150  const std::size_t bufferVolume = requiredBufferSize / sizeof(NumericalType);
151  auto * workspaceBuffer = createArrayGPU<NumericalType>(bufferVolume);
152  if (verbose)
153    std::cout << "Allocated workspace buffer of size (bytes) = " << requiredBufferSize << std::endl;
154
155  // Attach the workspace buffer to the workspace descriptor
156  HANDLE_CUDM_ERROR(cudensitymatWorkspaceSetMemory(handle,
157                      workspaceDescr,
158                      CUDENSITYMAT_MEMSPACE_DEVICE,
159                      CUDENSITYMAT_WORKSPACE_SCRATCH,
160                      workspaceBuffer,
161                      requiredBufferSize));
162  if (verbose)
163    std::cout << "Attached workspace buffer of size (bytes) = " << requiredBufferSize << std::endl;
164
165  // Compute the operator eigenspectrum
166  HANDLE_CUDA_ERROR(cudaDeviceSynchronize());
167  startTime = std::chrono::high_resolution_clock::now();
168  HANDLE_CUDM_ERROR(cudensitymatOperatorSpectrumCompute(handle,
169                      spectrum,
170                      0.0,
171                      batchSize,
172                      0,
173                      nullptr,
174                      numEigenStates,
175                      eigenStates.data(),
176                      eigenvalues,
177                      tolerances.data(),
178                      workspaceDescr,
179                      0x0));
180  HANDLE_CUDA_ERROR(cudaDeviceSynchronize());
181  finishTime = std::chrono::high_resolution_clock::now();
182  timeSec = finishTime - startTime;
183  if (verbose)
184    std::cout << "Operator eigenspectrum computation time (sec) = " << timeSec.count() << std::endl;
185
186  // Print the eigenvalues
187  if (verbose) {
188    std::cout << "Eigenvalues:\n";
189    printArrayGPU<NumericalType>(eigenvalues, numEigenStates);
190  }
191
192  // Print the residual norms
193  if (verbose) {
194    std::cout << "Residual norms:\n";
195    printArrayCPU<double>(tolerances.data(), numEigenStates);
196  }
197
198  HANDLE_CUDA_ERROR(cudaDeviceSynchronize());
199
200  // Destroy workspace descriptor
201  HANDLE_CUDM_ERROR(cudensitymatDestroyWorkspace(workspaceDescr));
202
203  // Destroy workspace buffer storage
204  destroyArrayGPU(workspaceBuffer);
205
206  // Destroy operator eigenspectrum computation object
207  HANDLE_CUDM_ERROR(cudensitymatDestroyOperatorSpectrum(spectrum));
208
209  // Destroy eigenvalues storage
210  destroyArrayGPU(eigenvalues);
211
212  // Destroy quantum states
213  for (int32_t id = 0; id < numEigenStates; ++id)
214    HANDLE_CUDM_ERROR(cudensitymatDestroyState(eigenStates[id]));
215
216  // Destroy quantum state storage
217  for (int32_t id = 0; id < numEigenStates; ++id)
218    destroyArrayGPU(eigenStatesElems[id]);
219
220  if (verbose)
221    std::cout << "Destroyed resources\n" << std::flush;
222}
223
224
225int main(int argc, char ** argv)
226{
227  // Assign a GPU to the process
228  HANDLE_CUDA_ERROR(cudaSetDevice(0));
229  if (verbose)
230    std::cout << "Set active device\n";
231
232  // Create a library handle
233  cudensitymatHandle_t handle;
234  HANDLE_CUDM_ERROR(cudensitymatCreate(&handle));
235  if (verbose)
236    std::cout << "Created a library handle\n";
237
238  // Run the example
239  exampleWorkflow(handle);
240
241  // Destroy the library handle
242  HANDLE_CUDM_ERROR(cudensitymatDestroy(handle));
243  if (verbose)
244    std::cout << "Destroyed the library handle\n";
245
246  HANDLE_CUDA_ERROR(cudaDeviceReset());
247
248  // Done
249  return 0;
250}

Code example (serial execution of 1-site MPS-DMRG ground-state eigensolver)#

The following code example illustrates how to use the cuDensityMat library to compute the ground state (smallest-real eigenpair) of a Heisenberg Hamiltonian encoded as an MPO, acting on a pure MPS state, using the split-scope 1-site DMRG method (CUDENSITYMAT_EIGEN_SCOPE_SPLIT + CUDENSITYMAT_EIGEN_APPROACH_KRYLOV + CUDENSITYMAT_EIGEN_SPECTRUM_SMALLEST_REAL) at the default 1-site update granularity (CUDENSITYMAT_EIGEN_SPLIT_SCOPE_DMRG_NUM_SITES of 1). The example builds the Hamiltonian MPO, creates an initial MPS state, configures the DMRG and Krylov sub-configs, prepares and computes the ground state, then reports the energy and residual. The full sample code can be found in the NVIDIA/cuQuantum repository (main serial 1-site MPS-DMRG code as well as the utility code).

  1/* Copyright (c) 2024-2026, NVIDIA CORPORATION & AFFILIATES.
  2 *
  3 * SPDX-License-Identifier: BSD-3-Clause
  4 */
  5
  6// DMRG eigensolver example.
  7//
  8// Computes the ground state of the 1-D spin-1/2 antiferromagnetic Heisenberg
  9// chain  H = J * sum_i ( Sx_i Sx_{i+1} + Sy_i Sy_{i+1} + Sz_i Sz_{i+1} )  with
 10// the 1-site DMRG solver (SCOPE_SPLIT + APPROACH_KRYLOV + SMALLEST_REAL) on an
 11// MPS state.  The exact N=8 open-boundary energy (J=1) is E0 = -3.374932598688;
 12// the example checks the DMRG result against it.
 13//
 14// Workflow:
 15//  1. Build the Heisenberg MPO (bond dim 5)
 16//  2. Create an initial MPS state (noisy Neel product)
 17//  3. Create the EigenDecomposition (SMALLEST_REAL + SCOPE_SPLIT + KRYLOV)
 18//  4. Configure the DMRG and Krylov sub-configs
 19//  5. Prepare + attach workspace + Compute
 20//  6. Report the ground-state energy and residual vs the exact value
 21//  7. Clean up
 22
 23#include <cudensitymat.h>
 24#include "helpers.h"
 25
 26#include <algorithm>
 27#include <cmath>
 28#include <complex>
 29#include <vector>
 30#include <numeric>
 31#include <random>
 32#include <iostream>
 33#include <iomanip>
 34#include <cassert>
 35
 36
 37using Complex = std::complex<double>;
 38constexpr cudaDataType_t kDataType = CUDA_C_64F;
 39
 40constexpr bool verbose = true;
 41
 42// --- Simulation parameters ---
 43constexpr int32_t  NUM_SITES  = 8;
 44constexpr int64_t  PHYS_DIM   = 2;     // spin-1/2
 45constexpr int64_t  MPO_BOND   = 5;     // Heisenberg MPO bond dim
 46constexpr int64_t  MAX_BOND   = 16;    // MPS bond-dim cap (= full Schmidt rank at N=8)
 47constexpr int32_t  MAX_SWEEPS = 50;
 48constexpr int32_t  KRYLOV_MAX_DIM = 3; // local Krylov subspace cap (boundary-dim constraint)
 49constexpr double   ENERGY_TOL = 1e-10;
 50constexpr double   RESIDUAL   = 1e-6;
 51constexpr double   NOISE_EPS  = 1e-2;
 52constexpr uint64_t RNG_SEED   = 1729;
 53
 54constexpr double   J_COUPLING = 1.0;   // antiferromagnetic
 55
 56// Exact ground-state energy for the N=8 open-boundary Heisenberg chain (J=1),
 57// from dense diagonalization of the 2^8 x 2^8 Hamiltonian.  Printed reference.
 58constexpr double   E0_EXACT   = -3.374932598688;
 59
 60
 61// ============================================================================
 62// Heisenberg MPO builder  (spin-1/2, bond dimension 5)
 63// ============================================================================
 64//
 65// Standard W-matrix: column 0 holds the finish operators (I, Sx, Sy, Sz), the
 66// bottom row the start operators (J*Sx, J*Sy, J*Sz, I); a term J*S^a_i S^a_{i+1}
 67// starts at site i and finishes at i+1, with no on-site term.  Left boundary is
 68// the bottom row, right boundary is column 0.
 69// Site tensor layout (b_L, d, b_R, d): index = aL + bL*(ket + d*(aR + bR*bra)).
 70
 71struct HeisenbergMPO {
 72
 73  std::vector<std::vector<Complex>> hostTensors;
 74  std::vector<void*> gpuPtrs;
 75
 76  // d x d spin-1/2 operators, entry (bra, ket) = <bra | S | ket>.
 77  static Complex I_op(int bra, int ket)  { return (bra == ket) ? Complex(1, 0) : Complex(0, 0); }
 78  static Complex Sx_op(int bra, int ket) { return (bra != ket) ? Complex(0.5, 0) : Complex(0, 0); }
 79  static Complex Sy_op(int bra, int ket) {
 80    if (bra == 0 && ket == 1) return Complex(0, -0.5);   // -i/2
 81    if (bra == 1 && ket == 0) return Complex(0,  0.5);   // +i/2
 82    return Complex(0, 0);
 83  }
 84  static Complex Sz_op(int bra, int ket) {
 85    if (bra == ket) return Complex(bra == 0 ? 0.5 : -0.5, 0);
 86    return Complex(0, 0);
 87  }
 88
 89  void build() {
 90    const Complex zero{0.0, 0.0};
 91    hostTensors.assign(NUM_SITES, {});
 92    gpuPtrs.assign(NUM_SITES, nullptr);
 93
 94    auto idx_full = [&](int64_t bL, int64_t bR, int64_t aL, int64_t ket,
 95                        int64_t aR, int64_t bra) {
 96      return aL + bL * (ket + PHYS_DIM * (aR + bR * bra));
 97    };
 98    // Place a d x d block at MPO corner (aL, aR); M is a callable (bra, ket).
 99    auto place = [&](std::vector<Complex>& T, int64_t bL, int64_t bR,
100                     int64_t aL, int64_t aR, auto M) {
101      for (int bra = 0; bra < PHYS_DIM; ++bra)
102        for (int ket = 0; ket < PHYS_DIM; ++ket)
103          T[idx_full(bL, bR, aL, ket, aR, bra)] += M(bra, ket);
104    };
105
106    const double J = J_COUPLING;
107    for (int32_t site = 0; site < NUM_SITES; ++site) {
108      const int64_t bL  = (site == 0) ? 1 : MPO_BOND;
109      const int64_t bR  = (site == NUM_SITES - 1) ? 1 : MPO_BOND;
110      const int64_t vol = bL * PHYS_DIM * bR * PHYS_DIM;
111      hostTensors[site].assign(vol, zero);
112      auto& T = hostTensors[site];
113
114      if (site == 0) {
115        // Bottom row of W:  [0, J*Sx, J*Sy, J*Sz, I],  aL = 0.
116        place(T, bL, bR, 0, 1, [&](int b, int k){ return Complex(J, 0) * Sx_op(b, k); });
117        place(T, bL, bR, 0, 2, [&](int b, int k){ return Complex(J, 0) * Sy_op(b, k); });
118        place(T, bL, bR, 0, 3, [&](int b, int k){ return Complex(J, 0) * Sz_op(b, k); });
119        place(T, bL, bR, 0, 4, [&](int b, int k){ return I_op(b, k); });
120      } else if (site == NUM_SITES - 1) {
121        // First column of W:  [I, Sx, Sy, Sz, 0]^T,  aR = 0.
122        place(T, bL, bR, 0, 0, [&](int b, int k){ return I_op(b, k); });
123        place(T, bL, bR, 1, 0, [&](int b, int k){ return Sx_op(b, k); });
124        place(T, bL, bR, 2, 0, [&](int b, int k){ return Sy_op(b, k); });
125        place(T, bL, bR, 3, 0, [&](int b, int k){ return Sz_op(b, k); });
126      } else {
127        // Full bulk W: first column (finish) + bottom row (start).
128        place(T, bL, bR, 0, 0, [&](int b, int k){ return I_op(b, k); });
129        place(T, bL, bR, 1, 0, [&](int b, int k){ return Sx_op(b, k); });
130        place(T, bL, bR, 2, 0, [&](int b, int k){ return Sy_op(b, k); });
131        place(T, bL, bR, 3, 0, [&](int b, int k){ return Sz_op(b, k); });
132        place(T, bL, bR, 4, 1, [&](int b, int k){ return Complex(J, 0) * Sx_op(b, k); });
133        place(T, bL, bR, 4, 2, [&](int b, int k){ return Complex(J, 0) * Sy_op(b, k); });
134        place(T, bL, bR, 4, 3, [&](int b, int k){ return Complex(J, 0) * Sz_op(b, k); });
135        place(T, bL, bR, 4, 4, [&](int b, int k){ return I_op(b, k); });
136      }
137
138      gpuPtrs[site] = createInitializeArrayGPU(hostTensors[site]);
139    }
140  }
141
142  void destroy() {
143    for (auto & ptr : gpuPtrs) {
144      if (ptr) { destroyArrayGPU(ptr); ptr = nullptr; }
145    }
146  }
147};
148
149
150// ============================================================================
151// Noisy Neel initial MPS  ( |up,down,up,...> + epsilon * Gaussian noise )
152// ============================================================================
153//
154// Neel lies in the total-Sz = 0 sector of the singlet ground state; the noise
155// gives DMRG a generic (non-exact) starting point.
156
157struct NoisyNeelMPS {
158
159  std::vector<std::vector<Complex>> hostTensors;
160  std::vector<void*> gpuPtrs;
161
162  void build(const std::vector<int64_t>& bondDims, double epsilon, uint64_t seed) {
163    const Complex zero{0.0, 0.0}, one{1.0, 0.0};
164    std::mt19937_64 rng(seed);
165    std::normal_distribution<double> noise(0.0, epsilon);
166
167    hostTensors.assign(NUM_SITES, {});
168    gpuPtrs.assign(NUM_SITES, nullptr);
169
170    for (int32_t site = 0; site < NUM_SITES; ++site) {
171      const int64_t bL  = (site == 0) ? 1 : bondDims[site - 1];
172      const int64_t bR  = (site == NUM_SITES - 1) ? 1 : bondDims[site];
173      const int64_t vol = bL * PHYS_DIM * bR;
174      hostTensors[site].assign(vol, zero);
175
176      auto idx = [&](int64_t aL, int64_t sigma, int64_t aR) {
177        return aL + bL * (sigma + PHYS_DIM * aR);
178      };
179
180      // Neel seed: site i occupies |i mod 2>  (alternating up / down).
181      const int64_t spin = site % 2;
182      hostTensors[site][idx(0, spin, 0)] = one;
183      // Per-element Gaussian noise (Re, Im) iid N(0, epsilon^2).
184      for (auto & v : hostTensors[site]) {
185        v += Complex(noise(rng), noise(rng));
186      }
187
188      gpuPtrs[site] = createInitializeArrayGPU(hostTensors[site]);
189    }
190  }
191
192  void destroy() {
193    for (auto & ptr : gpuPtrs) {
194      if (ptr) { destroyArrayGPU(ptr); ptr = nullptr; }
195    }
196  }
197};
198
199
200// ============================================================================
201// Example workflow
202// ============================================================================
203
204void exampleWorkflow(cudensitymatHandle_t handle)
205{
206  if (verbose) {
207    std::cout << "DMRG eigensolver example\n"
208              << "  Model:           spin-1/2 antiferromagnetic Heisenberg chain\n"
209              << "  H = J * sum_i ( Sx_i Sx_{i+1} + Sy_i Sy_{i+1} + Sz_i Sz_{i+1} )\n"
210              << "  N (sites)       = " << NUM_SITES  << "\n"
211              << "  d (phys dim)    = " << PHYS_DIM   << "\n"
212              << "  J (coupling)    = " << J_COUPLING << "\n"
213              << "  chi (bond cap)  = " << MAX_BOND   << "\n"
214              << "  max sweeps      = " << MAX_SWEEPS << "\n"
215              << "  energy tol      = " << ENERGY_TOL << "\n\n";
216  }
217
218  // --- 1. Build the Heisenberg MPO ---
219  HeisenbergMPO mpo;
220  mpo.build();
221  if (verbose) std::cout << "Built Heisenberg MPO (bond dim " << MPO_BOND << ")\n";
222
223  const std::vector<int64_t> spaceShape(NUM_SITES, PHYS_DIM);
224  std::vector<int64_t> mpoBondDims(NUM_SITES - 1, MPO_BOND);
225
226  cudensitymatMatrixProductOperator_t mpoHandle = nullptr;
227  std::vector<cudensitymatWrappedTensorCallback_t> mpoCB(
228      NUM_SITES, cudensitymatTensorCallbackNone);
229  std::vector<cudensitymatWrappedTensorGradientCallback_t> mpoGCB(
230      NUM_SITES, cudensitymatTensorGradientCallbackNone);
231  HANDLE_CUDM_ERROR(cudensitymatCreateMatrixProductOperator(handle,
232                      NUM_SITES,
233                      spaceShape.data(),
234                      CUDENSITYMAT_BOUNDARY_CONDITION_OPEN,
235                      mpoBondDims.data(),
236                      kDataType,
237                      mpo.gpuPtrs.data(),
238                      mpoCB.data(),
239                      mpoGCB.data(),
240                      &mpoHandle));
241  if (verbose) std::cout << "Created MPO handle\n";
242
243  // --- 2. Wrap the MPO into an Operator (one term, one MPO product) ---
244  cudensitymatOperatorTerm_t operatorTerm = nullptr;
245  HANDLE_CUDM_ERROR(cudensitymatCreateOperatorTerm(handle,
246                      NUM_SITES, spaceShape.data(), &operatorTerm));
247
248  std::vector<int32_t> modesActedOn(NUM_SITES);
249  std::iota(modesActedOn.begin(), modesActedOn.end(), 0);
250  std::vector<int32_t> modeDuality(NUM_SITES, 0);
251  std::vector<int32_t> mpoConj = {0};
252  HANDLE_CUDM_ERROR(cudensitymatOperatorTermAppendMPOProduct(handle,
253                      operatorTerm,
254                      1, &mpoHandle, mpoConj.data(),
255                      modesActedOn.data(), modeDuality.data(),
256                      make_cuDoubleComplex(1.0, 0.0),
257                      cudensitymatScalarCallbackNone,
258                      cudensitymatScalarGradientCallbackNone));
259
260  cudensitymatOperator_t hamiltonian = nullptr;
261  HANDLE_CUDM_ERROR(cudensitymatCreateOperator(handle,
262                      NUM_SITES, spaceShape.data(), &hamiltonian));
263  HANDLE_CUDM_ERROR(cudensitymatOperatorAppendTerm(handle,
264                      hamiltonian, operatorTerm, 0,
265                      make_cuDoubleComplex(1.0, 0.0),
266                      cudensitymatScalarCallbackNone,
267                      cudensitymatScalarGradientCallbackNone));
268  if (verbose) std::cout << "Created Hamiltonian operator from MPO\n";
269
270  // --- 3. Build the initial MPS state ---
271  //
272  // Bond dims are capped at the natural Schmidt-rank bound min(d^i, d^(N-i)).
273  std::vector<int64_t> mpsBondDims(NUM_SITES - 1);
274  for (int32_t i = 0; i < NUM_SITES - 1; ++i) {
275    int64_t leftDim = 1;
276    for (int32_t j = 0; j <= i; ++j) leftDim *= spaceShape[j];
277    int64_t rightDim = 1;
278    for (int32_t j = i + 1; j < NUM_SITES; ++j) rightDim *= spaceShape[j];
279    mpsBondDims[i] = std::min<int64_t>({MAX_BOND, leftDim, rightDim});
280  }
281
282  cudensitymatState_t mpsState = nullptr;
283  HANDLE_CUDM_ERROR(cudensitymatCreateStateMPS(handle,
284                      CUDENSITYMAT_STATE_PURITY_PURE,
285                      NUM_SITES,
286                      spaceShape.data(),
287                      CUDENSITYMAT_BOUNDARY_CONDITION_OPEN,
288                      mpsBondDims.data(),
289                      kDataType,
290                      /*batchSize=*/1,
291                      &mpsState));
292
293  NoisyNeelMPS mps;
294  mps.build(mpsBondDims, NOISE_EPS, RNG_SEED);
295
296  int32_t numComponents = 0;
297  HANDLE_CUDM_ERROR(cudensitymatStateGetNumComponents(handle, mpsState, &numComponents));
298  std::vector<std::size_t> componentSizes(numComponents);
299  HANDLE_CUDM_ERROR(cudensitymatStateGetComponentStorageSize(handle, mpsState,
300                      numComponents, componentSizes.data()));
301  HANDLE_CUDM_ERROR(cudensitymatStateAttachComponentStorage(handle, mpsState,
302                      numComponents, mps.gpuPtrs.data(), componentSizes.data()));
303  if (verbose) std::cout << "Built and attached noisy-Neel MPS (alternating up/down + eps*Gaussian, seed="
304                         << RNG_SEED << ")\n";
305
306  // --- 4. Create EigenDecomposition (SCOPE_SPLIT + APPROACH_KRYLOV + SMALLEST_REAL) ---
307  cudensitymatEigenDecomposition_t eigen = nullptr;
308  HANDLE_CUDM_ERROR(cudensitymatCreateEigenDecomposition(handle,
309                      hamiltonian,
310                      /*isHermitian=*/1,
311                      CUDENSITYMAT_EIGEN_SPECTRUM_SMALLEST_REAL,
312                      CUDENSITYMAT_EIGEN_SCOPE_SPLIT,
313                      CUDENSITYMAT_EIGEN_APPROACH_KRYLOV,
314                      &eigen));
315  if (verbose) std::cout << "Created EigenDecomposition object\n";
316
317  // --- 5. Configure DMRG attributes (MAX_SWEEPS, ENERGY_TOLERANCE).
318  // Sub-config values are deep-copied at Configure, so the handle can be freed now.
319  cudensitymatEigenDecompositionScopeSplitDMRGConfig_t dmrgCfg = nullptr;
320  HANDLE_CUDM_ERROR(cudensitymatCreateEigenDecompositionScopeSplitDMRGConfig(handle, &dmrgCfg));
321  {
322    const int32_t maxSweeps = MAX_SWEEPS;
323    HANDLE_CUDM_ERROR(cudensitymatEigenDecompositionScopeSplitDMRGConfigSetAttribute(
324        handle, dmrgCfg,
325        CUDENSITYMAT_EIGEN_SPLIT_SCOPE_DMRG_MAX_SWEEPS,
326        &maxSweeps, sizeof(maxSweeps)));
327    const double energyTol = ENERGY_TOL;
328    HANDLE_CUDM_ERROR(cudensitymatEigenDecompositionScopeSplitDMRGConfigSetAttribute(
329        handle, dmrgCfg,
330        CUDENSITYMAT_EIGEN_SPLIT_SCOPE_DMRG_ENERGY_TOLERANCE,
331        &energyTol, sizeof(energyTol)));
332  }
333  HANDLE_CUDM_ERROR(cudensitymatEigenDecompositionConfigure(handle, eigen,
334                      CUDENSITYMAT_EIGEN_SPLIT_SCOPE_DMRG_CONFIG,
335                      &dmrgCfg, sizeof(dmrgCfg)));
336  HANDLE_CUDM_ERROR(cudensitymatDestroyEigenDecompositionScopeSplitDMRGConfig(dmrgCfg));
337  dmrgCfg = nullptr;
338  if (verbose) std::cout << "Configured DMRG sub-config (max_sweeps="
339                         << MAX_SWEEPS << ", energy_tol=" << ENERGY_TOL << ")\n";
340
341  // --- 5b. Configure the Krylov sub-config.
342  // The 1-site local problem is only bL*d*bR = 4 at the boundary; the engine
343  // needs dim >= (Krylov max_dim + min block size), so cap max_dim at 3.
344  cudensitymatEigenDecompositionApproachKrylovConfig_t krylovCfg = nullptr;
345  HANDLE_CUDM_ERROR(cudensitymatCreateEigenDecompositionApproachKrylovConfig(handle, &krylovCfg));
346  {
347    const int32_t krylovMaxDim = KRYLOV_MAX_DIM;
348    HANDLE_CUDM_ERROR(cudensitymatEigenDecompositionApproachKrylovConfigSetAttribute(
349        handle, krylovCfg,
350        CUDENSITYMAT_EIGEN_APPROACH_KRYLOV_MAX_DIM,
351        &krylovMaxDim, sizeof(krylovMaxDim)));
352  }
353  HANDLE_CUDM_ERROR(cudensitymatEigenDecompositionConfigure(handle, eigen,
354                      CUDENSITYMAT_EIGEN_APPROACH_KRYLOV_CONFIG,
355                      &krylovCfg, sizeof(krylovCfg)));
356  HANDLE_CUDM_ERROR(cudensitymatDestroyEigenDecompositionApproachKrylovConfig(krylovCfg));
357  krylovCfg = nullptr;
358  if (verbose) std::cout << "Configured Krylov sub-config (max_dim=" << KRYLOV_MAX_DIM << ")\n";
359
360  // --- 6. Prepare + attach workspace ---
361  cudensitymatWorkspaceDescriptor_t workspaceDescr = nullptr;
362  HANDLE_CUDM_ERROR(cudensitymatCreateWorkspace(handle, &workspaceDescr));
363
364  std::size_t freeMem = 0, totalMem = 0;
365  HANDLE_CUDA_ERROR(cudaMemGetInfo(&freeMem, &totalMem));
366  freeMem = static_cast<std::size_t>(static_cast<double>(freeMem) * 0.85);
367  if (verbose) std::cout << "Available workspace memory (bytes) = " << freeMem << "\n";
368
369  HANDLE_CUDM_ERROR(cudensitymatEigenDecompositionPrepare(handle, eigen,
370                      /*maxEigenStates=*/1, mpsState, CUDENSITYMAT_COMPUTE_64F,
371                      freeMem, workspaceDescr, /*stream=*/0x0));
372
373  std::size_t scratchSize = 0;
374  HANDLE_CUDM_ERROR(cudensitymatWorkspaceGetMemorySize(handle, workspaceDescr,
375                      CUDENSITYMAT_MEMSPACE_DEVICE,
376                      CUDENSITYMAT_WORKSPACE_SCRATCH,
377                      &scratchSize));
378  void * scratchBuf = nullptr;
379  if (scratchSize > 0) {
380    HANDLE_CUDA_ERROR(cudaMalloc(&scratchBuf, scratchSize));
381    HANDLE_CUDM_ERROR(cudensitymatWorkspaceSetMemory(handle, workspaceDescr,
382                        CUDENSITYMAT_MEMSPACE_DEVICE,
383                        CUDENSITYMAT_WORKSPACE_SCRATCH,
384                        scratchBuf, scratchSize));
385  }
386  if (verbose) std::cout << "Prepared DMRG plan; scratch workspace = " << scratchSize << " bytes\n";
387
388  // --- 7. Compute the ground-state pair (eigenvalue, MPS) ---
389  void * eigenvalueGpu = createArrayGPU<Complex>(1);
390  double residual = RESIDUAL;
391  cudensitymatState_t eigenstatesArr[1] = { mpsState };
392
393  HANDLE_CUDA_ERROR(cudaDeviceSynchronize());
394  HANDLE_CUDM_ERROR(cudensitymatEigenDecompositionCompute(handle, eigen,
395                      /*time=*/0.0,
396                      /*batchSize=*/1,
397                      /*numParams=*/0, /*params=*/nullptr,
398                      /*numEigenStates=*/1, eigenstatesArr,
399                      eigenvalueGpu, &residual,
400                      workspaceDescr, /*stream=*/0x0));
401  HANDLE_CUDA_ERROR(cudaDeviceSynchronize());
402
403  cuDoubleComplex eigenvalueHost{0.0, 0.0};
404  HANDLE_CUDA_ERROR(cudaMemcpy(&eigenvalueHost, eigenvalueGpu,
405                               sizeof(cuDoubleComplex), cudaMemcpyDeviceToHost));
406
407  const double eDmrg     = eigenvalueHost.x;
408  const double energyErr = std::abs(eDmrg - E0_EXACT);
409  const bool   converged = (energyErr < 1e-6) && (residual < RESIDUAL);
410
411  std::cout << "\n=================================================================\n"
412            << "DMRG ground-state energy : " << std::scientific << std::setprecision(12)
413            << eDmrg << "\n"
414            << "Exact (dense ED, N=" << NUM_SITES << ")  : " << E0_EXACT << "\n"
415            << "Energy error             : " << std::scientific << std::setprecision(3)
416            << energyErr << "\n"
417            << "Final residual           : " << residual << "\n"
418            << "Result                   : " << (converged ? "PASS" : "FAIL") << "\n"
419            << "=================================================================\n\n";
420
421  // --- 8. Cleanup (reverse Create order) ---
422  destroyArrayGPU(eigenvalueGpu);
423  if (scratchBuf) HANDLE_CUDA_ERROR(cudaFree(scratchBuf));
424  HANDLE_CUDM_ERROR(cudensitymatDestroyWorkspace(workspaceDescr));
425  HANDLE_CUDM_ERROR(cudensitymatDestroyEigenDecomposition(eigen));
426  HANDLE_CUDM_ERROR(cudensitymatDestroyState(mpsState));
427  HANDLE_CUDM_ERROR(cudensitymatDestroyOperator(hamiltonian));
428  HANDLE_CUDM_ERROR(cudensitymatDestroyOperatorTerm(operatorTerm));
429  HANDLE_CUDM_ERROR(cudensitymatDestroyMatrixProductOperator(mpoHandle));
430  mps.destroy();
431  mpo.destroy();
432
433  if (verbose) std::cout << "Destroyed all resources\n";
434}
435
436
437int main(int /*argc*/, char ** /*argv*/)
438{
439  HANDLE_CUDA_ERROR(cudaSetDevice(0));
440  if (verbose) std::cout << "Set active CUDA device 0\n";
441
442  cudensitymatHandle_t handle;
443  HANDLE_CUDM_ERROR(cudensitymatCreate(&handle));
444  if (verbose) std::cout << "Created cuDensityMat library handle\n";
445
446  exampleWorkflow(handle);
447
448  HANDLE_CUDM_ERROR(cudensitymatDestroy(handle));
449  if (verbose) std::cout << "Destroyed cuDensityMat library handle\n";
450
451  HANDLE_CUDA_ERROR(cudaDeviceReset());
452  return 0;
453}

Code example (serial execution of 2-site MPS-DMRG ground-state eigensolver)#

The following code example illustrates how to use the cuDensityMat library to compute the ground state of a transverse-field Ising Hamiltonian encoded as an MPO, acting on a pure MPS state, using the split-scope 2-site DMRG method with CUDENSITYMAT_EIGEN_SPLIT_SCOPE_DMRG_NUM_SITES set to 2. The 2-site variant solves two adjacent sites jointly and re-splits the result with an SVD truncation, so the intervening bond dimension adapts during the computation. The example attaches an SVD configuration via CUDENSITYMAT_EIGEN_SPLIT_SCOPE_DMRG_SVD_CONFIG (setting CUDENSITYMAT_SVD_CONFIG_MAX_EXTENT), seeds the initial current bond extents strictly between 1 and the buffer maximum via cudensitymatStateMPSSetCurrentBondExtents, computes the ground state, then queries cudensitymatStateMPSGetCurrentBondExtents to report the adapted bond extents. The full sample code can be found in the NVIDIA/cuQuantum repository (main serial 2-site MPS-DMRG code as well as the utility code).

  1/* Copyright (c) 2024-2026, NVIDIA CORPORATION & AFFILIATES.
  2 *
  3 * SPDX-License-Identifier: BSD-3-Clause
  4 */
  5
  6// Two-site DMRG eigensolver example.
  7//
  8// Computes the ground state of the 1-D transverse-field Ising model
  9//   H = -J * sum_i Z_i Z_{i+1}  -  h * sum_i X_i   (open boundary)
 10// with the 2-site DMRG solver (SCOPE_SPLIT + APPROACH_KRYLOV + SMALLEST_REAL)
 11// on an MPS state.  Two adjacent MPS sites are merged, locally eigensolved,
 12// SVD-split with adaptive bond truncation, and synced back per step.
 13//
 14// The chain is blocked: each MPS site carries a pair of qubits (phys dim 4,
 15// basis {|00>, |01>, |10>, |11>}), so 3 MPS sites model a 6-qubit chain.  A
 16// small SVD MAX_EXTENT cap and a partial-fill initial bond layout (current
 17// bond extents strictly between 1 and the buffer maximum) exercise the 2-site
 18// memory-layout and adaptive-growth contracts.
 19//
 20// 3 MPS sites is intentionally small: both bonds are boundary-adjacent, so the
 21// partial-fill current<max layout works here pending a cuTensorNet update that
 22// enables N>=4 interior-bond growth.
 23//
 24// Workflow:
 25//  1. Build the blocked-TFIM MPO (bond dim 3)
 26//  2. Wrap the MPO into a Hamiltonian operator
 27//  3. Create an initial MPS state (noisy product) + partial-fill bond layout
 28//  4. Create the EigenDecomposition (SMALLEST_REAL + SCOPE_SPLIT + KRYLOV)
 29//  5. Configure DMRG (NUM_SITES=2 + SVD config), Krylov, Prepare, attach
 30//  6. Compute the ground-state pair
 31//  7. Report the energy and the adapted (current vs max) bond extents
 32//  8. Clean up
 33
 34#include <cudensitymat.h>
 35#include "helpers.h"
 36
 37#include <algorithm>
 38#include <cmath>
 39#include <complex>
 40#include <vector>
 41#include <numeric>
 42#include <random>
 43#include <iostream>
 44#include <iomanip>
 45#include <cassert>
 46
 47
 48using Complex = std::complex<double>;
 49constexpr cudaDataType_t kDataType = CUDA_C_64F;
 50
 51constexpr bool verbose = true;
 52
 53// --- Simulation parameters ---
 54constexpr int32_t  NUM_SITES   = 3;     // blocked MPS sites (== 6 qubits); small by design
 55constexpr int64_t  PHYS_DIM    = 4;     // 2 qubits per blocked site
 56constexpr int64_t  MPO_BOND    = 3;     // standard TFIM MPO bond dim
 57constexpr int64_t  MAX_BOND    = 8;     // MPS bond-dim buffer cap
 58constexpr int32_t  NUM_DMRG_SITES = 2;  // 2-site DMRG
 59constexpr int64_t  SVD_MAX_EXTENT = 3;  // modest SVD truncation cap (< buffer extent)
 60constexpr int64_t  INIT_CURRENT_BOND = 2; // partial fill: strictly between 1 and the maximum
 61constexpr int32_t  MAX_SWEEPS  = 30;
 62constexpr int32_t  KRYLOV_MAX_DIM = 5;  // local Krylov subspace cap
 63constexpr double   ENERGY_TOL  = 1e-10;
 64constexpr double   RESIDUAL    = 1e-6;
 65constexpr double   NOISE_EPS   = 1e-2;
 66constexpr uint64_t RNG_SEED    = 1729;
 67
 68constexpr double   J_COUPLING  = 1.0;   // ZZ coupling
 69constexpr double   H_FIELD     = 0.5;   // transverse field
 70
 71// Exact ground-state energy for the 6-qubit OBC TFIM (J=1, h=0.5), from dense
 72// diagonalization of the 2^6 x 2^6 Hamiltonian.  Printed reference.
 73constexpr double   E0_EXACT    = -5.522029570800221;
 74
 75
 76// ============================================================================
 77// Blocked-TFIM MPO  (phys dim 4 per site, bond dimension 3)
 78// ============================================================================
 79//
 80// Each MPS site carries two qubits (q_left, q_right), basis index i = 2*q_left
 81// + q_right.  The W-matrix carries the standard 3-state TFIM finite automaton:
 82// column 0 holds the finish operators, the bottom row the start operators, plus
 83// the on-site block term B = -J*ZZ - h*(Xleft + Xright).
 84// Site tensor layout (aL, ket, aR, bra): idx = aL + bL*(ket + d*(aR + bR*bra)).
 85
 86struct BlockedTFIMMPO {
 87
 88  std::vector<std::vector<Complex>> hostTensors;
 89  std::vector<void*> gpuPtrs;
 90
 91  // 4x4 operators on a 2-qubit block; entry (bra, ket).
 92  static Complex I4_(int r, int c) { return (r == c) ? Complex(1, 0) : Complex(0, 0); }
 93  static Complex Zleft_(int r, int c) {
 94    if (r != c) return Complex(0, 0);
 95    return (r < 2) ? Complex(1, 0) : Complex(-1, 0);     // Z on first qubit: diag(+,+,-,-)
 96  }
 97  static Complex Zright_(int r, int c) {
 98    if (r != c) return Complex(0, 0);
 99    return ((r & 1) == 0) ? Complex(1, 0) : Complex(-1, 0);   // Z on second qubit: diag(+,-,+,-)
100  }
101  static Complex ZZ_(int r, int c) {
102    if (r != c) return Complex(0, 0);
103    const int rL = (r >> 1) & 1, rR = r & 1;
104    return (rL == rR) ? Complex(1, 0) : Complex(-1, 0);  // Z⊗Z: diag(+,-,-,+)
105  }
106  static Complex Xleft_(int r, int c)  { return (r == (c ^ 0b10)) ? Complex(1, 0) : Complex(0, 0); }
107  static Complex Xright_(int r, int c) { return (r == (c ^ 0b01)) ? Complex(1, 0) : Complex(0, 0); }
108  // Within-block on-site Hamiltonian B = -J*ZZ - h*(Xleft + Xright).
109  static Complex B_(int r, int c) {
110    return -Complex(J_COUPLING, 0) * ZZ_(r, c)
111           - Complex(H_FIELD, 0) * (Xleft_(r, c) + Xright_(r, c));
112  }
113
114  void build() {
115    const Complex zero{0.0, 0.0};
116    hostTensors.assign(NUM_SITES, {});
117    gpuPtrs.assign(NUM_SITES, nullptr);
118
119    // Left boundary site 0 — shape (bL=1, phys=4, bR=3, phys=4).
120    {
121      const int64_t bL = 1, bR = MPO_BOND;
122      hostTensors[0].assign(bL * PHYS_DIM * bR * PHYS_DIM, zero);
123      auto idx = [&](int64_t aL, int64_t ket, int64_t aR, int64_t bra) {
124        return aL + bL * (ket + PHYS_DIM * (aR + bR * bra));
125      };
126      for (int bra = 0; bra < PHYS_DIM; ++bra) {
127        for (int ket = 0; ket < PHYS_DIM; ++ket) {
128          hostTensors[0][idx(0, ket, 0, bra)] = B_(bra, ket);
129          hostTensors[0][idx(0, ket, 1, bra)] = Zright_(bra, ket);
130          hostTensors[0][idx(0, ket, 2, bra)] = I4_(bra, ket);
131        }
132      }
133      gpuPtrs[0] = createInitializeArrayGPU(hostTensors[0]);
134    }
135
136    // Bulk sites — shape (bL=3, phys=4, bR=3, phys=4).
137    for (int32_t site = 1; site < NUM_SITES - 1; ++site) {
138      const int64_t bL = MPO_BOND, bR = MPO_BOND;
139      hostTensors[site].assign(bL * PHYS_DIM * bR * PHYS_DIM, zero);
140      auto idx = [&](int64_t aL, int64_t ket, int64_t aR, int64_t bra) {
141        return aL + bL * (ket + PHYS_DIM * (aR + bR * bra));
142      };
143      for (int bra = 0; bra < PHYS_DIM; ++bra) {
144        for (int ket = 0; ket < PHYS_DIM; ++ket) {
145          hostTensors[site][idx(0, ket, 0, bra)] = I4_(bra, ket);
146          hostTensors[site][idx(1, ket, 0, bra)] = -Complex(J_COUPLING, 0) * Zleft_(bra, ket);
147          hostTensors[site][idx(2, ket, 0, bra)] = B_(bra, ket);
148          hostTensors[site][idx(2, ket, 1, bra)] = Zright_(bra, ket);
149          hostTensors[site][idx(2, ket, 2, bra)] = I4_(bra, ket);
150        }
151      }
152      gpuPtrs[site] = createInitializeArrayGPU(hostTensors[site]);
153    }
154
155    // Right boundary site N-1 — shape (bL=3, phys=4, bR=1, phys=4).
156    {
157      const int32_t last = NUM_SITES - 1;
158      const int64_t bL = MPO_BOND, bR = 1;
159      hostTensors[last].assign(bL * PHYS_DIM * bR * PHYS_DIM, zero);
160      auto idx = [&](int64_t aL, int64_t ket, int64_t aR, int64_t bra) {
161        return aL + bL * (ket + PHYS_DIM * (aR + bR * bra));
162      };
163      for (int bra = 0; bra < PHYS_DIM; ++bra) {
164        for (int ket = 0; ket < PHYS_DIM; ++ket) {
165          hostTensors[last][idx(0, ket, 0, bra)] = I4_(bra, ket);
166          hostTensors[last][idx(1, ket, 0, bra)] = -Complex(J_COUPLING, 0) * Zleft_(bra, ket);
167          hostTensors[last][idx(2, ket, 0, bra)] = B_(bra, ket);
168        }
169      }
170      gpuPtrs[last] = createInitializeArrayGPU(hostTensors[last]);
171    }
172  }
173
174  void destroy() {
175    for (auto & ptr : gpuPtrs) {
176      if (ptr) { destroyArrayGPU(ptr); ptr = nullptr; }
177    }
178  }
179};
180
181
182// ============================================================================
183// Noisy product initial MPS  ( |00,00,...> + epsilon * Gaussian noise )
184// ============================================================================
185//
186// A low-energy ferromagnetic product seed gives DMRG a generic, non-exact
187// starting point.
188
189struct NoisyProductMPS {
190
191  std::vector<std::vector<Complex>> hostTensors;
192  std::vector<void*> gpuPtrs;
193
194  void build(const std::vector<int64_t>& bondDims, double epsilon, uint64_t seed) {
195    const Complex zero{0.0, 0.0}, one{1.0, 0.0};
196    std::mt19937_64 rng(seed);
197    std::normal_distribution<double> noise(0.0, epsilon);
198
199    hostTensors.assign(NUM_SITES, {});
200    gpuPtrs.assign(NUM_SITES, nullptr);
201
202    for (int32_t site = 0; site < NUM_SITES; ++site) {
203      const int64_t bL  = (site == 0) ? 1 : bondDims[site - 1];
204      const int64_t bR  = (site == NUM_SITES - 1) ? 1 : bondDims[site];
205      const int64_t vol = bL * PHYS_DIM * bR;
206      hostTensors[site].assign(vol, zero);
207
208      auto idx = [&](int64_t aL, int64_t sigma, int64_t aR) {
209        return aL + bL * (sigma + PHYS_DIM * aR);
210      };
211
212      // Product seed: every block in qubit-pair state |00> (index 0).
213      hostTensors[site][idx(0, 0, 0)] = one;
214      // Per-element Gaussian noise (Re, Im) iid N(0, epsilon^2).
215      for (auto & v : hostTensors[site]) {
216        v += Complex(noise(rng), noise(rng));
217      }
218
219      gpuPtrs[site] = createInitializeArrayGPU(hostTensors[site]);
220    }
221  }
222
223  void destroy() {
224    for (auto & ptr : gpuPtrs) {
225      if (ptr) { destroyArrayGPU(ptr); ptr = nullptr; }
226    }
227  }
228};
229
230
231// ============================================================================
232// Example workflow
233// ============================================================================
234
235void exampleWorkflow(cudensitymatHandle_t handle)
236{
237  if (verbose) {
238    std::cout << "Two-site DMRG eigensolver example\n"
239              << "  Model:           blocked transverse-field Ising chain (phys dim 4)\n"
240              << "  H = -J * sum_i Z_i Z_{i+1}  -  h * sum_i X_i  (open boundary)\n"
241              << "  N (MPS sites)   = " << NUM_SITES      << "   (== " << 2 * NUM_SITES << " qubits)\n"
242              << "  d (phys dim)    = " << PHYS_DIM       << "\n"
243              << "  J (coupling)    = " << J_COUPLING     << "\n"
244              << "  h (field)       = " << H_FIELD        << "\n"
245              << "  chi (bond cap)  = " << MAX_BOND       << "\n"
246              << "  DMRG num_sites  = " << NUM_DMRG_SITES << "\n"
247              << "  SVD max_extent  = " << SVD_MAX_EXTENT << "\n"
248              << "  max sweeps      = " << MAX_SWEEPS     << "\n"
249              << "  energy tol      = " << ENERGY_TOL     << "\n\n";
250  }
251
252  // --- 1. Build the blocked-TFIM MPO ---
253  BlockedTFIMMPO mpo;
254  mpo.build();
255  if (verbose) std::cout << "Built blocked-TFIM MPO (bond dim " << MPO_BOND << ")\n";
256
257  const std::vector<int64_t> spaceShape(NUM_SITES, PHYS_DIM);
258  std::vector<int64_t> mpoBondDims(NUM_SITES - 1, MPO_BOND);
259
260  cudensitymatMatrixProductOperator_t mpoHandle = nullptr;
261  std::vector<cudensitymatWrappedTensorCallback_t> mpoCB(
262      NUM_SITES, cudensitymatTensorCallbackNone);
263  std::vector<cudensitymatWrappedTensorGradientCallback_t> mpoGCB(
264      NUM_SITES, cudensitymatTensorGradientCallbackNone);
265  HANDLE_CUDM_ERROR(cudensitymatCreateMatrixProductOperator(handle,
266                      NUM_SITES,
267                      spaceShape.data(),
268                      CUDENSITYMAT_BOUNDARY_CONDITION_OPEN,
269                      mpoBondDims.data(),
270                      kDataType,
271                      mpo.gpuPtrs.data(),
272                      mpoCB.data(),
273                      mpoGCB.data(),
274                      &mpoHandle));
275  if (verbose) std::cout << "Created MPO handle\n";
276
277  // --- 2. Wrap the MPO into an Operator (one term, one MPO product) ---
278  cudensitymatOperatorTerm_t operatorTerm = nullptr;
279  HANDLE_CUDM_ERROR(cudensitymatCreateOperatorTerm(handle,
280                      NUM_SITES, spaceShape.data(), &operatorTerm));
281
282  std::vector<int32_t> modesActedOn(NUM_SITES);
283  std::iota(modesActedOn.begin(), modesActedOn.end(), 0);
284  std::vector<int32_t> modeDuality(NUM_SITES, 0);
285  std::vector<int32_t> mpoConj = {0};
286  HANDLE_CUDM_ERROR(cudensitymatOperatorTermAppendMPOProduct(handle,
287                      operatorTerm,
288                      1, &mpoHandle, mpoConj.data(),
289                      modesActedOn.data(), modeDuality.data(),
290                      make_cuDoubleComplex(1.0, 0.0),
291                      cudensitymatScalarCallbackNone,
292                      cudensitymatScalarGradientCallbackNone));
293
294  cudensitymatOperator_t hamiltonian = nullptr;
295  HANDLE_CUDM_ERROR(cudensitymatCreateOperator(handle,
296                      NUM_SITES, spaceShape.data(), &hamiltonian));
297  HANDLE_CUDM_ERROR(cudensitymatOperatorAppendTerm(handle,
298                      hamiltonian, operatorTerm, 0,
299                      make_cuDoubleComplex(1.0, 0.0),
300                      cudensitymatScalarCallbackNone,
301                      cudensitymatScalarGradientCallbackNone));
302  if (verbose) std::cout << "Created Hamiltonian operator from MPO\n";
303
304  // --- 3. Build the initial MPS state ---
305  //
306  // Buffer (maximum) bond dims are capped at the natural Schmidt-rank bound.
307  std::vector<int64_t> mpsBondDims(NUM_SITES - 1);
308  for (int32_t i = 0; i < NUM_SITES - 1; ++i) {
309    int64_t leftDim = 1;
310    for (int32_t j = 0; j <= i; ++j) leftDim *= spaceShape[j];
311    int64_t rightDim = 1;
312    for (int32_t j = i + 1; j < NUM_SITES; ++j) rightDim *= spaceShape[j];
313    mpsBondDims[i] = std::min<int64_t>({MAX_BOND, leftDim, rightDim});
314  }
315
316  cudensitymatState_t mpsState = nullptr;
317  HANDLE_CUDM_ERROR(cudensitymatCreateStateMPS(handle,
318                      CUDENSITYMAT_STATE_PURITY_PURE,
319                      NUM_SITES,
320                      spaceShape.data(),
321                      CUDENSITYMAT_BOUNDARY_CONDITION_OPEN,
322                      mpsBondDims.data(),
323                      kDataType,
324                      /*batchSize=*/1,
325                      &mpsState));
326
327  NoisyProductMPS mps;
328  mps.build(mpsBondDims, NOISE_EPS, RNG_SEED);
329
330  int32_t numComponents = 0;
331  HANDLE_CUDM_ERROR(cudensitymatStateGetNumComponents(handle, mpsState, &numComponents));
332  std::vector<std::size_t> componentSizes(numComponents);
333  HANDLE_CUDM_ERROR(cudensitymatStateGetComponentStorageSize(handle, mpsState,
334                      numComponents, componentSizes.data()));
335  HANDLE_CUDM_ERROR(cudensitymatStateAttachComponentStorage(handle, mpsState,
336                      numComponents, mps.gpuPtrs.data(), componentSizes.data()));
337  if (verbose) std::cout << "Built and attached noisy-product MPS (seed=" << RNG_SEED << ")\n";
338
339  // --- 3b. Partial-fill bond layout: set initial CURRENT bond extents strictly
340  // between 1 and the buffer maximum, exercising the 2-site partial-fill layout.
341  const int32_t numBonds = NUM_SITES - 1;
342  std::vector<int64_t> initialCurrentBonds(numBonds, INIT_CURRENT_BOND);
343  HANDLE_CUDM_ERROR(cudensitymatStateMPSSetCurrentBondExtents(handle, mpsState,
344                      initialCurrentBonds.data()));
345  if (verbose) {
346    std::cout << "Set initial current bond extents (partial fill) = {";
347    for (int32_t b = 0; b < numBonds; ++b)
348      std::cout << initialCurrentBonds[b] << (b + 1 < numBonds ? "," : "");
349    std::cout << "}  (buffer max = {";
350    for (int32_t b = 0; b < numBonds; ++b)
351      std::cout << mpsBondDims[b] << (b + 1 < numBonds ? "," : "");
352    std::cout << "})\n";
353  }
354
355  // --- 4. Create EigenDecomposition (SCOPE_SPLIT + APPROACH_KRYLOV + SMALLEST_REAL) ---
356  cudensitymatEigenDecomposition_t eigen = nullptr;
357  HANDLE_CUDM_ERROR(cudensitymatCreateEigenDecomposition(handle,
358                      hamiltonian,
359                      /*isHermitian=*/1,
360                      CUDENSITYMAT_EIGEN_SPECTRUM_SMALLEST_REAL,
361                      CUDENSITYMAT_EIGEN_SCOPE_SPLIT,
362                      CUDENSITYMAT_EIGEN_APPROACH_KRYLOV,
363                      &eigen));
364  if (verbose) std::cout << "Created EigenDecomposition object\n";
365
366  // --- 5. Configure DMRG attributes (NUM_SITES=2, SVD config, MAX_SWEEPS, ENERGY_TOLERANCE).
367  // Sub-config values are deep-copied at Configure, so the handles can be freed afterward.
368  cudensitymatEigenDecompositionScopeSplitDMRGConfig_t dmrgCfg = nullptr;
369  HANDLE_CUDM_ERROR(cudensitymatCreateEigenDecompositionScopeSplitDMRGConfig(handle, &dmrgCfg));
370  {
371    const int32_t dmrgNumSites = NUM_DMRG_SITES;
372    HANDLE_CUDM_ERROR(cudensitymatEigenDecompositionScopeSplitDMRGConfigSetAttribute(
373        handle, dmrgCfg,
374        CUDENSITYMAT_EIGEN_SPLIT_SCOPE_DMRG_NUM_SITES,
375        &dmrgNumSites, sizeof(dmrgNumSites)));
376
377    // SVD config: a modest MAX_EXTENT cap plus value cutoffs. The cap is below
378    // the buffer maximum, so the truncated current bond extents adapt below it.
379    cudensitymatSVDConfig_t svd = nullptr;
380    HANDLE_CUDM_ERROR(cudensitymatCreateSVDConfig(handle, &svd));
381    const int64_t maxExtent = SVD_MAX_EXTENT;
382    const double absCutoff = 1e-12, relCutoff = 1e-12;
383    HANDLE_CUDM_ERROR(cudensitymatSVDConfigSetAttribute(handle, svd,
384        CUDENSITYMAT_SVD_CONFIG_MAX_EXTENT, &maxExtent, sizeof(maxExtent)));
385    HANDLE_CUDM_ERROR(cudensitymatSVDConfigSetAttribute(handle, svd,
386        CUDENSITYMAT_SVD_CONFIG_ABS_CUTOFF, &absCutoff, sizeof(absCutoff)));
387    HANDLE_CUDM_ERROR(cudensitymatSVDConfigSetAttribute(handle, svd,
388        CUDENSITYMAT_SVD_CONFIG_REL_CUTOFF, &relCutoff, sizeof(relCutoff)));
389    HANDLE_CUDM_ERROR(cudensitymatEigenDecompositionScopeSplitDMRGConfigSetAttribute(
390        handle, dmrgCfg,
391        CUDENSITYMAT_EIGEN_SPLIT_SCOPE_DMRG_SVD_CONFIG, &svd, sizeof(svd)));
392    HANDLE_CUDM_ERROR(cudensitymatDestroySVDConfig(svd));   // safe once attached (deep-cloned)
393
394    const int32_t maxSweeps = MAX_SWEEPS;
395    HANDLE_CUDM_ERROR(cudensitymatEigenDecompositionScopeSplitDMRGConfigSetAttribute(
396        handle, dmrgCfg,
397        CUDENSITYMAT_EIGEN_SPLIT_SCOPE_DMRG_MAX_SWEEPS,
398        &maxSweeps, sizeof(maxSweeps)));
399    const double energyTol = ENERGY_TOL;
400    HANDLE_CUDM_ERROR(cudensitymatEigenDecompositionScopeSplitDMRGConfigSetAttribute(
401        handle, dmrgCfg,
402        CUDENSITYMAT_EIGEN_SPLIT_SCOPE_DMRG_ENERGY_TOLERANCE,
403        &energyTol, sizeof(energyTol)));
404  }
405  HANDLE_CUDM_ERROR(cudensitymatEigenDecompositionConfigure(handle, eigen,
406                      CUDENSITYMAT_EIGEN_SPLIT_SCOPE_DMRG_CONFIG,
407                      &dmrgCfg, sizeof(dmrgCfg)));
408  HANDLE_CUDM_ERROR(cudensitymatDestroyEigenDecompositionScopeSplitDMRGConfig(dmrgCfg));
409  dmrgCfg = nullptr;
410  if (verbose) std::cout << "Configured DMRG sub-config (num_sites=" << NUM_DMRG_SITES
411                         << ", svd_max_extent=" << SVD_MAX_EXTENT
412                         << ", max_sweeps=" << MAX_SWEEPS << ", energy_tol=" << ENERGY_TOL << ")\n";
413
414  // --- 5b. Configure the Krylov sub-config.
415  cudensitymatEigenDecompositionApproachKrylovConfig_t krylovCfg = nullptr;
416  HANDLE_CUDM_ERROR(cudensitymatCreateEigenDecompositionApproachKrylovConfig(handle, &krylovCfg));
417  {
418    const int32_t krylovMaxDim = KRYLOV_MAX_DIM;
419    HANDLE_CUDM_ERROR(cudensitymatEigenDecompositionApproachKrylovConfigSetAttribute(
420        handle, krylovCfg,
421        CUDENSITYMAT_EIGEN_APPROACH_KRYLOV_MAX_DIM,
422        &krylovMaxDim, sizeof(krylovMaxDim)));
423  }
424  HANDLE_CUDM_ERROR(cudensitymatEigenDecompositionConfigure(handle, eigen,
425                      CUDENSITYMAT_EIGEN_APPROACH_KRYLOV_CONFIG,
426                      &krylovCfg, sizeof(krylovCfg)));
427  HANDLE_CUDM_ERROR(cudensitymatDestroyEigenDecompositionApproachKrylovConfig(krylovCfg));
428  krylovCfg = nullptr;
429  if (verbose) std::cout << "Configured Krylov sub-config (max_dim=" << KRYLOV_MAX_DIM << ")\n";
430
431  // --- 6. Prepare + attach workspace ---
432  cudensitymatWorkspaceDescriptor_t workspaceDescr = nullptr;
433  HANDLE_CUDM_ERROR(cudensitymatCreateWorkspace(handle, &workspaceDescr));
434
435  std::size_t freeMem = 0, totalMem = 0;
436  HANDLE_CUDA_ERROR(cudaMemGetInfo(&freeMem, &totalMem));
437  freeMem = static_cast<std::size_t>(static_cast<double>(freeMem) * 0.85);
438  if (verbose) std::cout << "Available workspace memory (bytes) = " << freeMem << "\n";
439
440  HANDLE_CUDM_ERROR(cudensitymatEigenDecompositionPrepare(handle, eigen,
441                      /*maxEigenStates=*/1, mpsState, CUDENSITYMAT_COMPUTE_64F,
442                      freeMem, workspaceDescr, /*stream=*/0x0));
443
444  std::size_t scratchSize = 0;
445  HANDLE_CUDM_ERROR(cudensitymatWorkspaceGetMemorySize(handle, workspaceDescr,
446                      CUDENSITYMAT_MEMSPACE_DEVICE,
447                      CUDENSITYMAT_WORKSPACE_SCRATCH,
448                      &scratchSize));
449  void * scratchBuf = nullptr;
450  if (scratchSize > 0) {
451    HANDLE_CUDA_ERROR(cudaMalloc(&scratchBuf, scratchSize));
452    HANDLE_CUDM_ERROR(cudensitymatWorkspaceSetMemory(handle, workspaceDescr,
453                        CUDENSITYMAT_MEMSPACE_DEVICE,
454                        CUDENSITYMAT_WORKSPACE_SCRATCH,
455                        scratchBuf, scratchSize));
456  }
457  if (verbose) std::cout << "Prepared DMRG plan; scratch workspace = " << scratchSize << " bytes\n";
458
459  // --- 7. Compute the ground-state pair (eigenvalue, MPS) ---
460  void * eigenvalueGpu = createArrayGPU<Complex>(1);
461  double residual = RESIDUAL;
462  cudensitymatState_t eigenstatesArr[1] = { mpsState };
463
464  HANDLE_CUDA_ERROR(cudaDeviceSynchronize());
465  HANDLE_CUDM_ERROR(cudensitymatEigenDecompositionCompute(handle, eigen,
466                      /*time=*/0.0,
467                      /*batchSize=*/1,
468                      /*numParams=*/0, /*params=*/nullptr,
469                      /*numEigenStates=*/1, eigenstatesArr,
470                      eigenvalueGpu, &residual,
471                      workspaceDescr, /*stream=*/0x0));
472  HANDLE_CUDA_ERROR(cudaDeviceSynchronize());
473
474  cuDoubleComplex eigenvalueHost{0.0, 0.0};
475  HANDLE_CUDA_ERROR(cudaMemcpy(&eigenvalueHost, eigenvalueGpu,
476                               sizeof(cuDoubleComplex), cudaMemcpyDeviceToHost));
477
478  // Query the adapted (current) bond extents after Compute.
479  std::vector<int64_t> finalCurrentBonds(numBonds, 0);
480  HANDLE_CUDM_ERROR(cudensitymatStateMPSGetCurrentBondExtents(handle, mpsState,
481                      finalCurrentBonds.data()));
482
483  const double eDmrg     = eigenvalueHost.x;
484  const double energyErr = std::abs(eDmrg - E0_EXACT);
485  const bool   converged = (residual < RESIDUAL);
486
487  std::cout << "\n=================================================================\n"
488            << "DMRG ground-state energy : " << std::scientific << std::setprecision(12)
489            << eDmrg << "\n"
490            << "Exact (dense ED, N=" << 2 * NUM_SITES << " qubits) : " << E0_EXACT << "\n"
491            << "Energy error             : " << std::scientific << std::setprecision(3)
492            << energyErr << "\n"
493            << "Final residual           : " << residual << "\n";
494  std::cout << "Bond extents (current/max):";
495  for (int32_t b = 0; b < numBonds; ++b)
496    std::cout << " " << finalCurrentBonds[b] << "/" << mpsBondDims[b];
497  std::cout << "\n"
498            << "Result                   : " << (converged ? "PASS" : "FAIL") << "\n"
499            << "=================================================================\n\n";
500
501  // --- 8. Cleanup (reverse Create order) ---
502  destroyArrayGPU(eigenvalueGpu);
503  if (scratchBuf) HANDLE_CUDA_ERROR(cudaFree(scratchBuf));
504  HANDLE_CUDM_ERROR(cudensitymatDestroyWorkspace(workspaceDescr));
505  HANDLE_CUDM_ERROR(cudensitymatDestroyEigenDecomposition(eigen));
506  HANDLE_CUDM_ERROR(cudensitymatDestroyState(mpsState));
507  HANDLE_CUDM_ERROR(cudensitymatDestroyOperator(hamiltonian));
508  HANDLE_CUDM_ERROR(cudensitymatDestroyOperatorTerm(operatorTerm));
509  HANDLE_CUDM_ERROR(cudensitymatDestroyMatrixProductOperator(mpoHandle));
510  mps.destroy();
511  mpo.destroy();
512
513  if (verbose) std::cout << "Destroyed all resources\n";
514}
515
516
517int main(int /*argc*/, char ** /*argv*/)
518{
519  HANDLE_CUDA_ERROR(cudaSetDevice(0));
520  if (verbose) std::cout << "Set active CUDA device 0\n";
521
522  cudensitymatHandle_t handle;
523  HANDLE_CUDM_ERROR(cudensitymatCreate(&handle));
524  if (verbose) std::cout << "Created cuDensityMat library handle\n";
525
526  exampleWorkflow(handle);
527
528  HANDLE_CUDM_ERROR(cudensitymatDestroy(handle));
529  if (verbose) std::cout << "Destroyed cuDensityMat library handle\n";
530
531  HANDLE_CUDA_ERROR(cudaDeviceReset());
532  return 0;
533}

Useful tips#

  • For debugging, one can set the environment variable CUDENSITYMAT_LOG_LEVEL=n. The level n = 0, 1, …, 5 corresponds to the logger level as described in the table below. The environment variable CUDENSITYMAT_LOG_FILE=<filepath> can be used to redirect the log output to a custom file at <filepath> instead of stdout.

Level

Summary

Long Description

0

Off

Logging is disabled (default)

1

Errors

Only errors will be logged

2

Performance Trace

API calls that launch CUDA kernels will log their parameters and important information

3

Performance Hints

Hints that can potentially improve the application’s performance

4

Heuristics Trace

Provides general information about the library execution, may contain details about heuristic status

5

API Trace

API calls will log their parameter and important information