#
# 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