Suppose we have the $p\times p$ random matrix

$\begin{equation*} \bm{A} = \sum_{i=1}^n \bm{x}_i\bm{x}_i^T, \end{equation*}$where $n\geq p$ and each $\bm{x}_i\in\mathbb{R}^p$ is independently drawn from a zero-mean multivariate normal distribution

$\begin{equation*} \bm{x}_i \sim \mathcal{N}_p(\bm{0},\bm{\Sigma}) \end{equation*}$with positive definite covariance matrix $\bm{\Sigma}$. Then $\bm{A}$ is Wishart-distributed, and we write

$\begin{equation*} \bm{A} \sim \mathcal{W}_p(\bm{\Sigma},n), \end{equation*}$where $n$ is called the *degrees of freedom*. We refer to the special case
$\mathcal{W}_p(\bm{I}_p,n)$ as the *standard* Wishart distribution, where
$\bm{I}_p$ is the $p\times p$ identity matrix.

The Wishart distribution arises as the conjugate prior to the inverse covariance matrix of a multivariate normal distribution in Bayesian statistics, among other places.

One property of the Wishart distribution that we will make use of later is that if $\bm{A}\sim\mathcal{W}_p(\bm{\Sigma},n)$ and $\bm{C}\in\mathbb{R}^{p \times p}$ is a constant, full-rank matrix, then

$\begin{equation}\label{1} \bm{C}\bm{A}\bm{C}^T \sim \mathcal{W}_p(\bm{C}\bm{\Sigma}\bm{C}^T,n). \end{equation}$Suppose we want to compute the probability that the eigenvalues of some standard Wishart-distributed matrix

$\begin{equation*} \bm{S} \sim \mathcal{W}_p(\bm{I}_p,n) \end{equation*}$are contained in a given interval $[a,b]$, with $0\leq a\leq b$. We denote this probability as

$\begin{equation}\label{2} \mathrm{Pr}[a\leq\lambda_{\min}(\bm{S}), \lambda_{\max}(\bm{S})\leq b], \end{equation}$where $\lambda_{\min}(\bm{S})$ and $\lambda_{\max}(\bm{S})$ are the minimum and maximum eigenvalues of $\bm{S}$, respectively. The probability $\eqref{2}$ is equivalent to

$\begin{equation}\label{3} \mathrm{Pr}[a\bm{I}_p \preccurlyeq \bm{S} \preccurlyeq b\bm{I}_p] \end{equation}$(see Appendix A.5.2 of (Boyd & Vandenberghe, 2004)), where the notation $\bm{A}\preccurlyeq\bm{B}$ means that $\bm{B}-\bm{A}$ is positive semidefinite.

Algorithm 1 of (Chiani, 2017) tells us how to compute the function $\psi(a,b)$ such that

$\begin{equation*} \mathrm{Pr}[a\bm{I}_p \preccurlyeq \bm{S} \preccurlyeq b\bm{I}_p] = \psi(a, b), \end{equation*}$and I've implemented this algorithm in Python here. This immediately gives us the cumulative density functions (CDFs) of the minimum and maximum eigenvalues of $\bm{S}$:

$\begin{align*} \mathrm{Pr}[a\leq\lambda_{\min}(\bm{S})] &= \psi(a, \infty) = 1 - C_{\min}(a), \\ \mathrm{Pr}[\lambda_{\max}(\bm{S})\leq b] &= \psi(0, b) = C_{\max}(b), \end{align*}$where $C_{\min}$ and $C_{\max}$ are the CDFs for the minimum and maximum eigenvalues, respectively.

We now have a method for computing the CDFs of the eigenvalues, but we do not have the inverse CDFs (i.e., the quantile functions), which compute the bounds required to achieve a given probability. For example, suppose we want to find the value $b\geq0$ such that $\lambda_{\max}(\bm{S})\leq b$ with a given probability $\rho\in[0,1]$. One way to do this is by solving the optimization problem

