Skip to content

Commit 0346774

Browse files
authored
fix: To fix Python QEC conversion for empty repetition-code X matrices (#660)
This PR tries to fix [[B] 6428084](https://nvbugspro.nvidia.com/bug/6428084) . It also adds one test to expose this bug and verify the fix. Thanks for looking this PR. In our suggested fix, we keep the existing C++ `cudaqx::tensor` semantics unchanged: an empty directional parity/observable tensor may remain rank 0. In the Python binding layer, we add a small wrapper for matrix-returning `Code` methods that detects this valid rank-0 empty sentinel and converts it to an empty 2-D NumPy matrix with the correct width, e.g. `(0, code.get_num_data_qubits())`. It also adds rank validation in the generic tensor-to-NumPy helpers before reading `shape[0]` / `shape[1]` and skips `memcpy` for zero-sized tensors, so invalid-rank conversions fail cleanly instead of causing undefined behaviour. Signed-off-by: Kaiqi Yan <kaiqiy@nvidia.com>
1 parent 511d204 commit 0346774

3 files changed

Lines changed: 62 additions & 13 deletions

File tree

libs/qec/python/bindings/py_code.cpp

Lines changed: 27 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -227,6 +227,21 @@ static nb::object get_python_kernel_or_throw(const code &self, operation op) {
227227

228228
return it->second;
229229
}
230+
231+
template <typename T>
232+
static auto copyCodeMatrixToPyArray(const cudaqx::tensor<T> &tensor,
233+
std::size_t emptyCols) {
234+
if (tensor.rank() == 0 && tensor.size() == 0) {
235+
T *data_copy = new T[0];
236+
size_t arr_shape[] = {0, emptyCols};
237+
return nb::ndarray<nb::numpy, T>(
238+
data_copy, 2, arr_shape, nb::capsule(data_copy, [](void *p) noexcept {
239+
delete[] static_cast<T *>(p);
240+
}));
241+
}
242+
243+
return cudaq::python::copyCUDAQXTensorToPyArray(tensor);
244+
}
230245
} // namespace
231246

232247
// Registry to store code factory functions
@@ -364,42 +379,43 @@ void bindCode(nb::module_ &mod) {
364379
.def(
365380
"get_parity",
366381
[](code &code) {
367-
return cudaq::python::copyCUDAQXTensorToPyArray(code.get_parity());
382+
return copyCodeMatrixToPyArray(code.get_parity(),
383+
2 * code.get_num_data_qubits());
368384
},
369385
"Get the parity check matrix of the code")
370386
.def(
371387
"get_parity_x",
372388
[](code &code) {
373-
return cudaq::python::copyCUDAQXTensorToPyArray(
374-
code.get_parity_x());
389+
return copyCodeMatrixToPyArray(code.get_parity_x(),
390+
code.get_num_data_qubits());
375391
},
376392
"Get the X-type parity check matrix of the code")
377393
.def(
378394
"get_parity_z",
379395
[](code &code) {
380-
return cudaq::python::copyCUDAQXTensorToPyArray(
381-
code.get_parity_z());
396+
return copyCodeMatrixToPyArray(code.get_parity_z(),
397+
code.get_num_data_qubits());
382398
},
383399
"Get the Z-type parity check matrix of the code")
384400
.def(
385401
"get_pauli_observables_matrix",
386402
[](code &code) {
387-
return cudaq::python::copyCUDAQXTensorToPyArray(
388-
code.get_pauli_observables_matrix());
403+
return copyCodeMatrixToPyArray(code.get_pauli_observables_matrix(),
404+
2 * code.get_num_data_qubits());
389405
},
390406
"Get a matrix of the Pauli observables of the code")
391407
.def(
392408
"get_observables_x",
393409
[](code &code) {
394-
return cudaq::python::copyCUDAQXTensorToPyArray(
395-
code.get_observables_x());
410+
return copyCodeMatrixToPyArray(code.get_observables_x(),
411+
code.get_num_data_qubits());
396412
},
397413
"Get the Pauli X observables of the code")
398414
.def(
399415
"get_observables_z",
400416
[](code &code) {
401-
return cudaq::python::copyCUDAQXTensorToPyArray(
402-
code.get_observables_z());
417+
return copyCodeMatrixToPyArray(code.get_observables_z(),
418+
code.get_num_data_qubits());
403419
},
404420
"Get the Pauli Z observables of the code")
405421
.def("get_stabilizers", &code::get_stabilizers,

libs/qec/python/bindings/type_casters.h

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
#pragma once
99

1010
#include <cstring>
11+
#include <stdexcept>
1112

1213
#include "common/ObserveResult.h"
1314
#include "cuda-qx/core/heterogeneous_map.h"
@@ -256,13 +257,18 @@ namespace python {
256257
template <typename T>
257258
auto copyCUDAQXTensorToPyArray(const cudaqx::tensor<T> &tensor) {
258259
auto shape = tensor.shape();
260+
if (shape.size() != 2)
261+
throw std::runtime_error(
262+
"Expected a rank-2 cudaqx::tensor for NumPy conversion.");
263+
259264
auto rows = shape[0];
260265
auto cols = shape[1];
261266
size_t total_size = rows * cols;
262267

263268
// Allocate new memory and copy the data
264269
T *data_copy = new T[total_size];
265-
std::memcpy(data_copy, tensor.data(), total_size * sizeof(T));
270+
if (total_size > 0)
271+
std::memcpy(data_copy, tensor.data(), total_size * sizeof(T));
266272

267273
size_t arr_shape[] = {rows, cols};
268274
return nb::ndarray<nb::numpy, T>(data_copy, 2, arr_shape,
@@ -274,12 +280,17 @@ auto copyCUDAQXTensorToPyArray(const cudaqx::tensor<T> &tensor) {
274280
template <typename T>
275281
auto copy1DCUDAQXTensorToPyArray(const cudaqx::tensor<T> &tensor) {
276282
auto shape = tensor.shape();
283+
if (shape.size() != 1)
284+
throw std::runtime_error(
285+
"Expected a rank-1 cudaqx::tensor for NumPy conversion.");
286+
277287
auto rows = shape[0];
278288
size_t total_size = rows;
279289

280290
// Allocate new memory and copy the data
281291
T *data_copy = new T[total_size];
282-
std::memcpy(data_copy, tensor.data(), total_size * sizeof(T));
292+
if (total_size > 0)
293+
std::memcpy(data_copy, tensor.data(), total_size * sizeof(T));
283294

284295
size_t arr_shape[] = {rows};
285296
return nb::ndarray<nb::numpy, T>(data_copy, 1, arr_shape,

libs/qec/python/tests/test_code.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,28 @@ def test_code_parity_matrices():
3939
assert parity_z.shape == (3, 7)
4040

4141

42+
def test_repetition_empty_x_matrices_preserve_rank():
43+
repetition = qec.get_code("repetition", distance=3)
44+
45+
# Repetition is Z-only; empty X-side results must still be matrices with
46+
# one column per data qubit so Python callers can inspect their shape.
47+
parity_x = repetition.get_parity_x()
48+
assert isinstance(parity_x, np.ndarray)
49+
assert parity_x.dtype == np.uint8
50+
assert parity_x.shape == (0, 3)
51+
52+
observables_x = repetition.get_observables_x()
53+
assert isinstance(observables_x, np.ndarray)
54+
assert observables_x.dtype == np.uint8
55+
assert observables_x.shape == (0, 3)
56+
57+
parity_z = repetition.get_parity_z()
58+
assert parity_z.shape == (2, 3)
59+
60+
observables_z = repetition.get_observables_z()
61+
assert observables_z.shape == (1, 3)
62+
63+
4264
def test_code_stabilizers():
4365
steane = qec.get_code("steane")
4466
stabilizers = steane.get_stabilizers()

0 commit comments

Comments
 (0)