<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>logbook</title><description>notes, research, and technical observations by geetansh jangid.</description><link>https://geetansh.tech/</link><item><title>The Equations of Diffusion, Written Out in Full</title><link>https://geetansh.tech/logbook/the-equations-of-diffusion/</link><guid isPermaLink="true">https://geetansh.tech/logbook/the-equations-of-diffusion/</guid><description>A slow walk through the mathematics of denoising diffusion, from the forward chain to the score function and the reverse SDE.</description><pubDate>Thu, 05 Nov 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Diffusion models look intimidating for an unusual reason: the ideas are not that hard, but the equations are long. A single line can contain a product over a thousand timesteps, an expectation over a Gaussian, and a gradient of a log-density, all at once.&lt;/p&gt;
&lt;p&gt;These are my notes on what those lines actually mean. I have tried to write every important equation out in full, rather than hiding the details behind &quot;after some algebra&quot;.&lt;/p&gt;
&lt;p&gt;Throughout, let $x_0 \in \mathbb{R}^d$ be a data point, $x_1, \ldots, x_T$ be a sequence of increasingly noisy versions of it, and $\theta$ be the parameters of a neural network.&lt;/p&gt;
&lt;h2&gt;The forward process&lt;/h2&gt;
&lt;p&gt;The forward process is a fixed Markov chain that adds a little Gaussian noise at each step:&lt;/p&gt;
&lt;p&gt;$$
q\left(x_t \mid x_{t-1}\right)
= \mathcal{N}!\left(x_t;\ \sqrt{1-\beta_t},x_{t-1},\ \beta_t I_d\right)
= \frac{1}{\left(2\pi\beta_t\right)^{d/2}}
\exp!\left(
-\frac{1}{2\beta_t}
\left\lVert x_t-\sqrt{1-\beta_t},x_{t-1}\right\rVert_2^2
\right),
$$&lt;/p&gt;
&lt;p&gt;where $0 &amp;lt; \beta_1 &amp;lt; \cdots &amp;lt; \beta_T &amp;lt; 1$ is a variance schedule chosen by hand. Each step keeps the signal slightly smaller and the noise slightly larger, so the chain drifts from data towards a standard normal distribution.&lt;/p&gt;
&lt;p&gt;Because every step is Gaussian and linear, the composition of all the steps is also Gaussian. Define&lt;/p&gt;
&lt;p&gt;$$
\alpha_t = 1-\beta_t,
\qquad
\bar\alpha_t = \prod_{s=1}^{t}\alpha_s .
$$&lt;/p&gt;
&lt;p&gt;Then the distribution of $x_t$ given the original data has a closed form:&lt;/p&gt;
&lt;p&gt;$$
q\left(x_t \mid x_0\right)
= \mathcal{N}!\left(x_t;\ \sqrt{\bar\alpha_t},x_0,\ \left(1-\bar\alpha_t\right)I_d\right)
= \frac{1}{\left(2\pi\left(1-\bar\alpha_t\right)\right)^{d/2}}
\exp!\left(
-\frac{\left\lVert x_t-\sqrt{\bar\alpha_t},x_0\right\rVert_2^2}
{2\left(1-\bar\alpha_t\right)}
\right).
$$&lt;/p&gt;
&lt;p&gt;Equivalently, and this is the form used in code, we can sample $x_t$ directly by reparameterisation:&lt;/p&gt;
&lt;p&gt;$$
x_t
= \sqrt{\bar\alpha_t},x_0&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;\sqrt{1-\bar\alpha_t},\varepsilon,
\qquad
\varepsilon \sim \mathcal{N}\left(0, I_d\right).
$$&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The product $\bar\alpha_t$ is the only thing we need to remember about the schedule. When $\bar\alpha_T$ is close to zero, $x_T$ is almost pure noise and carries almost no information about $x_0$.&lt;/p&gt;
&lt;h2&gt;The reverse direction is the hard part&lt;/h2&gt;
&lt;p&gt;Generation means running the chain backwards. If we knew the true reverse distribution, we could sample $x_T \sim \mathcal{N}(0,I_d)$ and denoise step by step. Bayes&apos; rule gives an expression for it:&lt;/p&gt;
&lt;p&gt;$$
q\left(x_{t-1}\mid x_t\right)
= \frac{q\left(x_t\mid x_{t-1}\right),q\left(x_{t-1}\right)}{q\left(x_t\right)}
= \frac{
q\left(x_t\mid x_{t-1}\right)
\displaystyle\int q\left(x_{t-1}\mid x_0\right)q\left(x_0\right)\mathrm{d}x_0
}{
\displaystyle\int q\left(x_t\mid x_0\right)q\left(x_0\right)\mathrm{d}x_0
}.
$$&lt;/p&gt;
&lt;p&gt;That is a perfectly correct formula and a completely useless one, because both integrals run over the unknown data distribution. We cannot evaluate it, let alone sample from it.&lt;/p&gt;
&lt;p&gt;So we approximate it. Every conditional in the reverse chain is chosen to be Gaussian,&lt;/p&gt;
&lt;p&gt;$$
p_\theta\left(x_{t-1}\mid x_t\right)
= \mathcal{N}!\left(x_{t-1};\ \mu_\theta\left(x_t,t\right),\ \Sigma_\theta\left(x_t,t\right)\right),
$$&lt;/p&gt;
&lt;p&gt;and a neural network learns the mean and variance. The question is what objective to train it with.&lt;/p&gt;
&lt;h2&gt;The evidence lower bound&lt;/h2&gt;
&lt;p&gt;We would like to maximise the log-likelihood of the data, $\log p_\theta(x_0)$. That is also intractable, so we maximise a lower bound instead. The bound comes from introducing the forward chain as a variational distribution and applying Jensen&apos;s inequality:&lt;/p&gt;
&lt;p&gt;$$
\log p_\theta\left(x_0\right)
= \log \mathbb{E}&lt;em&gt;{q\left(x&lt;/em&gt;{1:T}\mid x_0\right)}
\left[
\frac{p_\theta\left(x_{0:T}\right)}{q\left(x_{1:T}\mid x_0\right)}
\right]
\ge
\mathbb{E}&lt;em&gt;{q\left(x&lt;/em&gt;{1:T}\mid x_0\right)}
\left[
\log \frac{p_\theta\left(x_{0:T}\right)}{q\left(x_{1:T}\mid x_0\right)}
\right].
$$&lt;/p&gt;
&lt;p&gt;Writing the joint distributions out and grouping terms by timestep gives the full expansion:&lt;/p&gt;
&lt;p&gt;$$
\begin{aligned}
\mathcal{L}&lt;em&gt;{\mathrm{ELBO}}\left(\theta\right)
&amp;amp;= \mathbb{E}&lt;/em&gt;{q\left(x_{1:T}\mid x_0\right)}
\left[
\log p_\theta\left(x_{0:T}\right)
-\log q\left(x_{1:T}\mid x_0\right)
\right] \
&amp;amp;= \mathbb{E}&lt;em&gt;{q}\left[
\log p\left(x_T\right)
+\sum&lt;/em&gt;{t=1}^{T}\log p_\theta\left(x_{t-1}\mid x_t\right)
-\sum_{t=1}^{T}\log q\left(x_t\mid x_{t-1}\right)
\right] \
&amp;amp;= \underbrace{
\mathbb{E}&lt;em&gt;{q}\left[\log p\left(x_T\right)-\log q\left(x_T\mid x_0\right)\right]
}&lt;/em&gt;{\text{prior term}}
-\sum_{t=2}^{T}
\underbrace{
\mathbb{E}&lt;em&gt;{q}\left[
\log \frac{q\left(x&lt;/em&gt;{t-1}\mid x_t,x_0\right)}
{p_\theta\left(x_{t-1}\mid x_t\right)}
\right]
}&lt;em&gt;{\text{denoising term}}
+\underbrace{
\mathbb{E}&lt;/em&gt;{q}\left[\log p_\theta\left(x_0\mid x_1\right)\right]
}_{\text{reconstruction term}} .
\end{aligned}
$$&lt;/p&gt;
&lt;p&gt;The first term has no learnable parameters: it measures how far the final noisy state is from the prior, and it is small by construction if the schedule is good. The third term is a single denoising step at the end. The interesting part is the sum in the middle.&lt;/p&gt;
&lt;p&gt;Each term in that sum is a KL divergence between two Gaussians, because the forward posterior $q(x_{t-1}\mid x_t,x_0)$ is also Gaussian. Deriving it from Bayes&apos; rule gives&lt;/p&gt;
&lt;p&gt;$$
q\left(x_{t-1}\mid x_t,x_0\right)
= \mathcal{N}!\left(x_{t-1};\ \tilde\mu_t\left(x_t,x_0\right),\ \tilde\beta_t I_d\right),
$$&lt;/p&gt;
&lt;p&gt;with&lt;/p&gt;
&lt;p&gt;$$
\tilde\mu_t\left(x_t,x_0\right)
= \frac{\sqrt{\bar\alpha_{t-1}},\beta_t}{1-\bar\alpha_t},x_0&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;\frac{\sqrt{\alpha_t}\left(1-\bar\alpha_{t-1}\right)}{1-\bar\alpha_t},x_t,
\qquad
\tilde\beta_t
= \frac{1-\bar\alpha_{t-1}}{1-\bar\alpha_t},\beta_t .
$$&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;For reference, the KL divergence between two multivariate Gaussians is&lt;/p&gt;
&lt;p&gt;$$
D_{\mathrm{KL}}!\left(
\mathcal{N}\left(\mu_q,\Sigma_q\right)
\ \Vert&lt;br /&gt;\mathcal{N}\left(\mu_p,\Sigma_p\right)
\right)
= \frac{1}{2}\left[
\log\frac{\det\Sigma_p}{\det\Sigma_q}&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;d&lt;/li&gt;
&lt;/ul&gt;
&lt;ul&gt;
&lt;li&gt;\operatorname{tr}!\left(\Sigma_p^{-1}\Sigma_q\right)&lt;/li&gt;
&lt;li&gt;\left(\mu_p-\mu_q\right)^{\top}
\Sigma_p^{-1}
\left(\mu_p-\mu_q\right)
\right].
$$&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In our case both covariances are multiples of the identity, so the log-determinant and trace terms are constants with respect to $\theta$. Minimising the KL divergence therefore reduces to matching the means:&lt;/p&gt;
&lt;p&gt;$$
\min_\theta\ D_{\mathrm{KL}}!\left(
q\left(x_{t-1}\mid x_t,x_0\right)
\ \Vert&lt;br /&gt;p_\theta\left(x_{t-1}\mid x_t\right)
\right)
\quad\Longleftrightarrow\quad
\min_\theta\ \left\lVert
\mu_\theta\left(x_t,t\right)
-\tilde\mu_t\left(x_t,x_0\right)
\right\rVert_2^2 .
$$&lt;/p&gt;
&lt;p&gt;That is the whole trick. A scary sum of KL divergences collapses into a handful of mean-squared errors.&lt;/p&gt;
&lt;h2&gt;Predicting noise instead of means&lt;/h2&gt;
&lt;p&gt;The mean $\tilde\mu_t$ depends on $x_0$, which we do not have at sampling time. But the reparameterisation lets us trade $x_0$ for the noise $\varepsilon$ that produced $x_t$:&lt;/p&gt;
&lt;p&gt;$$
x_0
= \frac{x_t-\sqrt{1-\bar\alpha_t},\varepsilon}{\sqrt{\bar\alpha_t}} .
$$&lt;/p&gt;
&lt;p&gt;Substituting that into $\tilde\mu_t$ and simplifying gives the mean in terms of $x_t$ and $\varepsilon$:&lt;/p&gt;
&lt;p&gt;$$
\mu_\theta\left(x_t,t\right)
= \frac{1}{\sqrt{\alpha_t}}\left(
x_t&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;\frac{\beta_t}{\sqrt{1-\bar\alpha_t}},
\varepsilon_\theta\left(x_t,t\right)
\right).
$$&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;So instead of training a network to output a mean, we train it to output the noise that was added. After dropping the per-timestep weighting — which empirically works better than the exact ELBO — the objective becomes the now-familiar denoising loss:&lt;/p&gt;
&lt;p&gt;$$
\mathcal{L}&lt;em&gt;{\mathrm{simple}}\left(\theta\right)
= \mathbb{E}&lt;/em&gt;{t\sim\mathcal{U}{1,\ldots,T}},
\mathbb{E}&lt;em&gt;{x_0\sim q\left(x_0\right)},
\mathbb{E}&lt;/em&gt;{\varepsilon\sim\mathcal{N}\left(0,I_d\right)}
\left[
\left\lVert
\varepsilon&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;\varepsilon_\theta!\left(
\sqrt{\bar\alpha_t},x_0
+\sqrt{1-\bar\alpha_t},\varepsilon,&lt;br /&gt;t
\right)
\right\rVert_2^2
\right].
$$&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In code, that is almost embarrassingly short:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;for x0 in loader:
    t = torch.randint(1, T + 1, (x0.shape[0],), device=x0.device)
    eps = torch.randn_like(x0)
    abar = alpha_bar[t].view(-1, 1)
    xt = abar.sqrt() * x0 + (1 - abar).sqrt() * eps
    loss = ((eps - model(xt, t)) ** 2).mean()
    loss.backward()
    optimizer.step()
    optimizer.zero_grad()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Everything else in a diffusion paper is, in some sense, an elaboration of this loop.&lt;/p&gt;