$\begin{equation*} \mathrm{argmin}_{b\geq0} (\rho - C_{\max}(b))^2, \end{equation*}$which is also implemented here, using one of scipy's built-in solvers.

We can extend the results above to the non-standard Wishart-distributed matrix

$\begin{equation*} \bm{A} \sim \mathcal{W}_p(\bm{\Sigma},n). \end{equation*}$In particular, generalizing $\eqref{3}$ we get

$\begin{equation}\label{4} \mathrm{Pr}[a\bm{\Sigma} \preccurlyeq \bm{A} \preccurlyeq b\bm{\Sigma}] = \psi(a,b). \end{equation}$To see this, let $\bm{L}$ be the Cholesky decomposition of $\bm{\Sigma}$, such that $\bm{L}\bm{L}^T=\bm{\Sigma}$. Since $\bm{\Sigma}$ is positive definite, $\bm{L}$ is full-rank and invertible. Then from $\eqref{1}$ we know that $\bm{L}^{-1}\bm{A}\bm{L}^{-T}\sim\mathcal{W}_p(\bm{L}^{-1}\bm{\Sigma}\bm{L}^{-T},n)=\mathcal{W}_p(\bm{I}_p,n)$, which means that $\mathrm{Pr}[a\bm{I}_p\preccurlyeq\bm{L}^{-1}\bm{A}\bm{L}^{-T}\preccurlyeq b\bm{I}_p]=\psi(a,b)$. Finally, since $a\bm{I}_p\preccurlyeq\bm{L}^{-1}\bm{A}\bm{L}^{-T}\preccurlyeq b\bm{I}_p$ if and only if $a\bm{\Sigma}\preccurlyeq\bm{A}\preccurlyeq b\bm{\Sigma}$ (this follows from Observation 7.1.8 of (Horn & Johnson, 2013)), we obtain $\eqref{4}$.

Notice that $\eqref{4}$ is no longer expressed in terms of eigenvalues of $\bm{A}$, but only expresses the probability that $\bm{A}$ is bounded by two matrices. This could be useful for formulating chance constraints on a positive definite matrix in convex optimization.

*Thanks to Abhishek Goudar for reading a draft of this.*

- Boyd, Stephen and Lieven Vandenberghe (2004).
*Convex Optimization*. Cambridge University Press. (Freely available here.) - Chiani, M. (2017). "On the Probability That All Eigenvalues of
Gaussian, Wishart, and Double Wishart Random Matrices Lie Within an
Interval," in
*IEEE Transactions on Information Theory*, vol. 63, no. 7, pp. 4521–4531, July 2017, doi: 10.1109/TIT.2017.2694846, arxiv: 1502.04189. - Horn, Roger A. and Charles R. Johnson (2013).
*Matrix Analysis*. Cambridge University Press.

The inertia of a spherical shell with mass $m$ and radius $r$ is well-known to be $\bm{J}_{\circ}=(2/3)mr^2\bm{I}_3$, where we've denoted the inertia as $\bm{J}$ to avoid confusion with the $3\times 3$ identity matrix $\bm{I}_3$. We also use the subscript $(\cdot)_\circ$ to differentiate the inertia of the shell from that of the uniform solid, which we will denote $\bm{J}_\bullet$. All of the inertia tensors in this post are taken about the centroid of the shape, which coincides with the center of mass.

We will derive $\bm{J}_\circ$ for the sphere here before moving on to the more general ellipsoidal case, because the derivations are similar. We will use a similar but slightly different approach to the derivation here.

Recall that the inertia tensor of a solid sphere of uniform density is $\bm{J}_\bullet=(2/5)mr^2\bm{I}_3$. Substituting in $m=\rho V=\rho(4/3)\pi r^3$, where $\rho$ is the density and $V$ is the volume of the sphere, we get $\bm{J}_\bullet=(8/15)\pi\rho r^5\bm{I}_3$.

To obtain a *shell*, we will take one solid sphere of radius $r$ and
subtract its inertia from that of a slightly larger solid sphere, which has
radius $(1+\Delta)r$ with $\Delta\geq 0$, and taking the limit $\Delta\to 0$.
This gives us

