diff --git a/docs/source/Support/bskReleaseNotes.rst b/docs/source/Support/bskReleaseNotes.rst index b61e3ca288..164a0daa5d 100644 --- a/docs/source/Support/bskReleaseNotes.rst +++ b/docs/source/Support/bskReleaseNotes.rst @@ -34,6 +34,8 @@ Version |release| - Refactored the CI build system scripts - Removed deprecated use of ``Basilisk.simulation.planetEphemeris.ClassicElementsMsgPayload``. Users need to use ``ClassicalElements()`` defined in ``orbitalMotion``. +- Fixed bug with recording message payload entries that are 2D arrays. This bug was introduced with the faster recording + strategy added in version 2.78.0. Version 2.78.0 (August 30, 2025) diff --git a/src/architecture/messaging/msgAutoSource/generateSWIGModules.py b/src/architecture/messaging/msgAutoSource/generateSWIGModules.py index 6cbfb0b764..cfdcfdbe08 100644 --- a/src/architecture/messaging/msgAutoSource/generateSWIGModules.py +++ b/src/architecture/messaging/msgAutoSource/generateSWIGModules.py @@ -308,8 +308,7 @@ def parseSwigXml(xmlPath: str, targetStructName: str, cpp: bool, recorderPropert if typeWithPointerRef in C_TYPE_TO_NPY_ENUM and len(typeArrayParts) < 3: npyType = C_TYPE_TO_NPY_ENUM[typeWithPointerRef] macroName = f"RECORDER_PROPERTY_NUMERIC_{len(typeArrayParts)}" - dimensions = "".join(f", {dim}" for dim in typeArrayParts) - result += f"{macroName}({targetStructName}, {fieldName}, {typeWithPointerRef}, {npyType} {dimensions});\n" + result += f"{macroName}({targetStructName}, {fieldName}, {typeWithPointerRef}, {npyType});\n" elif not recorderPropertyRollback: fullType = f"{typeWithPointerRef}{''.join(f'[{i}]' for i in typeArrayParts)}" result += f"RECORDER_PROPERTY({targetStructName}, {fieldName}, ({fullType}));\n" diff --git a/src/architecture/messaging/newMessaging.ih b/src/architecture/messaging/newMessaging.ih index c26eb23c14..1142818c93 100644 --- a/src/architecture/messaging/newMessaging.ih +++ b/src/architecture/messaging/newMessaging.ih @@ -333,9 +333,11 @@ typedef struct messageType; } else { npy_intp dims[1] = {static_cast(N)}; $result = PyArray_SimpleNew(1, dims, npyTypeNum); - for (size_t recordIndex = 0; recordIndex < N; ++recordIndex ) { - void* ptr = PyArray_GETPTR1((PyArrayObject*)$result, recordIndex); - *reinterpret_cast(ptr) = $1->record().at(recordIndex). ## fieldName; + if (!$result) SWIG_fail; + + for (size_t i = 0; i < N; ++i ) { + void* ptr = PyArray_GETPTR1((PyArrayObject*)$result, i); + *reinterpret_cast(ptr) = $1->record().at(i). ## fieldName; } } if (PyErr_Occurred()) SWIG_fail; @@ -355,11 +357,7 @@ typedef struct messageType; %enddef -%define RECORDER_PROPERTY_NUMERIC_1(payloadType, fieldName, fieldType, npyTypeNum, dim1) -/* - * Exposes a fixed-size 1D numeric array field (e.g. float[3]) as a NumPy array with shape [N, dim1]. - * Uses contiguous memcpy per record for efficiency. - */ +%define RECORDER_PROPERTY_NUMERIC_1(payloadType, fieldName, fieldType, npyTypeNum) %typemap(out) Recorder* Recorder::_ ## fieldName ## _array { size_t N = $1->record().size(); @@ -367,15 +365,21 @@ typedef struct messageType; npy_intp dims[1] = {0}; $result = PyArray_SimpleNew(1, dims, NPY_FLOAT64); } else { - npy_intp dims[2] = {static_cast(N), (npy_intp) dim1}; + /* Derive dimensions from compiled type */ + auto &first = $1->record().at(0).fieldName; + const size_t d1 = sizeof(first) / sizeof(first[0]); + const size_t elemsPerRecord = d1; + const size_t bytesPerRecord = elemsPerRecord * sizeof(fieldType); + + npy_intp dims[2] = {static_cast(N), (npy_intp) d1}; $result = PyArray_SimpleNew(2, dims, npyTypeNum); + if (!$result) SWIG_fail; fieldType* dest = static_cast(PyArray_DATA((PyArrayObject*)$result)); - size_t recordStride = dim1; - for (size_t recordIndex = 0; recordIndex < N; ++recordIndex) { - const fieldType* src = &($1->record().at(recordIndex).fieldName[0]); - std::memcpy(dest + recordIndex * recordStride, src, recordStride * sizeof(fieldType)); + for (size_t i = 0; i < N; ++i) { + const fieldType* src = &($1->record().at(i).fieldName[0]); + std::memcpy(dest + i * elemsPerRecord, src, bytesPerRecord); } } if (PyErr_Occurred()) SWIG_fail; @@ -395,10 +399,7 @@ typedef struct messageType; %enddef -%define RECORDER_PROPERTY_NUMERIC_2(payloadType, fieldName, fieldType, npyTypeNum, dim1, dim2) -/* - * Exposes a fixed-size 2D numeric array field (e.g. float[3][3]) as a NumPy array with shape [N, dim1, dim2]. - */ +%define RECORDER_PROPERTY_NUMERIC_2(payloadType, fieldName, fieldType, npyTypeNum) %typemap(out) Recorder* Recorder::_ ## fieldName ## _array { size_t N = $1->record().size(); @@ -406,18 +407,26 @@ typedef struct messageType; npy_intp dims[1] = {0}; $result = PyArray_SimpleNew(1, dims, NPY_FLOAT64); } else { - npy_intp dims[3] = {static_cast(N), (npy_intp) dim1, (npy_intp) dim2}; + /* Derive dimensions from compiled type */ + auto &first = $1->record().at(0).fieldName; + const size_t d1 = sizeof(first) / sizeof(first[0]); + const size_t d2 = sizeof(first[0]) / sizeof(first[0][0]); + const size_t elemsPerRecord = d1 * d2; + const size_t bytesPerRecord = elemsPerRecord * sizeof(fieldType); + + npy_intp dims[3] = {static_cast(N), (npy_intp) d1, (npy_intp) d2}; $result = PyArray_SimpleNew(3, dims, npyTypeNum); + if (!$result) SWIG_fail; fieldType* dest = static_cast(PyArray_DATA((PyArrayObject*)$result)); - size_t recordStride = dim1 * dim2; - for (size_t recordIndex = 0; recordIndex < N; ++recordIndex) { - const fieldType* src = &($1->record().at(recordIndex).fieldName[0][0]); - std::memcpy(dest + recordIndex * recordStride, src, recordStride * sizeof(fieldType)); + for (size_t i = 0; i < N; ++i) { + const fieldType* src = &($1->record().at(i).fieldName[0][0]); + std::memcpy(dest + i * elemsPerRecord, src, bytesPerRecord); } + + if (PyErr_Occurred()) SWIG_fail; } - if (PyErr_Occurred()) SWIG_fail; } %extend Recorder {