&lt;h2&gt;The score function and the continuous limit&lt;/h2&gt;
&lt;p&gt;There is a second way to view the same objective, and it generalises more cleanly. The &lt;strong&gt;score&lt;/strong&gt; of a distribution is the gradient of its log-density:&lt;/p&gt;
&lt;p&gt;$$
s_t\left(x\right)
= \nabla_x \log q_t\left(x\right).
$$&lt;/p&gt;
&lt;p&gt;The score points in the direction in which the density grows fastest, which is exactly the direction a sample should move to become more likely. For a Gaussian perturbation of data, the score has a closed form in terms of the noise:&lt;/p&gt;
&lt;p&gt;$$
\nabla_x\log q_t\left(x\right)
= \frac{\sqrt{\bar\alpha_t},
\mathbb{E}\left[x_0\mid x_t=x\right]-x}
{1-\bar\alpha_t}
= -\frac{\mathbb{E}\left[\varepsilon\mid x_t=x\right]}
{\sqrt{1-\bar\alpha_t}} .
$$&lt;/p&gt;
&lt;p&gt;This identity is often called Tweedie&apos;s formula. It says that denoising and score estimation are the same task up to a scale factor. Training a network $s_\theta(x,t)$ to match the score gives the denoising score matching objective:&lt;/p&gt;
&lt;p&gt;$$
\mathcal{L}&lt;em&gt;{\mathrm{DSM}}\left(\theta\right)
= \mathbb{E}&lt;/em&gt;{t}\left[
\lambda\left(t\right)
\mathbb{E}&lt;em&gt;{x_0}
\mathbb{E}&lt;/em&gt;{\varepsilon}
\left[
\left\lVert
s_\theta!\left(
\sqrt{\bar\alpha_t},x_0
+\sqrt{1-\bar\alpha_t},\varepsilon,&lt;br /&gt;t
\right)
+\frac{\varepsilon}{\sqrt{1-\bar\alpha_t}}
\right\rVert_2^2
\right]
\right],
$$&lt;/p&gt;
&lt;p&gt;where $\lambda(t)$ is a positive weighting. With $\lambda(t) = 1-\bar\alpha_t$ this is equivalent to $\mathcal{L}_{\mathrm{simple}}$.&lt;/p&gt;
&lt;p&gt;The continuous-time limit is where the notation becomes genuinely heavy. Let the number of steps go to infinity and replace the discrete schedule with a continuous function $\beta(t)$. The forward process becomes a stochastic differential equation,&lt;/p&gt;
&lt;p&gt;$$
\mathrm{d}x
= -\frac{1}{2}\beta\left(t\right)x,\mathrm{d}t&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;\sqrt{\beta\left(t\right)},\mathrm{d}w,
$$&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;where $w$ is a standard Wiener process. The density $q_t(x)$ then evolves according to the Fokker–Planck equation:&lt;/p&gt;
&lt;p&gt;$$
\frac{\partial}{\partial t}q_t\left(x\right)
= \frac{\beta\left(t\right)}{2}
\left[
\nabla\cdot\left(x,q_t\left(x\right)\right)
+\Delta q_t\left(x\right)
\right],
$$&lt;/p&gt;
&lt;p&gt;with $\Delta$ the Laplacian. Anderson&apos;s reverse-time result says that the time-reversed process is again a diffusion, with an extra drift term built from the score:&lt;/p&gt;
&lt;p&gt;$$
\mathrm{d}x
= \left[
-\frac{1}{2}\beta\left(t\right)x
-\beta\left(t\right)\nabla_x\log q_t\left(x\right)
\right]\mathrm{d}t&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;\sqrt{\beta\left(t\right)},\mathrm{d}\bar w,
$$&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;where $\bar w$ is a Wiener process running backwards in time. This is the continuous version of the reverse chain, and it makes the role of the score explicit: it is the only unknown quantity in the drift.&lt;/p&gt;
&lt;p&gt;There is also a deterministic counterpart. Removing the noise from the reverse SDE leaves the probability flow ODE,&lt;/p&gt;
&lt;p&gt;$$
\frac{\mathrm{d}x}{\mathrm{d}t}
= -\frac{1}{2}\beta\left(t\right)
\left[
x
+\nabla_x\log q_t\left(x\right)
\right],
$$&lt;/p&gt;
&lt;p&gt;which transports the same marginal densities $q_t(x)$ without any stochasticity. Sampling with a numerical ODE solver is often faster and more reproducible than simulating the SDE, at the cost of some sample diversity.&lt;/p&gt;
&lt;h2&gt;Guidance&lt;/h2&gt;
&lt;p&gt;Most practical systems do not sample from the unconditional model. They condition on a prompt $c$ and then exaggerate the conditioning. Classifier-free guidance does this by combining a conditional and an unconditional prediction:&lt;/p&gt;
&lt;p&gt;$$
\tilde{\varepsilon}&lt;em&gt;\theta\left(x_t,t,c\right)
= \left(1+w\right)\varepsilon&lt;/em&gt;\theta\left(x_t,t,c\right)&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;w,\varepsilon_\theta\left(x_t,t,\varnothing\right),
\qquad w \ge 0,
$$&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;where $\varnothing$ denotes the empty condition and $w$ is the guidance scale. In score form the same operation reads&lt;/p&gt;
&lt;p&gt;$$
\tilde{\nabla}_x\log q_t\left(x\mid c\right)
= \left(1+w\right)\nabla_x\log q_t\left(x\mid c\right)&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;w,\nabla_x\log q_t\left(x\right),
$$&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;which makes the geometry clearer: guidance adds a multiple of the difference between the conditional and unconditional score fields. Large $w$ pushes samples towards regions the condition considers likely, while shrinking diversity and eventually producing oversaturated artifacts.&lt;/p&gt;
&lt;h2&gt;Sampling&lt;/h2&gt;
&lt;p&gt;The reverse chain can be sampled with the ancestral rule implied by $p_\theta$, but many implementations use DDIM, which allows larger strides and a deterministic path. One DDIM update is&lt;/p&gt;
&lt;p&gt;$$
x_{t-1}
= \sqrt{\bar\alpha_{t-1}},
\underbrace{
\frac{x_t-\sqrt{1-\bar\alpha_t},
\varepsilon_\theta\left(x_t,t\right)}
{\sqrt{\bar\alpha_t}}
}_{\hat{x}_0}&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;\sqrt{1-\bar\alpha_{t-1}-\sigma_t^2},
\varepsilon_\theta\left(x_t,t\right)&lt;/li&gt;
&lt;li&gt;\sigma_t,\varepsilon,
$$&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;where $\varepsilon\sim\mathcal{N}(0,I_d)$ and $\sigma_t$ controls how much randomness is injected. Setting $\sigma_t=0$ gives the deterministic DDIM sampler; setting $\sigma_t$ to the forward posterior standard deviation recovers the ancestral sampler.&lt;/p&gt;
&lt;h2&gt;What the equations are really saying&lt;/h2&gt;
&lt;p&gt;Read quickly, the formulas above look like a pile of Gaussian integrals. Read slowly, they say something simple.&lt;/p&gt;
&lt;p&gt;The forward process is a fixed, known corruption. The reverse process is a learned repair. The ELBO turns the impossible problem of matching a whole distribution into a sequence of easy mean-matching problems. Reparameterisation turns those mean-matching problems into noise prediction. Tweedie&apos;s formula turns noise prediction into score estimation. The continuous limit replaces the discrete chain with an SDE, and the score becomes the drift. Guidance is just arithmetic on two score fields.&lt;/p&gt;
&lt;p&gt;Every long line in a diffusion paper is one of those steps, written without shortcuts. Once the steps are visible, the length stops being intimidating and starts being useful.&lt;/p&gt;
&lt;hr /&gt;&lt;p&gt;&lt;a href=&quot;https://geetansh.tech/logbook/the-equations-of-diffusion/&quot;&gt;view this entry on logbook&lt;/a&gt;&lt;/p&gt;</content:encoded><category>AI</category><category>Machine Learning</category><category>Mathematics</category></item><item><title>Understanding Large Language Models</title><link>https://geetansh.tech/logbook/understanding-llms/</link><guid isPermaLink="true">https://geetansh.tech/logbook/understanding-llms/</guid><description>A starting point for exploring the systems behind modern language models.</description><pubDate>Wed, 04 Nov 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Large language models can feel almost mysterious from the outside. You give them a prompt, and they produce a response that can explain, summarize, translate, or write code.&lt;/p&gt;
&lt;p&gt;But underneath that experience is a fairly understandable set of ideas: tokens, numerical representations, neural networks, and a training process that gradually shapes a model&apos;s behaviour.&lt;/p&gt;
&lt;h2&gt;A simplified view&lt;/h2&gt;
&lt;p&gt;A language model receives a sequence of tokens and estimates what token is likely to come next. Repeating this process allows it to generate longer sequences.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-mermaid&quot;&gt;graph TD
    A[Text prompt] --&amp;gt; B[Tokenizer]
    B --&amp;gt; C[Token IDs]
    C --&amp;gt; D[Transformer model]
    D --&amp;gt; E[Next-token probabilities]
    E --&amp;gt; F[Generated response]
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;The central idea&lt;/h2&gt;
&lt;p&gt;At a high level, an autoregressive language model estimates:&lt;/p&gt;
&lt;p&gt;$$
P(x_1, x_2, \ldots, x_n) = \prod_{i=1}^{n} P(x_i \mid x_1, \ldots, x_{i-1})
$$&lt;/p&gt;
&lt;p&gt;This is only the beginning of the story. The interesting part is how the model learns useful representations and uses attention to relate different parts of a sequence.&lt;/p&gt;
&lt;h2&gt;A small code example&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;def greet(name: str) -&amp;gt; str:
    return f&quot;Hello, {name}!&quot;

