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 leveln= 0, 1, …, 5 corresponds to the logger level as described in the table below. The environment variableCUDENSITYMAT_LOG_FILE=<filepath>can be used to redirect the log output to a custom file at<filepath>instead ofstdout.
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 |