<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Parsa Mesgarha</title><description/><link>https://articulateprimate.org/</link><item><title>SVD,Eigendecomposition and Mahalanobis (manobala) Distance</title><link>https://articulateprimate.org/mahalanobis-distance-svd-eigendecomposition/</link><guid isPermaLink="true">https://articulateprimate.org/mahalanobis-distance-svd-eigendecomposition/</guid><pubDate>Mon, 06 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Need for Mahalanobis distance&lt;/h2&gt;
&lt;p&gt;Distance calculated using euclidean geometry provides the absolute value between an origin and the target, yet this quantity lacks on stating how unique/unremarkable a particular point is in a data cluster. Standard deviation of a probability distibution comments on how the variables are spread. Mahalanobis distance is that quantity that tells the measure of a point from the mean in terms of standard deviation. This is particularly required in point clouds because this helps in determining outliers/noise while performing Iterative Closing Point Algorithm.&lt;/p&gt;
&lt;h2&gt;Formulas to get familiar with&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;The Mahalanobis distance is calculated as:&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;$$
d_M (x) = \sqrt{(x - \mu)^\top \Sigma^{-1}(x - \mu)}
$$&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Setting $e = x - \mu$ (the deviation from the mean):&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;$$
d_M^2 = e^\top \Sigma^{-1} e = \Vert e \Vert_\Sigma^2
$$&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;$\Sigma$ is the $d \times d$ covariance matrix&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;$$
\Sigma_{d \times d} = \frac{1}{n - 1} \sum_{i=1}^{n} (\mathbf{x}_i - \bar{\mathbf{x}})(\mathbf{x}_i - \bar{\mathbf{x}})^\top
$$&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Let $d=2$, so each observation is a 2D vector $\mathbf{x}&lt;em&gt;i = \begin{bmatrix}x&lt;/em&gt;{i1}\ x_{i2}\end{bmatrix}$ and the mean is $\bar{\mathbf{x}} = \begin{bmatrix}\bar{x}_1\ \bar{x}_2\end{bmatrix}$.
The deviation vector for observation $i$&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;$$
\mathbf{x}&lt;em&gt;i - \bar{\mathbf{x}} = \begin{bmatrix}x&lt;/em&gt;{i1} - \bar{x}&lt;em&gt;1\ x&lt;/em&gt;{i2} - \bar{x}_2\end{bmatrix}
$$&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The outer product is $(\mathbf{x}_i - \bar{\mathbf{x}})(\mathbf{x}_i - \bar{\mathbf{x}})^\top$.
This is a column vector times a row vector — giving a $2\times2$ matrix:&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;$$
(\mathbf{x}_i - \bar{\mathbf{x}})(\mathbf{x}&lt;em&gt;i - \bar{\mathbf{x}})^\top = \begin{bmatrix}x&lt;/em&gt;{i1}-\bar{x}&lt;em&gt;1\ x&lt;/em&gt;{i2}-\bar{x}&lt;em&gt;2\end{bmatrix}\begin{bmatrix}x&lt;/em&gt;{i1}-\bar{x}&lt;em&gt;1 &amp;amp; x&lt;/em&gt;{i2}-\bar{x}_2\end{bmatrix}
$$&lt;/p&gt;
&lt;p&gt;$$
= \begin{bmatrix}(x_{i1}-\bar{x}&lt;em&gt;1)^2 &amp;amp; (x&lt;/em&gt;{i1}-\bar{x}&lt;em&gt;1)(x&lt;/em&gt;{i2}-\bar{x}&lt;em&gt;2)\ (x&lt;/em&gt;{i2}-\bar{x}&lt;em&gt;2)(x&lt;/em&gt;{i1}-\bar{x}&lt;em&gt;1) &amp;amp; (x&lt;/em&gt;{i2}-\bar{x}_2)^2\end{bmatrix}
$$&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;note: d is number of features(columns) and n is number of observations (rows)
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;Sum over all $n$ observations&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;$$
\sum_{i=1}^{n}(\mathbf{x}_i - \bar{\mathbf{x}})(\mathbf{x}&lt;em&gt;i - \bar{\mathbf{x}})^\top = \begin{bmatrix}\sum_i(x&lt;/em&gt;{i1}-\bar{x}&lt;em&gt;1)^2 &amp;amp; \sum_i(x&lt;/em&gt;{i1}-\bar{x}&lt;em&gt;1)(x&lt;/em&gt;{i2}-\bar{x}&lt;em&gt;2)\ \sum_i(x&lt;/em&gt;{i2}-\bar{x}&lt;em&gt;2)(x&lt;/em&gt;{i1}-\bar{x}&lt;em&gt;1) &amp;amp; \sum_i(x&lt;/em&gt;{i2}-\bar{x}_2)^2\end{bmatrix}
$$&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Divide by $n-1$&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;$$
\Sigma_{2\times2} = \frac{1}{n-1}\begin{bmatrix}\sum_i(x_{i1}-\bar{x}&lt;em&gt;1)^2 &amp;amp; \sum_i(x&lt;/em&gt;{i1}-\bar{x}&lt;em&gt;1)(x&lt;/em&gt;{i2}-\bar{x}&lt;em&gt;2)\ \sum_i(x&lt;/em&gt;{i2}-\bar{x}&lt;em&gt;2)(x&lt;/em&gt;{i1}-\bar{x}&lt;em&gt;1) &amp;amp; \sum_i(x&lt;/em&gt;{i2}-\bar{x}_2)^2\end{bmatrix}
$$&lt;/p&gt;
&lt;p&gt;Reading each entry&lt;/p&gt;
&lt;p&gt;$$
\Sigma_{2\times2} = \begin{bmatrix}\text{Var}(x_1) &amp;amp; \text{Cov}(x_1,x_2)\ \text{Cov}(x_2,x_1) &amp;amp; \text{Var}(x_2)\end{bmatrix}
$$&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;$(1,1)$&lt;/strong&gt;: $\frac{1}{n-1}\sum_i(x_{i1}-\bar{x}_1)^2$ — variance of feature 1. Same feature squared with itself.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;$(1,2)$&lt;/strong&gt;: $\frac{1}{n-1}\sum_i(x_{i1}-\bar{x}&lt;em&gt;1)(x&lt;/em&gt;{i2}-\bar{x}_2)$ — covariance of feature 1 and 2. Two different features multiplied.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;$(2,1)$&lt;/strong&gt;: same as $(1,2)$ — always symmetric since multiplication is commutative.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;$(2,2)$&lt;/strong&gt;: $\frac{1}{n-1}\sum_i(x_{i2}-\bar{x}_2)^2$ — variance of feature 2.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;Connection to $\frac{1}{n-1}X_c^\top X_c$ (centered data)&lt;/h3&gt;
&lt;p&gt;The formula $\frac{1}{n-1}\sum_i(\mathbf{x}_i - \bar{\mathbf{x}})(\mathbf{x}_i - \bar{\mathbf{x}})^\top$ and $\frac{1}{n-1}X_c^\top X_c$ are the same thing written two different ways:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The summation form adds up $n$ outer products one observation at a time&lt;/li&gt;
&lt;li&gt;The matrix form stacks all observations into $X_c$ and does one matrix multiplication&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Both produce the identical $2\times2$ covariance matrix — the summation form makes the meaning explicit, the matrix form is compact and fast to compute.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;⚠️ Centering requirement:&lt;/strong&gt; The matrix form only equals the covariance
matrix if $X_c$ is &lt;strong&gt;mean-centered&lt;/strong&gt; — each row must be
$(\mathbf{x}_i - \bar{\mathbf{x}})^\top$, so every column sums to zero.
The summation form has $-\bar{\mathbf{x}}$ built into each term; the
matrix form must bake it into $X_c$ itself.&lt;/p&gt;
&lt;p&gt;On raw (uncentered) data, $\frac{1}{n-1}X^\top X$ gives the &lt;strong&gt;second
moment matrix&lt;/strong&gt; instead:&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;$$
\frac{1}{n-1}X^\top X = \Sigma + \tfrac{n}{n-1},\bar{\mathbf{x}}\bar{\mathbf{x}}^\top
$$&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;equal to $\Sigma$ only when $\bar{\mathbf{x}} = \mathbf{0}$.&lt;/p&gt;
&lt;p&gt;Centering ≠ normalising. The ladder:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Raw $X$ → second moment matrix&lt;/li&gt;
&lt;li&gt;Centered $X$ → covariance matrix $\Sigma$ ← what Mahalanobis needs&lt;/li&gt;
&lt;li&gt;Centered + scaled by std dev → correlation matrix $R$
&lt;strong&gt;Worked example (1 feature, $n=3$):&lt;/strong&gt; let $x = [1, 2, 3]$, so $\bar x = 2$.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Centered: $x_c = [-1, 0, 1]$
$$
\frac{1}{n-1}x_c^\top x_c = \frac{1}{2}\left[(-1)^2+0^2+1^2\right] = 1
$$
This matches $\text{Var}(x) = \frac{1}{n-1}\sum_i(x_i-\bar x)^2 = 1$ exactly. ✓&lt;/p&gt;
&lt;p&gt;Raw (uncentered): using $x = [1,2,3]$ directly&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;$$
\frac{1}{n-1}x^\top x = \frac{1}{2}\left[1^2+2^2+3^2\right] = \frac{14}{2} = 7
$$&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;7 is &lt;strong&gt;not&lt;/strong&gt; the variance — it&apos;s inflated by the mean not being zero. This is exactly $\Sigma + \frac{n}{n-1}\bar x^2 = 1 + \frac{3}{2}(2)^2 = 1 + 6 = 7$. ✓&lt;/p&gt;
&lt;p&gt;Skipping the centering step silently swaps &quot;spread around the mean&quot; for &quot;spread around zero&quot; — the two only agree when $\bar x = 0$.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; The query point $x$ in $d_M(x)$ never needs pre-centering — the
$(x - \mu)$ inside the formula centers it explicitly. Centering only
matters for how $\Sigma$ is &lt;em&gt;computed&lt;/em&gt; from the data matrix.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;What is covariance, variance and standard deviation? and Ohh my Gaussian Curve&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;$\sigma^2$ is variance — the average &lt;strong&gt;squared&lt;/strong&gt; deviation of points from the mean; it measures how spread out the distribution is&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;$$
\sigma^2 = \frac{1}{n - 1}\sum_{i=1}^{n}(x_i - \bar{x})^2
$$&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;$\sigma$ is the &lt;strong&gt;standard deviation&lt;/strong&gt; — the square root of the variance. Taking the root brings the units back to the first power (e.g. meters instead of meters²), same units as the data.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;$\text{cov}(x_1, x_2)$ covariance tells how two features vary with respect to each other — positive when they move together, negative when they move oppositely, near zero when unrelated&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;variance ($\sigma^2$) can also be written as&lt;br /&gt;
$$
\sigma^2=E(X^2)-\mu^2
$$&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Where $E(X^2)$ is:
$$
\int_{-\infty}^{\infty} x^2 f(x), dx
$$
And $f(x)$ is the Gaussian density formula:
$$
\frac{1}{\sigma \sqrt{2\pi}} \exp\left(-\frac{(x - \mu)^2}{2\sigma^2}\right)
$$&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;$\frac{1}{\sigma \sqrt{2\pi}}$ is the normalising constant to keep area under curve equal to 1.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;A Gaussian random variable is written $X \sim N(\mu, \sigma^2)$ where $\mu$ is the mean and $\sigma^2$ is the variance&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;all Gaussian curves are bell shaped but they can be shifted (by tuning $\mu$) or stretched/squeezed (by tuning $\sigma$)&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://articulateprimate.org/images/normal_distribution_complete_clean.png&quot; alt=&quot;gaussian curve different size&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;area under all theses curves is 1&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Probability gives me a confidence/cerainity score of the belief we have on a method/result. A self driving car gets a LiDAR reading of 3.2 m to hit an obstacle, is it safe? It depends entirely on th threshold value of an acceptable uncertanity score. If $\sigma$ is less than 0.05m, then it is fine.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;So what the f*ck is Singular Value Decomposition?????&lt;/h2&gt;
&lt;p&gt;Any matrix $A$ irrespective of its &lt;strong&gt;shape&lt;/strong&gt;; number of independent columns it has (&lt;strong&gt;rank&lt;/strong&gt;); and the &lt;strong&gt;numbers present in it&lt;/strong&gt;, can be &lt;strong&gt;decomposed into 3 matrices&lt;/strong&gt;. &lt;strong&gt;That is it!!!&lt;/strong&gt; It is all that is need to be known to understand SVD apart from the formula.&lt;/p&gt;
&lt;p&gt;Now, the formula is made up of 3 matrices which basically rotate, transform, and maybe rotate any input vector again. The 3 stooges of matrices are made of the square roots of the eigen values and eigen vector of $A^TA$ and eigen vector $AA^T$.&lt;/p&gt;
&lt;p&gt;$$
A=US V^T
$$&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;$V$ columns are eigen vectors of $A^TA$&lt;/li&gt;
&lt;li&gt;$S$ is the singular values of $A$, which are the square roots of the eigen values of $A^TA$&lt;/li&gt;
&lt;li&gt;$U$ matrix consist of columns which are eigenvectors of $AA^T$&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;NOTE: SVD works only if $U$ and $V$ are orthonormal vectors. We will see why.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;$$
A^TA(v_{raw}) = \sigma^2(v_{raw}) \quad \textbf{--- (1)}
$$&lt;/p&gt;
&lt;p&gt;$$
(A^TA-\sigma^2 I)v_{raw}=0
$$&lt;/p&gt;
&lt;p&gt;$$
v_i = \frac{v_{raw}}{||v_{raw}||} \quad \textbf{--- (2)}
$$&lt;/p&gt;
&lt;p&gt;Substituting the &lt;strong&gt;normalised input eigen vector&lt;/strong&gt; from eqn$\textbf{(2)}$ in eqn $\textbf{(1)}$&lt;/p&gt;
&lt;p&gt;$$
A^TA(v_{i}) = \sigma^2(v_{i}) \quad \textbf{--- (3)}
$$&lt;/p&gt;
&lt;p&gt;$$
||Av_i||^2 = (Av_i)^T(Av_i) = v_i^TA^TAv_i \quad \textbf{--- (4)}
$$&lt;/p&gt;
&lt;p&gt;Substituting eqn$(3)$ in eqn$(4)$ we get&lt;/p&gt;
&lt;p&gt;$$
||Av_i||^2 = (Av_i)^T(Av_i) = v_i^TA^TAv_i = v_i^T\sigma^2(v_{i}) \quad \textbf{--- (5)}
$$&lt;/p&gt;
&lt;p&gt;Since $v_i$ is a unit vector, $v_i^Tv_i = 1$ (a scalar, not the identity matrix — that only applies when dotting a full orthonormal &lt;em&gt;matrix&lt;/em&gt; with its transpose, e.g. $V^TV = I$). Therefore eqn$(5)$ is&lt;/p&gt;
&lt;p&gt;$$
||Av_i|| = \sigma \quad \textbf{--- (6)}
$$&lt;/p&gt;
&lt;p&gt;we know that the magnitude of $Av_i$ from eqn$(6)$&lt;/p&gt;
&lt;p&gt;In eqn$(3)$ multiply both sides by $A$&lt;/p&gt;
&lt;p&gt;$$
AA^TA(v_{i}) = \sigma^2(Av_{i}) \quad \textbf{--- (7)}
$$&lt;/p&gt;
&lt;p&gt;normalising $Av_i$ we get&lt;/p&gt;
&lt;p&gt;$$
u_i=\frac{Av_i}{\sigma_i} \quad \textbf{--- (8)}
$$&lt;/p&gt;
&lt;p&gt;If we stack this relationship horizontally for all $n$ vector dimensions simultaneously, it maps perfectly to an equivalent matrix-matrix multiplication:&lt;/p&gt;
&lt;p&gt;$$
A \underbrace{\begin{bmatrix} \mathbf{v}_1 &amp;amp; \mathbf{v}_2 &amp;amp; \cdots &amp;amp; \mathbf{v}&lt;em&gt;n \end{bmatrix}}&lt;/em&gt;{V} = \underbrace{\begin{bmatrix} \mathbf{u}_1 &amp;amp; \mathbf{u}&lt;em&gt;2 &amp;amp; \cdots &amp;amp; \mathbf{u}&lt;em&gt;m \end{bmatrix}}&lt;/em&gt;{U} \underbrace{\begin{bmatrix} \sigma_1 &amp;amp; &amp;amp; \ &amp;amp; \sigma_2 &amp;amp; \ &amp;amp; &amp;amp; \ddots \end{bmatrix}}&lt;/em&gt;{\Sigma}
$$&lt;/p&gt;
&lt;p&gt;This simplifies neatly down to:&lt;/p&gt;
&lt;p&gt;$$
AV = U\Sigma
$$&lt;/p&gt;
&lt;p&gt;Because $V$ is an orthogonal matrix built from an orthonormal basis of eigenvectors, its inverse is identical to its transpose ($V^{-1} = V^\top$). Right-multiplying both sides by $V^\top$ completely isolates $A$:&lt;/p&gt;
&lt;p&gt;$$
AVV^\top = U\Sigma V^\top
$$&lt;/p&gt;
&lt;p&gt;$$
A = U\Sigma V^\top
$$&lt;/p&gt;
&lt;p&gt;So we have answered why we are using orthonormal eigenvectors and values of $A^TA$ for decomposing the matrix $A$ with the above derivation.&lt;/p&gt;
&lt;p&gt;$$ Singular : Value : Decomposition $$
&lt;img src=&quot;https://articulateprimate.org/images/SVD_transformation.png&quot; alt=&quot;svd transformation&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;Why did you even bring up mahalanobis distance and singular value decomposition together?&lt;/h2&gt;
&lt;p&gt;Look at the formulas of Mahalanobis distance and SVD they look similar. But that isn&apos;t enough, we need to answer if Mahalanobis distance is doing SVD. This intuition is partly right. Mahalanobis distance is prefere because the formula converts an anisotropic distribution (uneven) to isotropic distribution.&lt;/p&gt;
&lt;p&gt;From the previous formulas we can infer that covariance matrix is&lt;/p&gt;
&lt;p&gt;$$
\Sigma_{cov} = \frac{1}{n-1}\mathbf{X_c^T}\mathbf{X_c}
$$
$$
\mathbf{X_c} = US    V^T
$$&lt;/p&gt;
&lt;p&gt;Substituting:&lt;/p&gt;
&lt;p&gt;$$
\mathbf{X_c^T X_c} = (USV^\top)^\top(USV^\top) = VS^\top U^\top USV^\top = VS^2V^\top
$$&lt;/p&gt;
&lt;p&gt;(using $U^\top U = I$ and $S^\top S = S^2$ since $S$ is diagonal)&lt;/p&gt;
&lt;p&gt;Therefore:&lt;/p&gt;
&lt;p&gt;$$
\Sigma_{cov} = \frac{1}{n-1}\mathbf{V S^2 V^\top}
$$&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Note the order&lt;/strong&gt; — it&apos;s $VS^2V^\top$, not $V^\top S^2 V$. This matters: it tells you $V$ (not $V^\top$) holds the eigenvectors as its &lt;em&gt;columns&lt;/em&gt;, matching the standard eigendecomposition convention $\Sigma = V\Lambda V^\top$.&lt;/p&gt;
&lt;p&gt;Hence eigenvalues of $\Sigma_{cov}$&lt;/p&gt;
&lt;p&gt;$$
\lambda_i = \frac{\sigma^2_i}{n-1}
$$&lt;/p&gt;
&lt;p&gt;where $\sigma_i$ are the singular values of $\mathbf{X_c}$.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Is Mahalanobis distance doing SVD?&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Short answer: partly right, partly wrong.&lt;/strong&gt; Mahalanobis distance uses the &lt;em&gt;same underlying machinery&lt;/em&gt; as SVD — rotation by eigenvectors, scaling by eigenvalues — but it is not performing SVD itself. What it&apos;s actually doing is &lt;strong&gt;eigendecomposition&lt;/strong&gt;, applied specifically to the covariance matrix, followed by a distance computation that SVD alone doesn&apos;t give you.&lt;/p&gt;
&lt;h3&gt;What eigendecomposition actually is&lt;/h3&gt;
&lt;p&gt;For a &lt;strong&gt;symmetric&lt;/strong&gt; matrix like $\Sigma$, eigendecomposition is:&lt;/p&gt;
&lt;p&gt;$$
\Sigma = V \Lambda V^\top
$$&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;$V$: columns are eigenvectors of $\Sigma$ (same $V$ on &lt;em&gt;both&lt;/em&gt; sides — this only works because $\Sigma$ is symmetric)&lt;/li&gt;
&lt;li&gt;$\Lambda$: diagonal matrix of eigenvalues $\lambda_i$&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Compare this to general SVD:&lt;/p&gt;
&lt;p&gt;$$
A = U \Sigma_A V^\top
$$&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Two &lt;em&gt;different&lt;/em&gt; matrices $U$ and $V$ — because $A$ is not necessarily symmetric, so its &quot;natural input direction&quot; and &quot;natural output direction&quot; can differ&lt;/li&gt;
&lt;li&gt;$\Sigma_A$ contains &lt;strong&gt;singular values&lt;/strong&gt; $\sigma_i \geq 0$, not eigenvalues (which can be negative or complex for a non-symmetric $A$)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Why they coincide for $\Sigma$&lt;/h3&gt;
&lt;p&gt;Since the covariance matrix $\Sigma$ is symmetric and positive semi-definite:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Its eigenvalues are guaranteed real and $\geq 0$&lt;/li&gt;
&lt;li&gt;Its eigenvectors are orthogonal&lt;/li&gt;
&lt;li&gt;Therefore $U = V$ in its SVD, and its singular values &lt;em&gt;equal&lt;/em&gt; its eigenvalues exactly&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;So for $\Sigma$ specifically, eigendecomposition and SVD collapse into the same operation. That&apos;s the source of the &quot;Mahalanobis is doing SVD&quot; intuition — it isn&apos;t wrong, it&apos;s just that SVD and eigendecomposition happen to be indistinguishable for this one special (symmetric PSD) case.&lt;/p&gt;
&lt;h3&gt;Building Mahalanobis distance from the eigendecomposition&lt;/h3&gt;
&lt;p&gt;Start from:&lt;/p&gt;
&lt;p&gt;$$
d_M^2 = e^\top \Sigma^{-1} e
$$&lt;/p&gt;
&lt;p&gt;Substitute $\Sigma = V\Lambda V^\top \Rightarrow \Sigma^{-1} = V\Lambda^{-1}V^\top$ (valid since $V^{-1} = V^\top$ for orthogonal $V$):&lt;/p&gt;
&lt;p&gt;$$
d_M^2 = e^\top V \Lambda^{-1} V^\top e = (V^\top e)^\top \Lambda^{-1} (V^\top e)
$$&lt;/p&gt;
&lt;p&gt;Let $\tilde{e} = V^\top e$ — this is $e$ rotated into the eigenvector basis. Then:&lt;/p&gt;
&lt;p&gt;$$
d_M^2 = \tilde{e}^\top \Lambda^{-1} \tilde{e} = \sum_{i=1}^{d} \frac{\tilde{e}&lt;em&gt;i^2}{\lambda_i} = \sum&lt;/em&gt;{i=1}^{d}\left(\frac{\tilde{e}_i}{\sqrt{\lambda_i}}\right)^2 = \left| \Lambda^{-1/2} V^\top e \right|^2
$$&lt;/p&gt;
&lt;h3&gt;The actual comparison: SVD pipeline vs Mahalanobis pipeline&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Step&lt;/th&gt;
&lt;th&gt;SVD applied to a vector ($A\mathbf{x}$)&lt;/th&gt;
&lt;th&gt;Mahalanobis distance&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;rotate: $V^\top \mathbf{x}$&lt;/td&gt;
&lt;td&gt;rotate: $V^\top e$&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;scale: multiply by $\sigma_i$&lt;/td&gt;
&lt;td&gt;scale: &lt;strong&gt;divide&lt;/strong&gt; by $\sqrt{\lambda_i}$&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;rotate again: $U(\cdot)$&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;no third rotation&lt;/strong&gt; — take the norm instead&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;output&lt;/td&gt;
&lt;td&gt;a transformed &lt;strong&gt;vector&lt;/strong&gt; $\mathbf{y}$&lt;/td&gt;
&lt;td&gt;a scalar &lt;strong&gt;distance&lt;/strong&gt; $d_M$&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Two concrete differences:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;SVD stretches; Mahalanobis compresses.&lt;/strong&gt; SVD&apos;s $\Sigma_A$ multiplies by $\sigma_i$ (stretch). Mahalanobis&apos;s $\Lambda^{-1/2}$ divides by $\sqrt{\lambda_i}$ (compress) — it&apos;s &lt;em&gt;undoing&lt;/em&gt; the natural spread of the data, not applying it.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;SVD ends in a rotation; Mahalanobis ends in a norm.&lt;/strong&gt; The third SVD step rotates the scaled vector back into output space, giving another vector. Mahalanobis instead takes $|\cdot|^2$ at that point, collapsing everything to a single number.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;What this operation is actually called&lt;/h3&gt;
&lt;p&gt;The two-step transform $\Lambda^{-1/2}V^\top$ (rotate, then scale-down) is called &lt;strong&gt;whitening&lt;/strong&gt; — it takes an anisotropic (unevenly spread, correlated) distribution and turns it into an isotropic (evenly spread in all directions, uncorrelated) one:&lt;/p&gt;
&lt;p&gt;$$
\hat{e} = \Lambda^{-1/2}V^\top e \quad \Rightarrow \quad \text{Cov}(\hat{e}) = I
$$&lt;/p&gt;
&lt;p&gt;Once whitened, ordinary Euclidean distance on $\hat{e}$ &lt;em&gt;is&lt;/em&gt; the Mahalanobis distance on $e$ — because in the whitened space every direction is equally spread, so there&apos;s no shape left to correct for. That&apos;s the real punchline: &lt;strong&gt;Mahalanobis distance = Euclidean distance, computed after first eliminating the covariance.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://articulateprimate.org/images/whitening-transformation.png&quot; alt=&quot;whitening-transformation&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;Verdict&lt;/h3&gt;
&lt;p&gt;Mahalanobis distance is &lt;strong&gt;not&lt;/strong&gt; doing SVD. It is doing:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Eigendecomposition of $\Sigma$ (which happens to coincide with SVD &lt;em&gt;only&lt;/em&gt; because $\Sigma$ is symmetric PSD)&lt;/li&gt;
&lt;li&gt;A whitening transform (rotate + inverse-scale) built from that eigendecomposition&lt;/li&gt;
&lt;li&gt;A final Euclidean norm — a step SVD itself never includes&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The formulas &lt;em&gt;look&lt;/em&gt; similar because both are built from the same eigenvector/eigenvalue skeleton. But SVD&apos;s job is to decompose a general transformation into a vector output; Mahalanobis&apos;s job is to measure a distance that accounts for the shape of the data, ending in a scalar. Same skeleton, different destinations.&lt;/p&gt;
&lt;h4&gt;References&lt;/h4&gt;
&lt;p&gt;Get a detailed understanding &lt;a href=&quot;/resources/svd-mahalanobis-distance&quot;&gt;here →&lt;/a&gt;&lt;/p&gt;
</content:encoded></item><item><title>Building a $300 Research Robot — Part 0: Surviving the Setup</title><link>https://articulateprimate.org/building-a-research-robot-part-0/</link><guid isPermaLink="true">https://articulateprimate.org/building-a-research-robot-part-0/</guid><pubDate>Thu, 28 May 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Intro&lt;/h2&gt;
&lt;p&gt;Robotics people use Linux. Not because they&apos;re trying to be difficult. Just because that&apos;s how it works — ROS2 (the standard robotics middleware everyone uses) runs natively on Ubuntu, drivers for robotics hardware are written for Linux first, and if you show up to a robotics lab running Windows you&apos;ll get a very polite but firm look.&lt;/p&gt;
&lt;p&gt;So here I am. Ubuntu 24.04. Staring at a terminal. Building a robot.&lt;/p&gt;
&lt;p&gt;This post is for anyone in the same situation — technically capable, completely new to Linux, and learning by breaking things.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;What I&apos;m building&lt;/h2&gt;
&lt;p&gt;I&apos;m building a 2 DOF pan-tilt robot — a small assembly of two smart servo motors with a depth camera mounted on top. It can rotate left/right and tilt up/down. The camera watches the world, detects objects in 3D space, and the robot points toward them automatically — not because I hand-coded &quot;if object is left, rotate left&quot; but because a small AI policy &lt;em&gt;learned&lt;/em&gt; how to do it through thousands of simulated practice runs.&lt;/p&gt;
&lt;p&gt;Total hardware cost: about $239. The whole thing sits on a wooden block on my desk.&lt;/p&gt;
&lt;p&gt;The software stack is:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;ROS2 Jazzy&lt;/strong&gt; — the robotics middleware that lets different parts of the system talk to each other&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;EMOS&lt;/strong&gt; — a higher-level framework built on top of ROS2 that handles perception, planning, and control&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Ubuntu 24.04&lt;/strong&gt; — the Linux distribution everything runs on&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This post is just Part 0 — getting the environment working before touching any robot code.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Linux terms I had to learn before I could even read the error messages&lt;/h2&gt;
&lt;p&gt;Before the story, a quick glossary. Skip this if you already know Linux. If you don&apos;t — read it, because none of what follows will make sense without it.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The kernel&lt;/strong&gt; is the core of the operating system. It is the first program that is uploaded to your device and the first program that activates. It acts as a bridge between the hardware and software input outputs. It controls which program gets access to how much RAM.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;A driver&lt;/strong&gt; is software that lets your OS talk to a specific piece of hardware. It allows the kernel to communicate with the hardware components.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;DKMS (Dynamic Kernel Module Support)&lt;/strong&gt; is a system that automatically recompiles drivers whenever the kernel updates. When a new kernel version installs, DKMS tries to rebuild all hardware drivers for it. If it fails — which it did in my case — you end up with a broken state where the new kernel exists but your GPU driver doesn&apos;t work on it.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;apt&lt;/strong&gt; is Ubuntu&apos;s package manager — the tool you use to install, update, and remove software. Most commands start with &lt;code&gt;sudo apt ...&lt;/code&gt;. The &lt;code&gt;sudo&lt;/code&gt; part means &quot;run this as administrator&quot; — like right-clicking and choosing &quot;Run as Administrator&quot; on Windows.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;dpkg&lt;/strong&gt; is the lower-level tool that apt uses under the hood. You touch dpkg when apt can&apos;t fix itself and you have to go one level deeper.&lt;/p&gt;
&lt;p&gt;When I say &lt;strong&gt;&quot;broken packages,&quot;&lt;/strong&gt; I mean packages that are partially installed or stuck in a state where the system doesn&apos;t know what to do with them.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;HWE (Hardware Enablement Stack)&lt;/strong&gt; is Ubuntu&apos;s system for keeping your kernel and drivers up to date on LTS (Long Term Support) releases. HWE kernels are newer than the default and pulled in automatically during upgrades. This is what pulled in kernel 6.17 without me asking for it.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;My machine&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Laptop:&lt;/strong&gt; Dell G3 3500&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;GPU:&lt;/strong&gt; NVIDIA GeForce GTX 1650 (4GB VRAM)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;OS:&lt;/strong&gt; Ubuntu 24.04.3 LTS&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;ROS2:&lt;/strong&gt; Jazzy (pre-installed)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;NVIDIA Driver:&lt;/strong&gt; 580.65.06 (when I started)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;CUDA:&lt;/strong&gt; 13.0&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;The plan&lt;/h2&gt;
&lt;p&gt;Simple: update the system, then install EMOS.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo apt update &amp;amp;&amp;amp; sudo apt upgrade -y
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;What this does: &lt;code&gt;sudo apt update&lt;/code&gt; tells the system to check for newer versions of installed software. &lt;code&gt;sudo apt upgrade -y&lt;/code&gt; actually downloads and installs those updates. The &lt;code&gt;-y&lt;/code&gt; means &quot;say yes to everything automatically.&quot;&lt;/p&gt;
&lt;p&gt;This should have taken 5 minutes. Instead it took an hour.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;What broke: kernel 6.17 vs NVIDIA 580&lt;/h2&gt;
&lt;p&gt;So the upgrade finished and I had a wall of red text staring back at me. Great start.&lt;/p&gt;
&lt;p&gt;What happened, as far as I can tell: the upgrade quietly pulled in a new kernel — version 6.17 — as part of the routine update. I didn&apos;t ask for it, it just came along for the ride. The kernel is essentially the core of the operating system, the thing that talks to all your hardware. When it updates, all your hardware drivers need to be rebuilt for it — including the NVIDIA GPU driver. That rebuilding process is handled automatically by something called DKMS.&lt;/p&gt;
&lt;p&gt;Now here&apos;s something that confused me at first — if 6.17 was broken, why was my laptop still running fine? The answer is that Linux doesn&apos;t replace the old kernel when a new one installs. It sits alongside it. Both live on your disk at the same time. When you boot up, a menu called GRUB appears (usually just for a second before it auto-selects) and picks which kernel to load. By default it picks the newest one.&lt;/p&gt;
&lt;p&gt;Think of it like downloading an app update on your phone that gets stuck halfway. The old version still works. But now your storage has both the old and the broken new version sitting on it, and the app store is confused about what state everything is in.&lt;/p&gt;
&lt;p&gt;So in my case:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Kernel 6.14 was already installed and working&lt;/li&gt;
&lt;li&gt;The upgrade downloaded and partially installed kernel 6.17 alongside it&lt;/li&gt;
&lt;li&gt;DKMS failed during that installation — 6.17 never fully completed&lt;/li&gt;
&lt;li&gt;On reboot, GRUB tried to load 6.17 since it was newest — but fell back to 6.14 because the installation was broken&lt;/li&gt;
&lt;li&gt;I ended up running 6.14 while 6.17 sat half-installed on the disk doing nothing except breaking apt
DKMS failed with this:&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;Error! Bad return status for module build on kernel: 6.17.0-29-generic (x86_64)
dkms autoinstall on 6.17.0-29-generic/x86_64 failed for nvidia(10)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Honestly I&apos;m still not 100% sure why. My best guess is that the kernel headers — the instruction manual DKMS needs to rebuild the driver — arrived in the wrong order during the upgrade and the rebuild started before everything was ready. It probably wasn&apos;t a genuine incompatibility, because later when I cleaned everything up the exact same driver compiled fine on kernel 6.17 without any changes. So something in the installation process just went sideways.&lt;/p&gt;
&lt;p&gt;The result was four packages stuck in limbo — half installed, half not, blocking each other:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Errors were encountered while processing:
 linux-headers-6.17.0-29-generic
 linux-headers-generic-hwe-24.04
 linux-generic-hwe-24.04
 linux-image-6.17.0-29-generic