print(greet(&quot;world&quot;))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;There is a lot more to unpack, and that&apos;s exactly what this publication is for: understanding complicated ideas one layer at a time.&lt;/p&gt;
&lt;hr /&gt;&lt;p&gt;&lt;a href=&quot;https://geetansh.tech/logbook/understanding-llms/&quot;&gt;view this entry on logbook&lt;/a&gt;&lt;/p&gt;</content:encoded><category>AI</category><category>Machine Learning</category><category>Notes</category></item><item><title>Building a Personal Site with Astro</title><link>https://geetansh.tech/logbook/building-a-personal-site-with-astro/</link><guid isPermaLink="true">https://geetansh.tech/logbook/building-a-personal-site-with-astro/</guid><description>Why a static, Markdown-first publication is still the most durable way to own your writing.</description><pubDate>Wed, 28 Oct 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Every few years I rebuild this site. The stack changes, the design changes, but the goal stays the same: a place to publish that I control, that loads quickly, and that will still make sense in a decade.&lt;/p&gt;
&lt;p&gt;This time I settled on Astro. It is a static site generator, which means the output is just HTML, CSS, and a little JavaScript. No database, no server rendering, no framework runtime shipping to the browser unless I explicitly ask for it.&lt;/p&gt;
&lt;h2&gt;Why static first&lt;/h2&gt;
&lt;p&gt;A personal site is mostly text. Text does not need a request-time server. It needs a build step, a CDN, and a domain.&lt;/p&gt;
&lt;p&gt;Static output gives you a few properties that are hard to retrofit:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Speed.&lt;/strong&gt; Pages are pre-rendered files. There is nothing to compute on request.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Durability.&lt;/strong&gt; The build output can be hosted anywhere, including a bucket, GitHub Pages, or a Raspberry Pi in a cupboard.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Debuggability.&lt;/strong&gt; When something breaks, you can open the generated HTML and see exactly what the browser received.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The trade-off is that anything dynamic has to be handled at build time or in the client. For a publication, that is almost always fine.&lt;/p&gt;
&lt;h2&gt;Content as files&lt;/h2&gt;
&lt;p&gt;The articles here are Markdown files with frontmatter. A post looks like this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;---
title: &quot;A title&quot;
description: &quot;A short summary.&quot;
date: 2026-10-28
tags: [Web, Notes]
category: Technology
draft: false
---
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The content collection schema validates that frontmatter at build time. If I forget a description or write an invalid date, the build fails instead of publishing something broken. That small guardrail is worth more than it sounds.&lt;/p&gt;
&lt;p&gt;Because everything is a file, the entire archive is versioned in Git. I can diff a post, revert a typo, or move the whole thing to another machine with a clone.&lt;/p&gt;
&lt;h2&gt;The build pipeline&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://geetansh.tech/images/architecture.svg&quot; alt=&quot;Astro Build &amp;amp; Distribution Architecture&quot; /&gt;&lt;/p&gt;
&lt;p&gt;At a high level, the pipeline is simple:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-mermaid&quot;&gt;graph LR
    A[Markdown files] --&amp;gt; B[Content collection]
    B --&amp;gt; C[Astro build]
    C --&amp;gt; D[Static HTML]
    D --&amp;gt; E[CDN]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Feeds are generated from the same source. RSS, Atom, and JSON Feed are separate endpoints that read the collection, render each entry, and emit XML or JSON. There is no second copy of the content to keep in sync.&lt;/p&gt;