The density of the shell is given by

$\begin{equation}\label{2} \rho = \frac{m}{(4/3)\pi((1+\Delta)^3-1)r^3}, \end{equation}$where the volume (the denominator) is the difference between the volumes of the outer and inner spheres. Substituting $\eqref{2}$ into $\eqref{1}$, we get

$\begin{equation}\label{3} \bm{J}_\circ = \lim _{\Delta\to0}\ (2/5)m\frac{(1+\Delta)^5-1}{(1+\Delta)^3-1}r^2\bm{I}_3. \end{equation}$We can easily determine that

$\begin{equation}\label{4} \lim _{\Delta\to0}\ \frac{(1+\Delta)^5-1}{(1+\Delta)^3-1} = \frac{5}{3} \end{equation}$using the small block of Python code

```
import sympy
Δ = sympy.symbols("Δ")
lim = ((1 + Δ) ** 5 - 1) / ((1 + Δ) ** 3 - 1)
print(lim.expand().simplify().subs({Δ: 0}))
```

Substituting $\eqref{4}$ back into $\eqref{3}$, we achieve the expected result for the spherical shell:

$\begin{equation}\label{5} \bm{J}_{\circ} = (2/3)mr^2\bm{I}_3. \end{equation}$We are now ready to generalize to the inertia of the ellipsoidal shell. In this section of the article we'll now use $\bm{J}_\circ$ and $\bm{J}_\bullet$ to refer to the inertia tensors of the ellipsoidal shell and solid ellipsoid, respectively. We will consider an ellipsoid of mass $m$ and semi-axes $a,b,c$. This ellipsoid consists of the set of all points $\bm{x}\in\mathbb{R}^3$ that satisfy $\bm{x}^T\bm{E}^{-1}\bm{x}\leq 1$, where

$\begin{equation*} \bm{E} = \begin{bmatrix} a^2 & 0 & 0 \\ 0 & b^2 & 0 \\ 0 & 0 & c^2 \end{bmatrix}. \end{equation*}$The inertia tensor of a solid ellipsoid of uniform density is $\bm{J}_\bullet=(1/5)m\bm{S}$, where

$\begin{equation*} \bm{S} = \begin{bmatrix} b^2+c^2 & 0 & 0 \\ 0 & a^2+c^2 & 0 \\ 0 & 0 & a^2+b^2 \end{bmatrix} = \mathrm{tr}(\bm{E})\bm{I}_3 - \bm{E}, \end{equation*}$with $\mathrm{tr}(\cdot)$ denoting the matrix trace. Again we substitute in $m=\rho V=\rho(4/3)\pi abc$ to obtain $\bm{J}_\bullet=(4/15)\pi\rho abc\bm{S}$.

In the same way as for the spherical shell, we will obtain an ellipsoidal shell by subtracting an ellipsoid with semi-axes $a,b,c$ from a slightly larger ellipsoid with semi-axes $(1+\Delta)a,(1+\Delta)b,(1+\Delta)c$. Taking the limit $\Delta\to 0$, we get

$\begin{equation}\label{6} \bm{J}_\circ = \lim _{\Delta\to0}\ (4/15)\pi\rho((1+\Delta)^5-1)abc\bm{S}. \end{equation}$with density

$\begin{equation}\label{7} \rho = \frac{m}{(4/3)\pi((1+\Delta)^3-1)abc}. \end{equation}$Substituting $\eqref{7}$ into $\eqref{6}$, we get

$\begin{equation*} \bm{J}_\circ = \lim _{\Delta\to0}\ (1/5)m\frac{(1+\Delta)^5-1}{(1+\Delta)^3-1}\bm{S}. \end{equation*}$Again making use of $\eqref{4}$, we obtain our final result:

$\begin{equation}\label{8} \bm{J}_\circ = (1/3)m\bm{S}. \end{equation}$- The spherical shell inertia $\eqref{5}$ is of course a special case of the ellipsoidal shell inertia $\eqref{8}$ when $r=a=b=c$.
- The ellipsoidal shell inertia $\eqref{8}$ is equal to the inertia of a solid cuboid of uniform density with mass $m$ and side lengths $2a,2b,2c$.
- The ellipsoidal shell inertia $\eqref{8}$ is equal to the inertia of a system of six point masses, each with mass $m/6$, with one point each located at the ends of the semi-axes. That is, the point masses are located at $(\pm a,0,0)$, $(0, \pm b, 0)$, and $(0, 0, \pm c)$.

First, there is now the `BulletBody`

class for quickly creating rigid bodies.
Creating and adding a box to a simulation is as easy as

```
>>> import pybullet as pyb
>>> import pyb_utils
>>> pyb.connect(pyb.GUI)
# create a 1x1x1 cube at the origin
>>> box = pyb_utils.BulletBody.box(position=[0, 0, 0], half_extents=[0.5, 0.5, 0.5])
```

The BulletBody class takes care of the boilerplate for creating the visual and collision objects and combining them into a multibody. It also provides methods for getting and setting position, orientation, and velocity, as well as applying external forces and torques.

In addition to the box, there are dedicated constructors for spheres, cylinders, and capsules. For example:

```
# put a ball on top of the cube
>>> ball = pyb_utils.BulletBody.sphere(position=[0, 0, 1.5], radius=0.5)
# now put it somewhere else
>>> ball.set_pose(position=[2, 0, 0.5])
```

The BulletBody class makes it easy to add simple objects to act as obstacles or manipulation targets, for instance.

Second, pyb_utils now wraps some PyBullet functions and returns *named* tuples
rather than just vanilla tuples (but the calling API is exactly the same). This
makes it a lot easier to remember what the values in the returned tuples
actually mean. In particular, there are wrappers for `getDynamicsInfo`

,
`getContactPoints`

, `getClosestPoints`

, and `getConstraintInfo`

. These
functions all return tuples containing over ten values each. Continuing
our example from above, with regular PyBullet we have something like

```
# hard to read!
>>> pyb.getDynamicsInfo(box.uid, -1)
(1.0,
0.5,
(0.16666666666666666, 0.16666666666666666, 0.16666666666666666),
(0.0, 0.0, 0.0),
(0.0, 0.0, 0.0, 1.0),
0.0,
0.0,
0.0,
-1.0,
-1.0,
2,
0.001)
```

Instead, we can easily replace the call with the pyb_utils equivalent:

```
>>> info = pyb_utils.getDynamicsInfo(box.uid, -1)
# now we can access fields by name
>>> info.mass
1.0
>>> info.localInertiaPos
(0.0, 0.0, 0.0)
```

Now there is no need to count through the field names in the PyBullet documentation every time one of these functions is used. And since the calling API is exactly the same, there is no barrier to switching to the pyb_utils wrappers.

Finally, I want to mention a couple of simple quaternion utilities. They've actually been present in pyb_utils for quite a while, but I've come to appreciate them more over time for fast prototyping. PyBullet represents 3D orientation using quaternions in $(x,y,z,w)$ order (i.e., with the scalar part last). Using the spatialmath library under the hood, pyb_utils provides the functions:

```
quaternion_to_matrix # convert quaternion to rotation matrix
matrix_to_quaternion # convert rotation matrix to quaternion
quaternion_multiply # multiply two quaternions together (Hamilton product)
quaternion_rotate # rotate a point by a rotation represented by a quaternion
```

which make it easy to apply and compound rotations.
The main reason to use these functions rather than just those from spatialmath directly is
because by default spatialmath uses the convention that the scalar part of
the quaternion is *first*; to switch to the PyBullet convention one needs to constantly pass the
`order`

keyword argument to all of the functions, which is tiresome and hard to
debug if forgotten.

I wrote and continue adding to pyb_utils because it provides a set of tools useful for my research on robotics. I hope it can be useful for others as well. Pull requests are welcome!

]]>