E: Sub-process /usr/bin/dpkg returned an error code (1)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The one good thing: my laptop was still running on the old kernel (6.14) so nothing was actually broken yet. The GPU still worked. I just had this mess sitting in the background that needed sorting out before I could do anything else.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Attempt 1: the wrong fix&lt;/h2&gt;
&lt;p&gt;My first instinct was &lt;code&gt;sudo apt --fix-broken install&lt;/code&gt; followed by &lt;code&gt;sudo apt autoremove&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;apt --fix-broken install&lt;/code&gt; tries to resolve broken dependencies automatically. Reasonable first step.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;apt autoremove&lt;/code&gt; removes packages that are &quot;no longer needed.&quot; Sounds safe.&lt;/p&gt;
&lt;p&gt;It wasn&apos;t.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;autoremove&lt;/code&gt; looked at Ubuntu&apos;s dependency graph — which was unreliable because packages were in a broken state — and decided to remove:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The working NVIDIA 575 drivers&lt;/li&gt;
&lt;li&gt;The older kernel &lt;code&gt;6.14.0-29&lt;/code&gt;
Quick detour: a dependency graph is just Ubuntu&apos;s way of tracking which software needs which other software to work. A video player might depend on an audio library, which depends on a codec package, and so on. Ubuntu keeps a map of all these relationships so when you install or remove something it knows what else to bring along or clean up. When packages are broken or half-installed, that map becomes unreliable — like Google Maps that doesn&apos;t know a road is closed. &lt;code&gt;autoremove&lt;/code&gt; trusted the map, the map was wrong, and it removed things it shouldn&apos;t have.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Things I didn&apos;t ask it to remove. Things that were working fine.&lt;/p&gt;
&lt;p&gt;The 6.17 kernel stayed because it had a dependency holding it in place:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;linux-image-generic-hwe-24.04 depends on linux-image-6.17.0-29-generic
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;So now I had: broken 6.17 kernel, partially removed NVIDIA drivers, and a bigger mess than before.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Lesson: never run &lt;code&gt;apt autoremove&lt;/code&gt; when packages are in a broken state.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;What that means in plain terms: &lt;code&gt;autoremove&lt;/code&gt; is a cleanup tool, not a repair tool. It&apos;s meant to be run when everything is healthy and you just want to tidy up unused packages. Running it when things are already broken is like asking someone to clean your house while it&apos;s on fire — they&apos;ll start throwing things out without knowing what&apos;s important and what isn&apos;t.&lt;/p&gt;
&lt;p&gt;What I should have done instead:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Step 1 — Don&apos;t touch autoremove at all.&lt;/strong&gt; When you see broken packages, fix them first.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Step 2 — Run these repair commands first:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo dpkg --configure -a
sudo apt --fix-broken install -y
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;dpkg --configure -a&lt;/code&gt; tells dpkg to finish anything it left half-done. &lt;code&gt;apt --fix-broken install&lt;/code&gt; tells apt to look at what&apos;s broken and try to resolve it cleanly. These are the &quot;try to repair yourself&quot; commands.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Step 3 — Only run autoremove after those succeed.&lt;/strong&gt; Once the system is healthy, autoremove can safely figure out what&apos;s genuinely unused.&lt;/p&gt;
&lt;p&gt;The short version: see broken packages → run &lt;code&gt;dpkg --configure -a&lt;/code&gt; and &lt;code&gt;apt --fix-broken install&lt;/code&gt; → only then consider cleanup.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Attempt 2: trying to force-remove the 6.17 kernel&lt;/h2&gt;
&lt;p&gt;Since normal removal was blocked by dependencies, I tried force-removal:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo dpkg --remove --force-remove-reinstreq linux-image-6.17.0-29-generic linux-headers-6.17.0-29-generic linux-headers-generic-hwe-24.04 linux-generic-hwe-24.04
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;--force-remove-reinstreq&lt;/code&gt; removes packages stuck in a &quot;needs reinstall&quot; broken state, ignoring dependency checks. This got most of them — but not the image:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;dpkg: dependency problems prevent removal of linux-image-6.17.0-29-generic:
 linux-image-generic-hwe-24.04 depends on linux-image-6.17.0-29-generic.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The meta-package &lt;code&gt;linux-image-generic-hwe-24.04&lt;/code&gt; was still holding the kernel image hostage. I had to remove that first.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;The actual fix — in order&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Step 1 — Remove the meta-package holding the kernel in place:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo dpkg --remove --force-remove-reinstreq linux-image-generic-hwe-24.04
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A meta-package is a package that doesn&apos;t contain any software itself — it just declares dependencies on other packages. &lt;code&gt;linux-image-generic-hwe-24.04&lt;/code&gt; existed only to pull in &lt;code&gt;linux-image-6.17.0-29-generic&lt;/code&gt;. Removing it broke the dependency chain.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Step 2 — Now remove the kernel image:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo dpkg --remove --force-remove-reinstreq linux-image-6.17.0-29-generic
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Step 3 — Clean up dpkg&apos;s internal state:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo dpkg --configure -a
sudo apt --fix-broken install -y
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;dpkg --configure -a&lt;/code&gt; finishes configuring any packages that got interrupted mid-install. &lt;code&gt;apt --fix-broken install&lt;/code&gt; resolves remaining dependency problems. Run both every time after force-removing packages.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Step 4 — Reinstall NVIDIA drivers:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo apt install nvidia-driver-580 -y
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This time, apt pulled in version &lt;code&gt;580.159.03&lt;/code&gt; — a newer release that actually supports kernel 6.17. The original upgrade had been trying to install this all along. The broken dpkg state prevented it from completing. Clearing the state let it finish.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Step 5 — Reboot:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo reboot
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;After reboot — everything working:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;uname -r
# 6.17.0-29-generic ✓