&lt;h2&gt;What I would change next&lt;/h2&gt;
&lt;hr /&gt;&lt;p&gt;&lt;a href=&quot;https://geetansh.tech/logbook/building-a-personal-site-with-astro/&quot;&gt;view this entry on logbook&lt;/a&gt;&lt;/p&gt;</content:encoded><category>Web</category><category>Astro</category><category>Notes</category></item><item><title>The Linux Toolbox I Actually Use</title><link>https://geetansh.tech/logbook/the-linux-toolbox/</link><guid isPermaLink="true">https://geetansh.tech/logbook/the-linux-toolbox/</guid><description>A short list of command-line tools that have survived years of habit.</description><pubDate>Wed, 14 Oct 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;There is a certain genre of blog post that lists fifty command-line tools, most of which you install once and never touch again. This is not that. These are the tools I reach for almost daily, in roughly the order I would miss them.&lt;/p&gt;
&lt;h2&gt;Navigation and files&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;fd&lt;/code&gt; replaces &lt;code&gt;find&lt;/code&gt; for most of my searches. It is faster to type, respects &lt;code&gt;.gitignore&lt;/code&gt; by default, and its output is predictable.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;fd -e md -x wc -l
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;ripgrep&lt;/code&gt; (&lt;code&gt;rg&lt;/code&gt;) does the same for file contents. The default behaviour — recursive, ignores hidden and ignored files, prints line numbers — is exactly what I want ninety percent of the time.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;rg &quot;TODO&quot; --type ts
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;bat&lt;/code&gt; is &lt;code&gt;cat&lt;/code&gt; with syntax highlighting and line numbers. It is a small quality-of-life improvement that adds up when you read a lot of config files.&lt;/p&gt;
&lt;h2&gt;Processes and ports&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;htop&lt;/code&gt; is the classic, but I have been using &lt;code&gt;btop&lt;/code&gt; more. It is prettier, which sounds frivolous until you spend an afternoon watching a build.&lt;/p&gt;
&lt;p&gt;For ports, &lt;code&gt;ss -tulpn&lt;/code&gt; is the modern replacement for &lt;code&gt;netstat&lt;/code&gt;. When a dev server refuses to start because something already owns port 3000, this is the fastest way to find the culprit.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;ss -tulpn | rg 3000
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Git&lt;/h2&gt;
&lt;p&gt;I use &lt;code&gt;lazygit&lt;/code&gt; for staging and browsing history, and plain &lt;code&gt;git&lt;/code&gt; for everything else. The combination works because &lt;code&gt;lazygit&lt;/code&gt; makes it easy to &lt;em&gt;see&lt;/em&gt; what changed without running four separate diff commands.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;git absorb&lt;/code&gt; is the one unusual tool on this list. It takes staged changes and automatically fixes them into the right commits in your branch, which turns an untidy history into something reviewable with very little effort.&lt;/p&gt;
&lt;h2&gt;Shell&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;zoxide&lt;/code&gt; is a smarter &lt;code&gt;cd&lt;/code&gt;. It remembers directories you visit and lets you jump to them by partial name.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;z writings
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;fzf&lt;/code&gt; is the fuzzy finder that ties a lot of this together. Piping history, file names, or branches into it is a habit that is hard to give up once acquired.&lt;/p&gt;
&lt;h2&gt;The point&lt;/h2&gt;
&lt;p&gt;None of these tools are essential. &lt;code&gt;find&lt;/code&gt;, &lt;code&gt;grep&lt;/code&gt;, &lt;code&gt;cat&lt;/code&gt;, and &lt;code&gt;cd&lt;/code&gt; still work, and they are installed everywhere. The value of the replacements is that they remove small frictions — the kind you stop noticing after a while but that quietly shape how willing you are to poke around a system.&lt;/p&gt;
&lt;p&gt;A good toolbox is not the longest one. It is the one you know well enough to use without thinking.&lt;/p&gt;
&lt;hr /&gt;&lt;p&gt;&lt;a href=&quot;https://geetansh.tech/logbook/the-linux-toolbox/&quot;&gt;view this entry on logbook&lt;/a&gt;&lt;/p&gt;</content:encoded><category>Linux</category><category>Software</category><category>Notes</category></item><item><title>Notes on Attention Mechanisms</title><link>https://geetansh.tech/logbook/notes-on-attention-mechanisms/</link><guid isPermaLink="true">https://geetansh.tech/logbook/notes-on-attention-mechanisms/</guid><description>A working intuition for queries, keys, values, and why attention replaced recurrence.</description><pubDate>Wed, 30 Sep 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Attention is one of those ideas that sounds complicated until you find the right analogy, and then it sounds obvious. These are the notes I wish I had when I started reading about transformers.&lt;/p&gt;
&lt;h2&gt;The problem with recurrence&lt;/h2&gt;
&lt;p&gt;Before attention, sequence models processed tokens one at a time. A recurrent network carried a hidden state from step to step, compressing everything it had seen into a fixed-size vector. This worked, but it had two costs: it was hard to parallelise, and long-range dependencies tended to fade.&lt;/p&gt;
&lt;p&gt;Attention removes the bottleneck. Instead of forcing every token through a single running state, it lets each position look directly at every other position and decide what is relevant.&lt;/p&gt;
&lt;h2&gt;Queries, keys, and values&lt;/h2&gt;
&lt;p&gt;The standard explanation uses a library. Each token asks a question (its &lt;strong&gt;query&lt;/strong&gt;), every token advertises what it contains (its &lt;strong&gt;key&lt;/strong&gt;), and the match between them determines how much of each &lt;strong&gt;value&lt;/strong&gt; gets pulled in.&lt;/p&gt;
&lt;p&gt;Formally, scaled dot-product attention is:&lt;/p&gt;
&lt;p&gt;$$
\text{Attention}(Q, K, V) = \text{softmax}\left(\frac{QK^\top}{\sqrt{d_k}}\right)V
$$&lt;/p&gt;
&lt;p&gt;The scaling factor $\sqrt{d_k}$ matters. Without it, dot products grow with dimension, the softmax saturates, and gradients become tiny. Dividing by the square root keeps the logits in a reasonable range.&lt;/p&gt;
&lt;h2&gt;Multiple heads&lt;/h2&gt;
&lt;p&gt;A single attention operation produces one weighted average. Multiple heads let the model run several of these in parallel, each with its own learned projections.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-mermaid&quot;&gt;graph TD
    A[Input embeddings] --&amp;gt; B[Split into heads]
    B --&amp;gt; C[Head 1: local patterns]
    B --&amp;gt; D[Head 2: syntax]
    B --&amp;gt; E[Head N: long-range links]
    C --&amp;gt; F[Concatenate]
    D --&amp;gt; F
    E --&amp;gt; F
    F --&amp;gt; G[Output projection]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The heads are not assigned roles by hand. The hope is that gradient descent discovers useful specialisations on its own, and in practice it often does.&lt;/p&gt;
