Source code for test_msgDtype

#
#  ISC License
#
#  Copyright (c) 2026, Autonomous Vehicle Systems Lab, University of Colorado at Boulder
#
#  Permission to use, copy, modify, and/or distribute this software for any
#  purpose with or without fee is hereby granted, provided that the above
#  copyright notice and this permission notice appear in all copies.
#
#  THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
#  WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
#  MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
#  ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
#  WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
#  ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
#  OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
#
"""
Tests for PayloadType.__dtype__ - numpy structured dtype mirroring the C struct layout.

Each test:
  1. Creates a payload object.
  2. Gets a writable numpy view of the underlying C memory via sim_model.getObjectAddress.
  3. Writes values through numpy and asserts the Python wrapper sees them (numpy → Python).
  4. Writes values through the Python wrapper and asserts numpy sees them (Python → numpy).

Coverage:
  - Primitive scalars with implicit padding   (EpochMsgPayload)
  - 1-D float arrays                          (AttStateMsgPayload)
  - 2-D float array                           (SCMassPropsMsgPayload)
  - Enum field                                (DeviceStatusMsgPayload)
  - Char array + 2-D array + scalar           (SpicePlanetStateMsgPayload)
  - Array of nested structs                   (AccDataMsgPayload)
  - Pointer field → uint64 in dtype           (CameraImageMsgPayload)
  - Incomplete struct (C++ vectors) → None    (JointArrayStateMsgPayload)
"""

import ctypes

import numpy as np
import pytest

from Basilisk.architecture import messaging
from Basilisk.architecture import sim_model


def _writable_view(payload, dtype):
    """Return a writable 1-element structured numpy array backed by the payload's C memory."""
    addr = sim_model.getObjectAddress(payload)
    buf  = (ctypes.c_byte * dtype.itemsize).from_address(addr)
    return np.ndarray(1, dtype=dtype, buffer=buf)


# ---------------------------------------------------------------------------
# 1. Primitive scalars + implicit padding  (EpochMsgPayload)
#    struct: int year, month, day, hours, minutes; double seconds;
#    layout: 5×int32 (20 bytes) + 4-byte pad + float64 = 32 bytes total
# ---------------------------------------------------------------------------