nvidia-smi
# NVIDIA-SMI 580.159.03   Driver Version: 580.159.03   CUDA Version: 13.0 ✓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The irony: Think about what the upgrade was actually trying to do — bring in kernel 6.17 and a newer NVIDIA driver that supports it. That&apos;s the correct thing to do. The problem wasn&apos;t the plan, it was the execution. Packages installed in the wrong order, DKMS ran before the kernel headers were ready, and dpkg froze halfway through. When I force-removed the broken packages and reinstalled cleanly, all I did was give that original upgrade a clean slate to finish what it started. Same kernel, same driver, same end result — just without the half-done mess in the way. Mildly annoying in hindsight. Two hours of debugging to end up exactly where the upgrade was trying to take me anyway.. Once I cleared the mess, it completed exactly as intended — new kernel, new driver, everything working.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Installing EMOS&lt;/h2&gt;
&lt;p&gt;With the system clean, EMOS installed without drama.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Install the EMOS CLI:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;curl -sSL https://raw.githubusercontent.com/automatika-robotics/emos/main/stack/emos-cli/scripts/install.sh | sudo bash
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;curl&lt;/code&gt; downloads a file from a URL. Piping it to &lt;code&gt;sudo bash&lt;/code&gt; runs it immediately with admin privileges. This is standard practice for installing developer tools from trusted sources.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Install EMOS natively:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;emos install --mode native
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Native mode builds EMOS directly into your existing ROS2 installation. The other modes are Container (runs inside Docker, no ROS2 needed) and Pixi (isolated environment, no root required). Since I already have ROS2 Jazzy, native is cleanest.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Gotcha — ROS2 not sourced:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;First attempt failed with:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;E: Unable to locate package python3-ament-package
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This happened because the terminal didn&apos;t know where ROS2 was installed. In Linux, software installed in non-standard locations needs to be &quot;sourced&quot; — you run a script that tells the current terminal session where everything lives.&lt;/p&gt;
&lt;p&gt;Fix:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;source /opt/ros/jazzy/setup.bash
emos install --mode native
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;source&lt;/code&gt; loads a configuration file into the current terminal session. You need to run this in every new terminal before using ROS2 commands. To make it automatic so you never have to think about it again:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;echo &quot;source /opt/ros/jazzy/setup.bash&quot; &amp;gt;&amp;gt; ~/.bashrc
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This adds the source command to &lt;code&gt;~/.bashrc&lt;/code&gt; — a file that runs automatically every time you open a new terminal. After running this once, every new terminal will have ROS2 available automatically.&lt;/p&gt;
&lt;p&gt;After sourcing, EMOS installed cleanly.&lt;/p&gt;