&lt;h2&gt;Why it works&lt;/h2&gt;
&lt;p&gt;Three properties explain most of attention&apos;s success:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Parallelism.&lt;/strong&gt; All positions are processed at once, which maps well onto modern accelerators.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Short paths.&lt;/strong&gt; Any two tokens are one operation apart, so information does not have to survive a long chain of updates.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Learned weighting.&lt;/strong&gt; The model decides relevance from data rather than relying on a fixed window.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;What it does not solve&lt;/h2&gt;
&lt;p&gt;Attention is not free. Its cost grows quadratically with sequence length, which is why long-context models invest heavily in efficient variants. It also does not give the model memory in any persistent sense; context is still bounded by what fits in the window.&lt;/p&gt;
&lt;p&gt;Understanding the mechanism is useful, but it is only one layer. The rest of the transformer — normalisation, residual streams, feed-forward blocks, and the training objective — matters just as much.&lt;/p&gt;
&lt;hr /&gt;&lt;p&gt;&lt;a href=&quot;https://geetansh.tech/logbook/notes-on-attention-mechanisms/&quot;&gt;view this entry on logbook&lt;/a&gt;&lt;/p&gt;</content:encoded><category>AI</category><category>Machine Learning</category></item><item><title>Why I Write in Markdown</title><link>https://geetansh.tech/logbook/why-i-write-in-markdown/</link><guid isPermaLink="true">https://geetansh.tech/logbook/why-i-write-in-markdown/</guid><description>Plain text is the only format I trust to survive a decade of tool changes.</description><pubDate>Sat, 12 Sep 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;I have migrated my notes through at least six applications. Each migration lost something: formatting, attachments, links, or the notes themselves. The only files that survived intact were the ones written in plain text.&lt;/p&gt;
&lt;h2&gt;Format is a commitment&lt;/h2&gt;
&lt;p&gt;Every editor wants to be your permanent home. It stores your writing in a structured format, adds features you start to depend on, and quietly makes leaving expensive. This is not malicious. It is just how products work.&lt;/p&gt;
&lt;p&gt;Markdown is the opposite of that. It is a set of conventions for annotating plain text, and the underlying file is still readable without any tool at all. If every Markdown application disappeared tomorrow, &lt;code&gt;cat&lt;/code&gt; would still show me my writing.&lt;/p&gt;
&lt;h2&gt;Portability in practice&lt;/h2&gt;
&lt;p&gt;This site is built from Markdown files. The same files open correctly in Obsidian, VS Code, GitHub, and a terminal pager. When I want to publish, a build step converts them to HTML. When I want to move, I copy a folder.&lt;/p&gt;
&lt;p&gt;The important property is not that Markdown is perfect. It is that the failure mode is graceful. A broken link or an unsupported table is a small inconvenience, not a locked door.&lt;/p&gt;
&lt;h2&gt;What plain text gives up&lt;/h2&gt;
&lt;p&gt;It gives up some things, honestly:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Rich layout.&lt;/strong&gt; Complex documents need more than headings and lists.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Embedded media.&lt;/strong&gt; Images live beside the text rather than inside it.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Structured data.&lt;/strong&gt; Tables are a compromise, and nested data is awkward.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;For prose, none of that matters much. For a spreadsheet or a slide deck, Markdown is the wrong tool, and forcing it is its own kind of lock-in.&lt;/p&gt;
&lt;h2&gt;A small set of conventions&lt;/h2&gt;
&lt;p&gt;I keep the syntax narrow on purpose:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-markdown&quot;&gt;# One title per file
## Sections for navigation
**Bold** for emphasis, *italics* sparingly
`code` for identifiers
- lists for enumerations
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Frontmatter holds metadata that the prose should not carry: title, date, tags, draft status. Everything else stays in the body.&lt;/p&gt;
&lt;h2&gt;Writing as a durable act&lt;/h2&gt;
&lt;p&gt;The real reason I care is that writing is slow. A post might take hours or weeks. Putting that effort into a format controlled by a single company feels like building on rented land. Plain text is boring, and that is exactly the appeal: boring formats do not get discontinued.&lt;/p&gt;
&lt;hr /&gt;&lt;p&gt;&lt;a href=&quot;https://geetansh.tech/logbook/why-i-write-in-markdown/&quot;&gt;view this entry on logbook&lt;/a&gt;&lt;/p&gt;</content:encoded><category>Writing</category><category>Markdown</category><category>Notes</category></item><item><title>A Weekend with a Raspberry Pi</title><link>https://geetansh.tech/logbook/a-weekend-with-a-raspberry-pi/</link><guid isPermaLink="true">https://geetansh.tech/logbook/a-weekend-with-a-raspberry-pi/</guid><description>Turning a small single-board computer into a useful home service, and the lessons that came with it.</description><pubDate>Sat, 22 Aug 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;A Raspberry Pi is one of the cheapest ways to make a vague idea concrete. This weekend&apos;s idea was a small always-on service: a private dashboard for a few home devices, reachable only from my own network.&lt;/p&gt;
&lt;h2&gt;The plan&lt;/h2&gt;
&lt;p&gt;The requirements were deliberately modest:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Run on a Pi 4 with 4 GB of RAM.&lt;/li&gt;
&lt;li&gt;Serve a small web app over the local network.&lt;/li&gt;
&lt;li&gt;Survive a power cut without manual intervention.&lt;/li&gt;
&lt;li&gt;Be quiet, cool, and easy to move.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That last point matters more than it sounds. A server you can pick up and put in a drawer is a server you will actually keep.&lt;/p&gt;
&lt;h2&gt;Setup&lt;/h2&gt;
&lt;p&gt;I used a minimal 64-bit image and did the initial configuration over SSH, without ever plugging in a monitor.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;ssh pi@raspberrypi.local
sudo apt update &amp;amp;&amp;amp; sudo apt full-upgrade -y
sudo timedatectl set-timezone Asia/Kolkata
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Setting the timezone early avoids confusing timestamps in logs later, which is the kind of thing you only notice when you are already debugging something else.&lt;/p&gt;
&lt;h2&gt;Running the service&lt;/h2&gt;
&lt;p&gt;The app itself runs in a container. Containers are not free — they add a layer and a little overhead — but they make the dependency story clean. If I break something, I delete the container and start again.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;services:
  dashboard:
    image: ghcr.io/example/dashboard:latest
    restart: unless-stopped
    ports:
      - &quot;8080:8080&quot;
    volumes:
      - ./data:/app/data
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;restart: unless-stopped&lt;/code&gt; is doing a lot of work here. After a power cut, the container comes back without me touching anything.&lt;/p&gt;
&lt;h2&gt;What went wrong&lt;/h2&gt;
&lt;p&gt;Two things, predictably.&lt;/p&gt;
&lt;p&gt;First, the Pi throttled under load because it was sitting in a closed case with no airflow. Moving it onto a small stand dropped the temperature by about fifteen degrees, which was enough to stop the throttling.&lt;/p&gt;
&lt;p&gt;Second, &lt;code&gt;raspberrypi.local&lt;/code&gt; stopped resolving after a router reboot. mDNS is convenient until it is not. I added a static DHCP lease and started using the IP address for anything that needed to be reliable.&lt;/p&gt;
&lt;h2&gt;Was it worth it&lt;/h2&gt;
&lt;p&gt;Yes, but not for the reasons I expected. The service is useful, but the real value was the reminder that small systems are understandable. There is no orchestration layer, no cloud console, no hidden abstraction. When something breaks, I can trace it from the process to the container to the kernel without leaving the machine.&lt;/p&gt;
&lt;p&gt;That is a nice property for a weekend project, and an even nicer one for a tool you plan to keep.&lt;/p&gt;
&lt;hr /&gt;&lt;p&gt;&lt;a href=&quot;https://geetansh.tech/logbook/a-weekend-with-a-raspberry-pi/&quot;&gt;view this entry on logbook&lt;/a&gt;&lt;/p&gt;</content:encoded><category>Hardware</category><category>Linux</category><category>Experiments</category></item><item><title>Reading Code You Didn&apos;t Write</title><link>https://geetansh.tech/logbook/reading-code-you-didnt-write/</link><guid isPermaLink="true">https://geetansh.tech/logbook/reading-code-you-didnt-write/</guid><description>A practical routine for getting oriented in an unfamiliar codebase without pretending to understand it all.</description><pubDate>Sat, 18 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Most of the code I work with was written by someone else. Sometimes that someone is a stranger, sometimes a teammate, and sometimes a past version of me with different priorities and a worse memory. Getting oriented quickly is a skill, and like most skills it improves with a routine.&lt;/p&gt;
&lt;h2&gt;Start at the edges&lt;/h2&gt;
&lt;p&gt;Do not begin at &lt;code&gt;main&lt;/code&gt;. Begin where the program meets the outside world: routes, CLI commands, event handlers, or exported functions. These are the entry points, and they tell you what the system is &lt;em&gt;for&lt;/em&gt; before you drown in how it works.&lt;/p&gt;
&lt;p&gt;A useful first pass is to list the entry points and write one sentence for each:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;What triggers it?&lt;/li&gt;
&lt;li&gt;What does it return or change?&lt;/li&gt;
&lt;li&gt;What does it depend on?&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That list is often enough to sketch the architecture.&lt;/p&gt;
&lt;h2&gt;Follow one path end to end&lt;/h2&gt;
&lt;p&gt;Pick the most common operation and trace it from entry point to storage and back. Read every function on the way, but do not try to understand every branch. The goal is a spine you can hang later details on.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-mermaid&quot;&gt;graph LR
    A[HTTP request] --&amp;gt; B[Router]
    B --&amp;gt; C[Handler]
    C --&amp;gt; D[Service]
    D --&amp;gt; E[Database]
    E --&amp;gt; D
    D --&amp;gt; C
    C --&amp;gt; F[Response]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Once you have one working path, the rest of the codebase becomes variations on a theme.&lt;/p&gt;