[docs] def test_epochDtypeLayout(): """Check the Epoch payload dtype layout and padding offsets.""" dt = messaging.EpochMsgPayload.__dtype__ assert dt is not None assert dt.itemsize == 32 assert dt.fields['year'][1] == 0 assert dt.fields['month'][1] == 4 assert dt.fields['day'][1] == 8 assert dt.fields['hours'][1] == 12 assert dt.fields['minutes'][1] == 16 assert dt.fields['seconds'][1] == 24 # 4-byte padding gap at bytes 20-23
[docs] def test_epochDtypeNumpyToPython(): """Check numpy writes into Epoch payload memory update Python fields.""" p = messaging.EpochMsgPayload() arr = _writable_view(p, messaging.EpochMsgPayload.__dtype__) arr[0]['year'] = 2024 arr[0]['month'] = 3 arr[0]['day'] = 15 arr[0]['hours'] = 12 arr[0]['minutes'] = 30 arr[0]['seconds'] = 45.5 assert p.year == 2024 assert p.month == 3 assert p.day == 15 assert p.hours == 12 assert p.minutes == 30 assert p.seconds == pytest.approx(45.5)
[docs] def test_epochDtypePythonToNumpy(): """Check Python writes to Epoch fields are visible through the dtype view.""" p = messaging.EpochMsgPayload() arr = _writable_view(p, messaging.EpochMsgPayload.__dtype__) p.year = 1999 p.seconds = 0.125 assert arr[0]['year'] == 1999 assert arr[0]['seconds'] == pytest.approx(0.125)
# --------------------------------------------------------------------------- # 2. 1-D float arrays (AttStateMsgPayload) # struct: double state[3], rate[3]; - 48 bytes, no padding # ---------------------------------------------------------------------------
[docs] def test_attStateDtypeLayout(): """Check the attitude-state payload dtype layout and vector shapes.""" dt = messaging.AttStateMsgPayload.__dtype__ assert dt is not None assert dt.itemsize == 48 assert dt.fields['state'][1] == 0 assert dt.fields['rate'][1] == 24 assert dt['state'].shape == (3,) assert dt['rate'].shape == (3,)
[docs] def test_attStateDtypeNumpyToPython(): """Check numpy writes into attitude-state memory update Python fields.""" p = messaging.AttStateMsgPayload() arr = _writable_view(p, messaging.AttStateMsgPayload.__dtype__) arr[0]['state'] = [0.1, 0.2, 0.3] arr[0]['rate'] = [1.0, 2.0, 3.0] np.testing.assert_allclose(p.state, [0.1, 0.2, 0.3]) np.testing.assert_allclose(p.rate, [1.0, 2.0, 3.0])
[docs] def test_attStateDtypePythonToNumpy(): """Check Python attitude-state writes are visible through the dtype view.""" p = messaging.AttStateMsgPayload() arr = _writable_view(p, messaging.AttStateMsgPayload.__dtype__) p.state = [9.0, 8.0, 7.0] p.rate = [-1.0, -2.0, -3.0] np.testing.assert_allclose(arr[0]['state'], [9.0, 8.0, 7.0]) np.testing.assert_allclose(arr[0]['rate'], [-1.0, -2.0, -3.0])
# --------------------------------------------------------------------------- # 3. 2-D float array (SCMassPropsMsgPayload) # struct: double massSC; double c_B[3]; double ISC_PntB_B[3][3]; - 104 bytes # ---------------------------------------------------------------------------
[docs] def test_scMassPropsDtypeLayout(): """Check spacecraft mass-properties dtype offsets and matrix shape.""" dt = messaging.SCMassPropsMsgPayload.__dtype__ assert dt is not None assert dt.itemsize == 104 assert dt.fields['massSC'][1] == 0 assert dt.fields['c_B'][1] == 8 assert dt.fields['ISC_PntB_B'][1] == 32 assert dt['ISC_PntB_B'].shape == (3, 3)
[docs] def test_scMassPropsDtypeNumpyToPython(): """Check numpy writes into mass-properties memory update Python fields.""" p = messaging.SCMassPropsMsgPayload() arr = _writable_view(p, messaging.SCMassPropsMsgPayload.__dtype__) inertia = np.array([[1., 0., 0.], [0., 2., 0.], [0., 0., 3.]]) arr[0]['massSC'] = 500.0 arr[0]['c_B'] = [0.1, 0.2, 0.3] arr[0]['ISC_PntB_B'] = inertia assert p.massSC == pytest.approx(500.0) np.testing.assert_allclose(list(p.c_B), [0.1, 0.2, 0.3]) np.testing.assert_allclose(list(p.ISC_PntB_B), inertia.tolist())
[docs] def test_scMassPropsDtypePythonToNumpy(): """Check Python mass-property writes are visible through the dtype view.""" p = messaging.SCMassPropsMsgPayload() arr = _writable_view(p, messaging.SCMassPropsMsgPayload.__dtype__) p.massSC = 750.0 assert arr[0]['massSC'] == pytest.approx(750.0)
# --------------------------------------------------------------------------- # 4. Enum field (DeviceStatusMsgPayload) # struct: deviceState deviceStatus; - 4 bytes # ---------------------------------------------------------------------------
[docs] def test_deviceStatusDtypeLayout(): """Check the device-status enum dtype layout.""" dt = messaging.DeviceStatusMsgPayload.__dtype__ assert dt is not None assert dt.itemsize == 4 assert dt.fields['deviceStatus'][0] == np.dtype('int32') assert dt.fields['deviceStatus'][1] == 0
[docs] def test_deviceStatusDtypeNumpyToPython(): """Check numpy writes into device-status memory update Python fields.""" p = messaging.DeviceStatusMsgPayload() arr = _writable_view(p, messaging.DeviceStatusMsgPayload.__dtype__) arr[0]['deviceStatus'] = 1 # On assert p.deviceStatus == 1
[docs] def test_deviceStatusDtypePythonToNumpy(): """Check Python device-status writes are visible through the dtype view.""" p = messaging.DeviceStatusMsgPayload() arr = _writable_view(p, messaging.DeviceStatusMsgPayload.__dtype__) p.deviceStatus = 0 # Off assert arr[0]['deviceStatus'] == 0
# --------------------------------------------------------------------------- # 5. Char array + 2-D array + scalar (SpicePlanetStateMsgPayload) # struct: double J2000Current; double PositionVector[3]; double VelocityVector[3]; # double J20002Pfix[3][3]; double J20002Pfix_dot[3][3]; # int computeOrient; char PlanetName[64]; - 272 bytes # ---------------------------------------------------------------------------
[docs] def test_spicePlanetDtypeLayout(): """Check the SPICE planet-state dtype offsets and string field type.""" dt = messaging.SpicePlanetStateMsgPayload.__dtype__ assert dt is not None assert dt.itemsize == 272 assert dt['PlanetName'] == np.dtype('S64') assert dt.fields['J2000Current'][1] == 0 assert dt.fields['PlanetName'][1] == 204
[docs] def test_spicePlanetDtypeNumpyToPython(): """Check numpy writes into SPICE planet-state memory update Python fields.""" p = messaging.SpicePlanetStateMsgPayload() arr = _writable_view(p, messaging.SpicePlanetStateMsgPayload.__dtype__) arr[0]['J2000Current'] = 1234.5 arr[0]['PositionVector'] = [1e6, 2e6, 3e6] arr[0]['PlanetName'] = b'Earth' assert p.J2000Current == pytest.approx(1234.5) np.testing.assert_allclose(list(p.PositionVector), [1e6, 2e6, 3e6]) # SWIG exposes char[] as a Python string assert p.PlanetName[:5] == 'Earth'
[docs] def test_spicePlanetDtypePythonToNumpy(): """Check Python SPICE planet-state writes are visible through the dtype view.""" p = messaging.SpicePlanetStateMsgPayload() arr = _writable_view(p, messaging.SpicePlanetStateMsgPayload.__dtype__) p.J2000Current = 9999.0 assert arr[0]['J2000Current'] == pytest.approx(9999.0)
# --------------------------------------------------------------------------- # 6. Array of nested structs (AccDataMsgPayload) # struct: AccPktDataMsgPayload accPkts[120]; # each AccPktDataMsgPayload: uint64 measTime + double gyro_B[3] + double accel_B[3] = 56 bytes # total: 6720 bytes # ---------------------------------------------------------------------------
[docs] def test_accDataDtypeLayout(): """Check the accelerometer-packet nested dtype layout.""" dt = messaging.AccDataMsgPayload.__dtype__ assert dt is not None assert dt.itemsize == 6720 # accPkts field: array of 120 nested structs pkt_dt = dt['accPkts'].base assert pkt_dt.itemsize == 56 assert pkt_dt.fields['measTime'][1] == 0 assert pkt_dt.fields['gyro_B'][1] == 8 assert pkt_dt.fields['accel_B'][1] == 32
[docs] def test_accDataDtypeNumpyToPython(): """Check numpy writes into accelerometer-packet memory update Python fields.""" p = messaging.AccDataMsgPayload() arr = _writable_view(p, messaging.AccDataMsgPayload.__dtype__) arr[0]['accPkts'][0]['measTime'] = 999 arr[0]['accPkts'][0]['gyro_B'] = [0.1, 0.2, 0.3] assert p.accPkts[0].measTime == 999 np.testing.assert_allclose(list(p.accPkts[0].gyro_B), [0.1, 0.2, 0.3])
[docs] def test_accDataDtypePythonToNumpy(): """Check Python accelerometer-packet writes are visible through the dtype view.""" p = messaging.AccDataMsgPayload() arr = _writable_view(p, messaging.AccDataMsgPayload.__dtype__) p.accPkts[5].measTime = 42 assert arr[0]['accPkts'][5]['measTime'] == 42
# --------------------------------------------------------------------------- # 7. Struct with pointer field → pointer is uint64 in dtype (CameraImageMsgPayload) # struct: uint64 timeTag; int valid; int64 cameraID; void* imagePointer; # int32 imageBufferLength; int8 imageType; - 40 bytes # ---------------------------------------------------------------------------
[docs] def test_cameraImageDtypeLayout(): """Check the camera-image payload dtype layout and pointer field type.""" dt = messaging.CameraImageMsgPayload.__dtype__ assert dt is not None assert dt.itemsize == 40 assert dt['imagePointer'] == np.dtype('uint64') assert dt.fields['timeTag'][1] == 0 assert dt.fields['imagePointer'][1] == 24 assert dt.fields['imageBufferLength'][1] == 32
[docs] def test_cameraImageDtypeNumpyToPython(): """Check numpy writes into camera-image memory update Python fields.""" p = messaging.CameraImageMsgPayload() arr = _writable_view(p, messaging.CameraImageMsgPayload.__dtype__) arr[0]['timeTag'] = 123456789 arr[0]['valid'] = 1 arr[0]['imageBufferLength'] = 512 assert p.timeTag == 123456789 assert p.valid == 1 assert p.imageBufferLength == 512
[docs] def test_cameraImageDtypePythonToNumpy(): """Check Python camera-image writes are visible through the dtype view.""" p = messaging.CameraImageMsgPayload() arr = _writable_view(p, messaging.CameraImageMsgPayload.__dtype__) p.timeTag = 987654321 assert arr[0]['timeTag'] == 987654321
# --------------------------------------------------------------------------- # 8. Incomplete struct (C++ std::vector fields) → __dtype__ is None # ---------------------------------------------------------------------------
[docs] def test_jointArrayStateDtypeIsNone(): """Check payloads with unsupported vector fields expose no numpy dtype.""" assert messaging.JointArrayStateMsgPayload.__dtype__ is None