&lt;hr /&gt;
&lt;h2&gt;TL;DR — What to do when a kernel upgrade breaks your NVIDIA driver&lt;/h2&gt;
&lt;p&gt;If you ran &lt;code&gt;sudo apt upgrade&lt;/code&gt;, got a wall of red text, and &lt;code&gt;nvidia-smi&lt;/code&gt; stopped working, this is probably what happened and how to fix it.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Step 1 — Try the gentle fix first:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo apt --fix-broken install -y
sudo dpkg --configure -a
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Run these first. Sometimes this alone fixes it. If it doesn&apos;t, continue.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Step 2 — Find the broken kernel packages:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;dpkg -l | grep linux-image
dpkg -l | grep linux-headers
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Look for packages marked with &lt;code&gt;iF&lt;/code&gt; or &lt;code&gt;iU&lt;/code&gt; at the start of the line — those are broken. Note their names.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Step 3 — Force-remove them, meta-packages first:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo dpkg --remove --force-remove-reinstreq &amp;lt;package-name&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Meta-packages are the ones without a version number in the name — like &lt;code&gt;linux-image-generic-hwe-24.04&lt;/code&gt;. Remove those before the versioned ones like &lt;code&gt;linux-image-6.17.0-29-generic&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Step 4 — Clean up and reinstall your NVIDIA driver:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo dpkg --configure -a
sudo apt --fix-broken install -y
sudo apt install nvidia-driver-$(nvidia-smi --query-gpu=driver_version --format=csv,noheader | cut -d. -f1) -y
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Step 5 — Reboot and verify:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo reboot
# After reboot:
uname -r
nvidia-smi
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If &lt;code&gt;nvidia-smi&lt;/code&gt; shows your GPU, you&apos;re done.&lt;/p&gt;
&lt;hr /&gt;
</content:encoded></item></channel></rss>