&lt;h2&gt;Read the tests as documentation&lt;/h2&gt;
&lt;p&gt;Tests are the only part of a codebase that states intended behaviour explicitly. They also tell you what the authors were afraid of breaking. A test suite is usually a better introduction than a README, because it cannot silently drift out of date without failing.&lt;/p&gt;
&lt;p&gt;Start with the tests for the path you just traced. They will confirm your understanding and reveal edge cases you would have missed.&lt;/p&gt;
&lt;h2&gt;Use the tools, but sparingly&lt;/h2&gt;
&lt;p&gt;Search and navigation tools are essential, but they can also keep you at the surface. Jumping from definition to definition feels productive while building very little understanding.&lt;/p&gt;
&lt;p&gt;A better rhythm is to read a full file, then search for one specific term. Alternate between depth and breadth instead of doing either exclusively.&lt;/p&gt;
&lt;h2&gt;Keep a map&lt;/h2&gt;
&lt;p&gt;I keep a scratch file while exploring. It contains the entry points, the main data structures, and a list of questions. The questions are the most valuable part, because they turn confusion into something I can resolve deliberately.&lt;/p&gt;
&lt;p&gt;By the end of a day, the map is usually messy and half wrong. That is fine. The act of writing it is what turns a pile of files into a system you can reason about.&lt;/p&gt;
&lt;hr /&gt;&lt;p&gt;&lt;a href=&quot;https://geetansh.tech/logbook/reading-code-you-didnt-write/&quot;&gt;view this entry on logbook&lt;/a&gt;&lt;/p&gt;</content:encoded><category>Software</category><category>Notes</category></item></channel></rss>