Skip to content

Commit a75d2e1

Browse files
committed
Add new hingedJointArray unit test
1 parent 9cc93b1 commit a75d2e1

File tree

1 file changed

+260
-0
lines changed

1 file changed

+260
-0
lines changed
Lines changed: 260 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,260 @@
1+
#
2+
# ISC License
3+
#
4+
# Copyright (c) 2025, Autonomous Vehicle Systems Lab, University of Colorado Boulder
5+
#
6+
# Permission to use, copy, modify, and/or distribute this software for any
7+
# purpose with or without fee is hereby granted, provided that the above
8+
# copyright notice and this permission notice appear in all copies.
9+
#
10+
# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
11+
# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
12+
# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
13+
# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
14+
# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
15+
# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
16+
# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
17+
#
18+
#
19+
20+
import numpy as np
21+
import pytest
22+
23+
from Basilisk.utilities import SimulationBaseClass, macros
24+
from Basilisk.architecture import messaging
25+
from Basilisk.fswAlgorithms import hingedJointArrayMotor
26+
27+
def jointTorques(M, K, P, theta, thetaDot, desTheta, desThetaDot, bias, maxTorque=None):
28+
M = np.asarray(M, dtype=float)
29+
K = np.asarray(K, dtype=float)
30+
P = np.asarray(P, dtype=float)
31+
theta = np.asarray(theta, dtype=float).reshape(-1)
32+
thetaDot = np.asarray(thetaDot, dtype=float).reshape(-1)
33+
desTheta = np.asarray(desTheta, dtype=float).reshape(-1)
34+
desThetaDot= np.asarray(desThetaDot, dtype=float).reshape(-1)
35+
bias = np.asarray(bias, dtype=float).reshape(-1)
36+
37+
nj = theta.size
38+
e = theta - desTheta
39+
edot = thetaDot - desThetaDot
40+
theta_ddot_des = -(K @ e) - (P @ edot)
41+
42+
Mtt = M[0:3,0:3]
43+
Mtth = M[0:3,6:6+nj]
44+
Mtht = M[6:6+nj,0:3]
45+
Mthth = M[6:6+nj,6:6+nj]
46+
47+
transBias = bias[0:3]
48+
jointBias = bias[6:6+nj]
49+
50+
rhs = -Mtth @ theta_ddot_des + transBias
51+
52+
x = np.linalg.solve(Mtt, rhs)
53+
54+
u_H = Mthth @ theta_ddot_des + Mtht @ x - jointBias
55+
56+
if maxTorque is not None:
57+
for i in range(nj):
58+
if u_H[i] > maxTorque[i]:
59+
u_H[i] = maxTorque[i]
60+
elif u_H[i] < -maxTorque[i]:
61+
u_H[i] = -maxTorque[i]
62+
63+
return u_H
64+
65+
@pytest.mark.parametrize("nonActForces", [False, True])
66+
@pytest.mark.parametrize("maxTorque", [False, True])
67+
@pytest.mark.parametrize("numJoints", [1,4])
68+
@pytest.mark.parametrize("numSpacecraft", [1, 2])
69+
70+
def test_hingedJointArrayMotor(nonActForces, maxTorque, numJoints, numSpacecraft):
71+
r"""
72+
**Validation Test Description**
73+
74+
This unit test validates the motor torques found by the joint array controller for spacecraft with attached jointed arms.
75+
76+
**Test Parameters**
77+
78+
The presence of non actuator based forces, presence of max torque limits, number of joints, and number of spacecraft can be varied.
79+
80+
Args:
81+
nonActForces (bool): Flag to indicate presence of non-actuator forces
82+
maxTorque (bool): Flag to indicate presence of max torque limits
83+
numJoints (int): Number of joints in the arm
84+
numSpacecraft (int): Number of spacecraft in the simulation
85+
86+
**Description of Variables Being Tested**
87+
88+
In this test the motor torques output by the module are compared to the expected torques calculated in python.
89+
"""
90+
unitTaskName = "unitTask"
91+
unitProcessName = "TestProcess"
92+
93+
unitTestSim = SimulationBaseClass.SimBaseClass()
94+
testProcessRate = macros.sec2nano(0.1)
95+
testProc = unitTestSim.CreateNewProcess(unitProcessName)
96+
testProc.addTask(unitTestSim.CreateNewTask(unitTaskName, testProcessRate))
97+
98+
# setup module to be tested
99+
module = hingedJointArrayMotor.HingedJointArrayMotor()
100+
module.ModelTag = "hingedJointArrayMotorTag"
101+
Ktheta = 10.0 * np.eye(numJoints * numSpacecraft)
102+
Ptheta = 2.0 * np.sqrt(10.0) * np.eye(numJoints * numSpacecraft)
103+
module.setKtheta(Ktheta.flatten().tolist())
104+
module.setPtheta(Ptheta.flatten().tolist())
105+
if maxTorque:
106+
uMax = [0.03] * (numJoints * numSpacecraft)
107+
module.setUMax(uMax)
108+
for sc in range(numSpacecraft):
109+
for j in range(numJoints):
110+
module.addHingedJoint()
111+
unitTestSim.AddModelToTask(unitTaskName, module)
112+
113+
# create mass matrix input message
114+
M = np.eye((6 + numJoints) * numSpacecraft)
115+
massMatrixInMsgData = messaging.MJSysMassMatrixMsgPayload()
116+
massMatrixInMsgData.massMatrix.clear()
117+
for v in M.flatten():
118+
massMatrixInMsgData.massMatrix.push_back(v)
119+
120+
# create the joint states input messages vectors
121+
if numJoints == 1:
122+
theta = [0.1]
123+
thetaDot = [0.5]
124+
elif numJoints == 4:
125+
theta = [0.1, -0.5, 0.3, 0.7]
126+
thetaDot = [0.5, -0.2, 0.1, -0.3]
127+
jointStatesInMsgDataList = []
128+
jointStateDotsInMsgDataList = []
129+
for sc in range(numSpacecraft):
130+
for j in range(numJoints):
131+
jointStatesInMsgData = messaging.ScalarJointStateMsgPayload()
132+
jointStatesInMsgData.state = theta[j]
133+
jointStatesInMsgDataList.append(jointStatesInMsgData)
134+
135+
jointStateDotsInMsgData = messaging.ScalarJointStateMsgPayload()
136+
jointStateDotsInMsgData.state = thetaDot[j]
137+
jointStateDotsInMsgDataList.append(jointStateDotsInMsgData)
138+
139+
# create the desired joint state input message
140+
desJointStateInMsgData = messaging.JointArrayStateMsgPayload()
141+
desTheta = [0.12] * (numJoints * numSpacecraft)
142+
desThetaDot = [0.05] * (numJoints * numSpacecraft)
143+
desJointStateInMsgData.states.clear()
144+
desJointStateInMsgData.stateDots.clear()
145+
for v in np.asarray(desTheta).flatten():
146+
desJointStateInMsgData.states.push_back(float(v))
147+
for v in np.asarray(desThetaDot).flatten():
148+
desJointStateInMsgData.stateDots.push_back(float(v))
149+
150+
# create the joint reactions input message
151+
JointReactionsInMsgData = messaging.MJJointReactionsMsgPayload()
152+
jointTreeIdx = []
153+
jointTypes = []
154+
jointDOFStart = []
155+
currentDOF = 0
156+
for sc in range(numSpacecraft):
157+
idx = [sc] * (numJoints+1)
158+
jointTreeIdx.extend(idx)
159+
types = [0] + [3] * numJoints
160+
jointTypes.extend(types)
161+
jointDOFStart.append(currentDOF)
162+
currentDOF += 6
163+
for j in range(numJoints):
164+
jointDOFStart.append(currentDOF)
165+
currentDOF += 1
166+
if nonActForces:
167+
biasForces = [-2.0] * ((6 + numJoints) * numSpacecraft)
168+
else:
169+
biasForces = [0.0] * ((6 + numJoints) * numSpacecraft)
170+
passiveForces = [0.0] * ((6 + numJoints) * numSpacecraft)
171+
constraintForces = [0.0] * ((6 + numJoints) * numSpacecraft)
172+
appliedForces = [0.0] * ((6 + numJoints) * numSpacecraft)
173+
JointReactionsInMsgData.jointTreeIdx.clear()
174+
JointReactionsInMsgData.jointTypes.clear()
175+
JointReactionsInMsgData.jointDOFStart.clear()
176+
JointReactionsInMsgData.biasForces.clear()
177+
JointReactionsInMsgData.passiveForces.clear()
178+
JointReactionsInMsgData.constraintForces.clear()
179+
JointReactionsInMsgData.appliedForces.clear()
180+
for v in jointTreeIdx:
181+
JointReactionsInMsgData.jointTreeIdx.push_back(v)
182+
for v in jointTypes:
183+
JointReactionsInMsgData.jointTypes.push_back(v)
184+
for v in jointDOFStart:
185+
JointReactionsInMsgData.jointDOFStart.push_back(v)
186+
for v in biasForces:
187+
JointReactionsInMsgData.biasForces.push_back(v)
188+
for v in passiveForces:
189+
JointReactionsInMsgData.passiveForces.push_back(v)
190+
for v in constraintForces:
191+
JointReactionsInMsgData.constraintForces.push_back(v)
192+
for v in appliedForces:
193+
JointReactionsInMsgData.appliedForces.push_back(v)
194+
195+
# write input messages
196+
massMatrixInMsg = messaging.MJSysMassMatrixMsg().write(massMatrixInMsgData)
197+
reactionForcesInMsg = messaging.MJJointReactionsMsg().write(JointReactionsInMsgData)
198+
desJointStateInMsg = messaging.JointArrayStateMsg().write(desJointStateInMsgData)
199+
jointStatesInMsgList = []
200+
jointStateDotsInMsgList = []
201+
for i in range(numJoints * numSpacecraft):
202+
jointStatesInMsg = messaging.ScalarJointStateMsg().write(jointStatesInMsgDataList[i])
203+
jointStatesInMsgList.append(jointStatesInMsg)
204+
jointStateDotsInMsg = messaging.ScalarJointStateMsg().write(jointStateDotsInMsgDataList[i])
205+
jointStateDotsInMsgList.append(jointStateDotsInMsg)
206+
207+
# subscribe input messages to module
208+
module.massMatrixInMsg.subscribeTo(massMatrixInMsg)
209+
module.reactionForcesInMsg.subscribeTo(reactionForcesInMsg)
210+
module.desJointStatesInMsg.subscribeTo(desJointStateInMsg)
211+
for i in range(numJoints * numSpacecraft):
212+
module.jointStatesInMsgs[i].subscribeTo(jointStatesInMsgList[i])
213+
module.jointStateDotsInMsgs[i].subscribeTo(jointStateDotsInMsgList[i])
214+
215+
# setup output message recorder objects
216+
motorTorqueRecorders = []
217+
for i in range(numJoints * numSpacecraft):
218+
rec = module.motorTorquesOutMsgs[i].recorder()
219+
motorTorqueRecorders.append(rec)
220+
unitTestSim.AddModelToTask(unitTaskName, rec)
221+
222+
unitTestSim.InitializeSimulation()
223+
unitTestSim.ConfigureStopTime(macros.sec2nano(0.1))
224+
unitTestSim.ExecuteSimulation()
225+
226+
# pull module data
227+
moduleTorques = []
228+
for rec in motorTorqueRecorders:
229+
moduleTorques.append(rec.input[-1])
230+
moduleTorques = np.asarray(moduleTorques, dtype=float).reshape(-1)
231+
232+
# compute expected results
233+
reactionForces = np.asarray(biasForces, dtype=float).reshape(-1)
234+
expectedTorques = np.zeros(numJoints * numSpacecraft, dtype=float)
235+
for sc in range(numSpacecraft):
236+
startIdx = sc * (6 + numJoints)
237+
M_sc = M[startIdx:startIdx+6+numJoints, startIdx:startIdx+6+numJoints]
238+
K_sc = Ktheta[sc*numJoints:(sc+1)*numJoints, sc*numJoints:(sc+1)*numJoints]
239+
P_sc = Ptheta[sc*numJoints:(sc+1)*numJoints, sc*numJoints:(sc+1)*numJoints]
240+
theta_sc = theta[0:numJoints]
241+
thetaDot_sc = thetaDot[0:numJoints]
242+
desTheta_sc = desTheta[sc*numJoints:(sc+1)*numJoints]
243+
desThetaDot_sc = desThetaDot[sc*numJoints:(sc+1)*numJoints]
244+
bias_sc = -reactionForces[startIdx:startIdx+6+numJoints]
245+
246+
if maxTorque:
247+
uMax_sc = uMax
248+
else:
249+
uMax_sc = None
250+
251+
torques_sc = jointTorques(M_sc, K_sc, P_sc, theta_sc, thetaDot_sc,
252+
desTheta_sc, desThetaDot_sc, bias_sc, maxTorque=uMax_sc)
253+
expectedTorques[sc*numJoints:(sc+1)*numJoints] = torques_sc
254+
255+
# Assert the motor torques are correct
256+
np.testing.assert_allclose(moduleTorques, expectedTorques, rtol=1e-8,
257+
err_msg=f"Motor torques {moduleTorques} do not match expected values {expectedTorques}")
258+
259+
if __name__ == "__main__":
260+
test_hingedJointArrayMotor(True, False, 1, 1)

0 commit comments

Comments
 (0)