diff --git a/src/roboticstoolbox/ets/ETS.py b/src/roboticstoolbox/ets/ETS.py index 2be215940..c4ad72701 100644 --- a/src/roboticstoolbox/ets/ETS.py +++ b/src/roboticstoolbox/ets/ETS.py @@ -175,7 +175,12 @@ def fixjoint(self, j: int, value: float, unit: str = "rad") -> ETS: :returns: ETS with one fewer joint :raises ValueError: for an invalid joint position or angular unit """ - if not isinstance(j, (int, np.integer)) or not 0 <= j < self.n: + # bool subclasses int, but must not select joint 0 or 1. + if ( + isinstance(j, bool) + or not isinstance(j, (int, np.integer)) + or not 0 <= j < self.n + ): raise ValueError(f"joint index {j} out of range for {self.n} joints") if unit not in ("rad", "deg"): raise ValueError("unit must be 'rad' or 'deg'") diff --git a/src/roboticstoolbox/ets/ETS2.py b/src/roboticstoolbox/ets/ETS2.py index 8261a0abe..79ffebb1c 100644 --- a/src/roboticstoolbox/ets/ETS2.py +++ b/src/roboticstoolbox/ets/ETS2.py @@ -147,7 +147,12 @@ def fixjoint(self, j: int, value: float, unit: str = "rad") -> ETS2: :returns: ETS2 with one fewer joint :raises ValueError: for an invalid joint position or angular unit """ - if not isinstance(j, (int, np.integer)) or not 0 <= j < self.n: + # bool subclasses int, but must not select joint 0 or 1. + if ( + isinstance(j, bool) + or not isinstance(j, (int, np.integer)) + or not 0 <= j < self.n + ): raise ValueError(f"joint index {j} out of range for {self.n} joints") if unit not in ("rad", "deg"): raise ValueError("unit must be 'rad' or 'deg'") diff --git a/tests/test_ETS.py b/tests/test_ETS.py index bb02c8440..72b3746c3 100644 --- a/tests/test_ETS.py +++ b/tests/test_ETS.py @@ -231,6 +231,23 @@ def test_fixjoint_rotation_in_degrees(self): ets.fixjoint(0, 90, unit="grad") nt.assert_allclose(ets.eval(q), expected) + def test_fixjoint_boolean_and_integer_indices(self): + ets = Rz() * tx() + q = np.array([0.2, 0.3]) + expected = ets.eval(q) + + # Both 0 and 1 are valid positions, so rejection must be based on type. + for index in (False, True, np.bool_(False), np.bool_(True)): + with self.subTest(index=index), self.assertRaises(ValueError): + ets.fixjoint(index, 0.2) + + for index in (0, 1, np.int64(0), np.int64(1)): + with self.subTest(index=index): + fixed = ets.fixjoint(index, q[index]) + nt.assert_allclose(fixed.eval(np.delete(q, index)), expected) + self.assertEqual(ets.n, 2) + nt.assert_allclose(ets.eval(q), expected) + def test_fixjoint_single_joint_and_bad_indices(self): ets = rtb.ETS(Rz()) fixed = ets.fixjoint(0, 0.3) diff --git a/tests/test_ETS2.py b/tests/test_ETS2.py index fb683eac1..4d48b230c 100644 --- a/tests/test_ETS2.py +++ b/tests/test_ETS2.py @@ -230,6 +230,23 @@ def test_fixjoint_rotation_in_degrees(self): ets.fixjoint(0, 90, unit="grad") nt.assert_allclose(ets.eval(q), expected) + def test_fixjoint_boolean_and_integer_indices(self): + ets = R() * tx() + q = np.array([0.2, 0.3]) + expected = ets.eval(q) + + # Both 0 and 1 are valid positions, so rejection must be based on type. + for index in (False, True, np.bool_(False), np.bool_(True)): + with self.subTest(index=index), self.assertRaises(ValueError): + ets.fixjoint(index, 0.2) + + for index in (0, 1, np.int64(0), np.int64(1)): + with self.subTest(index=index): + fixed = ets.fixjoint(index, q[index]) + nt.assert_allclose(fixed.eval(np.delete(q, index)), expected) + self.assertEqual(ets.n, 2) + nt.assert_allclose(ets.eval(q), expected) + def test_fixjoint_single_joint_and_bad_indices(self): ets = rtb.ETS2(R()) fixed = ets.fixjoint(0, 0.3)