Susam Pal https://susam.net/ Susam's Feed Wander Console 0.5.0 https://susam.net/code/news/wander/0.5.0.html wnzfv Sun, 19 Apr 2026 00:00:00 +0000 Wander Console 0.5.0 is out. It is the fifth release of Wander, a small, decentralised, self-hosted web console that lets visitors to your website explore interesting websites and pages recommended by a community of independent website owners. To try it, go to susam.net/wander/. To learn more about how it works and how to set it up on your website, see the project README.

A screenshot of Wander Console
A screenshot of Wander Console

The big feature in this release is a built-in console network crawler. To try the console crawler, go to susam.net/wander/ > Console > Crawl. It performs a breadth-first search (BFS) traversal of the Wander network and lists all discovered consoles and page recommendations in a single pane.

A screenshot of Wander Console Crawler
A screenshot of Wander Console Crawler

If you have set up a Wander Console instance for yourself on your website, I recommend upgrading to the latest version to use this feature. It is fun to find out just how many Wander consoles belong to your neighbourhood. To upgrade, you only need to download the Wander Console bundle mentioned here and replace your existing Wander index.html with the new one.

If you own a personal website but have not set up a Wander Console yet, I suggest that you consider setting one up for yourself. You can see what it looks like by visiting mine at /wander/. To set up your own, follow these instructions from the README: Install. It just involves copying two files to your web server. It is as simple as that.

Read on website | #web | #technology

]]>
Wander Console 0.4.0 https://susam.net/code/news/wander/0.4.0.html wnzfz Sat, 04 Apr 2026 00:00:00 +0000 Wander Console 0.4.0 is the fourth release of Wander, a small, decentralised, self-hosted web console that lets visitors to your website explore interesting websites and pages recommended by a community of independent website owners. To try it, go to susam.net/wander/.

A screenshot of Wander Console 0.4.0
A screenshot of Wander Console 0.4.0

This release brings a few small additions as well as a few minor fixes. You can find the previous release pages here: /code/news/wander/. The sections below discuss the current release.

Contents

Wildcard Patterns

Wander Console now supports wildcard patterns in ignore lists. An asterisk (*) anywhere in an ignore pattern matches zero or more characters in URLs. For example, an ignore pattern like https://*.midreadpopup.example/ can be used to ignore URLs such as this:

  • https://alice.midreadpopup.example/
  • https://bob.jones.midreadpopup.example/

These ignore patterns are specified in a console's wander.js file. These are very important for providing a good wandering experience to visitors. The owner of a console decides what links they want to ignore in their ignore patterns. The ignore list typically contains commercial websites that do not fit the spirit of the small web, as well as defunct or incompatible websites that do not load in the console. A console with a well maintained ignore list ensures that a visitor to that console has a lower likelihood of encountering commercial or broken websites.

For a complete description of the ignore patterns, see Customise Ignore List.

The 'via' Query Parameter

By popular demand, Wander now adds a via= query parameter while loading a recommended web page in the console. The value of this parameter is the console that loaded the recommended page. For example, if you encounter midnight.pub/ while using the console at susam.net/wander/, the console loads the page using the following URL:

https://midnight.pub/?via=https://susam.net/wander/

This allows the owner of the recommended website to see, via their access logs, that the visit originated from a Wander Console. While this is the default behaviour now, it can be customised in two ways. The value can be changed from the full URL of the Wander Console to a small identifier that identifies the version of Wander Console used (e.g. via=wander-0.4.0). The query parameter can be disabled as well. For more details, see Customise 'via' Parameter.

Console Picker Algorithm

In earlier versions of the console, when a visitor came to your console to explore the Wander network, it picked the first recommendation from the list of recommended pages in it (i.e. your wander.js file). But subsequent recommendations came from your neighbours' consoles and then their neighbours' consoles and so on recursively. Your console (the starting console) was not considered again unless some other console in the network linked back to your console.

A common way to ensure that your console was also considered in subsequent recommendations too was to add a link to your console in your own console (i.e. in your wander.js). Yes, this created self-loops in the network but this wasn't considered a problem. In fact, this was considered desirable, so that when the console picked a console from the pool of discovered consoles to find the next recommendation, it considered itself to be part of the pool. This workaround is no longer necessary.

Since version 0.4.0 of Wander, each console will always consider itself to be part of the pool from which it picks consoles. This means that the web pages recommended by the starting console have a fair chance of being picked for the next web page recommendation.

The Wander Console loads the recommended web pages in an <iframe> element that has sandbox restrictions enabled. The sandbox properties restrict the side effects the loaded web page can have on the parent Wander Console window. For example, with the sandbox restrictions enabled, a loaded web page cannot redirect the parent window to another website. In fact, these days most modern browsers block this and show a warning anyway, but we also block this at a sandbox level too in the console implementation.

It turned out that our aggressive sandbox restrictions also blocked legitimate websites from opening a link in a new tab. We decided that opening a link in a new tab is harmless behaviour and we have relaxed the sandbox restrictions a little bit to allow it. Of course, when you click such a link within Wander console, the link will open in a new tab of your web browser (not within Wander Console, as the console does not have any notion of tabs).

Community

Although I developed this project on a whim, one early morning while taking a short break from my ongoing studies of algebraic graph theory, the subsequent warm reception on Hacker News and Lobsters has led to a growing community of Wander Console owners. There are two places where the community hangs out at the moment:

  • New consoles are announced in this thread on Codeberg: Share Your Wander Console.
  • We also have an Internet Relay Chat (IRC) channel named #wander on the Libera IRC network. This is a channel for people who enjoy building personal websites and want to talk to each other. You are welcome to join this channel, share your console URL, link to your website or recent articles as well as share links to other non-commercial personal websites.

If you own a personal website but you have not set up a Wander Console yet, I suggest that you consider setting one up for yourself. You can see what it looks like by visiting mine at /wander/. To set up your own, follow these instructions: Install. It just involves copying two files to your web server. It is about as simple as it gets.

Read on website | #web | #technology

]]>
Mar '26 Notes https://susam.net/26c.html mtsnt Mon, 30 Mar 2026 00:00:00 +0000 This is my third set of monthly notes for this year. In these notes, I capture various interesting facts and ideas I have stumbled upon during the month. Like in the last two months, I have been learning and exploring algebraic graph theory. The two main books I have been reading are Algebraic Graph Theory by Godsil and Royle and Algebraic Graph Theory, 2nd ed. by Norman Biggs. Much of what appears here comes from my study of these books as well as my own explorations and attempts to distill the ideas. This post is quite heavy on mathematics but there are some non-mathematical, computing-related notes towards the end.

The level of exposition is quite uneven throughout these notes. After all, they aren't meant to be a polished exposition but rather notes I take for myself. In some places I build concepts from first principles, while in others I gloss over details and focus only on the main results.

Sometime during the second half of the month, I also developed an open-source tool called Wander Console on a whim. It lets anyone with a website host a decentralised web console that recommends interesting websites from the 'small web' of independent, personal websites. Check my console here: wander/.

Although the initial version was ready after just about 1.5 hours of development during a break I was taking from studying algebraic graph theory, the subsequent warm reception on Hacker News and a growing community around it, along with the resulting feature requests and bug fixes, ended up taking more time than I had anticipated, at the expense of my algebraic graph theory studies. With a full-time job, it becomes difficult to find time for both open source development and mathematical studies. But eventually, I managed to return to my studies while making Wander Console improvements only occasionally during breaks from my studies.

Contents

  1. Group Theory
    1. Permutation
    2. Group Homomorphism
    3. Group Homomorphism Preserves Identity
    4. Group Homomorphism Preserves Inverses
    5. Image of a Group Homomorphism
    6. Group Monomorphism
      1. Standard Proof
      2. Alternate Proof
    7. Permutation Representation
    8. Group Action
      1. Why Right Action?
      2. Example 1
      3. Example 2
    9. Group Actions Induce Permutations
    10. Group Actions Determine Permutation Representations
    11. Permutation Representations Determine Group Actions
    12. Bijection Between Group Actions and Permutation Representations
    13. Orbits
    14. Stabilisers
    15. Orbit-Stabiliser Theorem
    16. Faithful Actions
    17. Semiregular Actions
    18. Transitive Actions
    19. Conjugacy
      1. Conjugation as Group Action
      2. Right Conjugation vs Left Conjugation
    20. Conjugate Subgroups
    21. Conjugacy of Stabilisers
  2. Algebraic Graph Theory
    1. Stabiliser Index
    2. Strongly Connected Directed Graph
    3. Shunting
    4. Automorphisms Preserve Successor Relation
    5. Test of \( s \)-arc Transitivity
    6. Moore Graphs
    7. Generalised Polygons
  3. Computing
    1. Select Between Lines, Inclusive
    2. Select Between Lines, Exclusive
    3. Signing and Verification with SSH Key
    4. Block IP Address with nftables
    5. Debian Logrotate Setup

Group Theory

Permutation

A permutation of a set \( X \) is a bijection \( X \to X . \)

For example, take \( X = \{ 1, 2, 3, 4, 5, 6 \} \) and define the map \[ \pi : X \to X; \; x \mapsto 1 + ((x + 1) \bmod 6). \] This maps \begin{align*} 1 &\mapsto 3, \\ 2 &\mapsto 4, \\ 3 &\mapsto 5, \\ 4 &\mapsto 6, \\ 5 &\mapsto 1, \\ 6 &\mapsto 2. \end{align*}

We can describe permutations more succinctly using cycle notation. The cycle notation of a permutation \( \pi \) consists of one or more sequences written next to each other such that the sequences are pairwise disjoint and \( \alpha \) maps each element in a sequence to the next element on its right. If the sequence is finite, then \( \alpha \) maps the final element back to the first one. Any element that does not appear in any sequence is mapped to itself. For example the cycle notation for the above permutation is \( (1 3 5)(2 4 6). \)

Group Homomorphism

A map \( \phi : G \to H \) from a group \( (G, \ast) \) to a group \( (H, \cdot) \) is a group homomorphism if, for all \( x, y \in G, \) \[ \phi(x \ast y) = \phi(x) \cdot \phi(y). \] We say that a group homomorphism is a map between groups that preserves the group operation. In other words, a group homomorphism sends products in \( G \) to products in \( H . \) For example, consider the groups \( (\mathbb{Z}, +) \) and \( (\mathbb{Z}_3, +). \) Then the map \[ \phi : \mathbb{Z} \to \mathbb{Z}_3; \; n \mapsto n \bmod 3 \] is a group homomorphism because \[ \phi(x + y) = (x + y) \bmod 3 = (x \bmod 3) + (y \bmod 3) = \phi(x) + \phi(y) \] for all \( x, y \in \mathbb{Z}. \) As another example, consider the groups \( (\mathbb{R}_{\gt 0}, \times) \) and \( (\mathbb{R}, +). \) Then the map \[ \log : \mathbb{R}_{\gt 0} \to \mathbb{R} \] is a group homomorphism because \[ \log(m \times n) = \log m + \log n. \] Note that a group homomorphism preserves the identity element. For example, \( 1 \) is the identity element of \( (\mathbb{R}_{\gt 0}, \times) \) and \( 0 \) is the identity element of \( (\mathbb{R}, +) \) and indeed \( \log 1 = 0. \) Also, a group homomorphism preserves inverses. Indeed \( \log m^{-1} = -\log m \) for all \( m \in \mathbb{R}_{\gt 0}. \) These observations are proved in the next two sections.

Group Homomorphism Preserves Identity

Let \( \phi : G \to H \) be a group homomorphism from \( (G, \ast) \) to \( (H, \cdot). \) Let \( e_1 \) be the identity in \( G \) and let \( e_2 \) be the identity in \( H. \) Then \( \phi(e_1) = e_2. \) The proof is straightforward. Note first that \[ \phi(e_1) \cdot \phi(e_1) = \phi(e_1 \ast e_1) = \phi(e_1) \] Multiplying both sides on the right by \( \phi(e_1)^{-1}, \) we get \[ (\phi(e_1) \cdot \phi(e_1)) \cdot \phi(e_1)^{-1} = \phi(e_1) \cdot \phi(e_1)^{-1}. \] Using the associative and inverse properties of groups, we can simplify both sides to get \[ \phi(e_1) = e_2. \]

Group Homomorphism Preserves Inverses

Let \( \phi : G \to H \) be a group homomorphism from \( (G, \ast) \) to \( (H, \cdot). \) Let \( e_1 \) be the identity in \( G \) and let \( e_2 \) be the identity in \( H. \) Then for all \( x \in G, \) \(\phi(x^{-1}) = (\phi(x))^{-1}. \) The proof of this is straightforward too. Note that \[ \phi(x) \cdot \phi(x^{-1}) = \phi(x \ast x^{-1}) = \phi(e_1) = e_2. \] Thus \( \phi(x^{-1}) \) is an inverse of \( \phi(x), \) so \[ \phi(x^{-1}) = (\phi(x))^{-1}. \] The image of the inverse of an element is the inverse of the image of that element.

Image of a Group Homomorphism

Let \( \phi : G \to H \) be a group homomorphism. Then the image of the \( \phi, \) denoted \[ \phi(G) = \{ \phi(x) : x \in G \} \] is a subgroup of \( H. \) We will prove this now.

Let \( a, b \in \phi(G). \) Then \( a = \phi(x) \) and \( b = \phi(y) \) for some \( x, y \in G. \) Now \( ab = \phi(x)\phi(y) = \phi(xy) \in \phi(G). \) Therefore \( \phi(G) \) satisfies the closure property.

Let \( e_1 \) and \( e_2 \) be the identities in \( G \) and \( H \) respectively. Since a group homomorphism preserves the identity, \( \phi(e_1) = e_2. \) Hence the identity of \( H \) lies in \( \phi(G). \)

Finally, let \( a \in \phi(G). \) Then \( a = \phi(x) \) for some \( x \in G. \) Then \( a^{-1} = \phi(x)^{-1} = \phi(x^{-1}) \in \phi(G). \) Therefore \( \phi(G) \) satisfies the inverse property as well. Therefore \( \phi(G) \) is a subgroup of \( H. \)

Group Monomorphism

A map \( \phi : G \to H \) from a group \( (G, \ast) \) to a group \( (H, \cdot) \) is a group monomorphism if \( \phi \) is a homomorphism and is injective. In other words, a homomorphism \( \phi \) is called a monomorphism if, for all \( x, y \in G, \) \[ \phi(x) = \phi(y) \implies x = y. \] Let \( e_1 \) be the identity element of \( G \) and let \( e_2 \) be the identity element of \( H. \) A useful result in group theory states that a homomorphism \( \phi : G \to H \) is a monomorphism if and only if its kernel is trivial, i.e. \[ \ker(\phi) = \{ x \in G : \phi(x) = e_2 \} = \{ e_1 \}. \] Let us prove this now.

Standard Proof

Suppose \( \phi : G \to H \) is a monomorphism. Since a homomorphism preserves the identity element, we have \( \phi(e_1) = e_2. \) Therefore \[ e_1 \in \ker(\phi). \] Let \( x \in \ker(\phi). \) Then \( \phi(x) = e_2 = \phi(e_1). \) Since \( \phi \) is injective, \( x = e_1. \) Therefore \[ \ker(\phi) = \{ e_1 \}. \] Conversely, suppose \( \ker(\phi) = \{ e_1 \}. \) Let \( x, y \in G \) such that \( \phi(x) = \phi(y). \) Then \[ \phi(x \ast y^{-1}) = \phi(x) \cdot \phi(y^{-1}) = \phi(x) \cdot (\phi(y))^{-1} = \phi(y) \cdot (\phi(y))^{-1} = e_2. \] Hence \[ x \ast y^{-1} \in \ker(\phi) = \{ e_1 \}, \] so \[ x \ast y^{-1} = e_1. \] Multiplying both sides on the right by \( y, \) we obtain \[ x = y. \] This completes the proof.

Alternate Proof

Here I briefly discuss an alternate way to think about the above proof. The above proof is how most texts usually present these arguments. In particular, the proof of injectivity typically proceeds by showing that equal images imply equal preimages. It's a standard proof technique. When I think about these proofs, however, the contrapositive argument feels more intuitive to me. I prefer to think about how unequal preimages must have unequal images. Mathematically, there is no difference at all but the contrapositive argument has always felt the most natural to me. Let me briefly describe how this proof runs in my mind when I think about it more intuitively.

Suppose \( \phi \) is a monomophorism. Since a homomorphism preserves the identity element, clearly \( \phi(e_1) = e_2. \) Since \( \phi \) is injective, it cannot map two distinct elements of \( G \) to \( e_2. \) Thus \( e_1 \) is the only element of \( G \) that \( \phi \) maps to \( e_2 \) which means \( \ker(\phi) = \{ e_1 \}. \)

To prove the converse, suppose \( \ker(\phi) = \{ e_1 \}. \) Consider distinct elements \( x, y \in G. \) Since \( x \ne y, \) we have \( x \ast y^{-1} \ne e_1. \) Therefore \( x \ast y^{-1} \notin \ker(\phi). \) Thus \( \phi(x \ast y^{-1}) \ne e_2. \) Since \( \phi \) is a homomorphism, \[ \phi(x \ast y^{-1}) = \phi(x) \cdot \phi(y^{-1}) = \phi(x) \cdot \phi(y)^{-1}. \] Therefore \( \phi(x) \cdot \phi(y)^{-1} \ne e_2 \) which implies \[ \phi(x) \ne \phi(y). \] This proves that \( \ker(\phi) = \{ e_1 \} \) implies that \( \phi \) is injective and thus a monomorphism.

Permutation Representation

Let \( G \) be a group and \( X \) a set. Then a homomorphism \[ \phi : G \to \operatorname{Sym}(X) \] is called a permutation representation of \( G \) on \( X . \) The homomorphism \( \phi \) maps each \( g \in G \) to a permutation of \( X. \) We say that each \( g \in G \) induces a permutation of \( X. \)

For example, let \( G = (\mathbb{Z}_3, +) \) and \( X = \{ 0, 1, 2, 3, 4, 5 \}. \) Define the map \( \phi : G \to \operatorname{Sym}(X) \) by \begin{align*} \phi(0) &= (), \\ \phi(1) &= (024)(135), \\ \phi(2) &= (042)(153). \end{align*} It is easy to verify that this is a homomorphism. Here is one way to verify it: \begin{align*} \phi(0)\phi(1) &= ()(024)(135) = (024)(135) = \phi(0 + 1), \\ \phi(0)\phi(2) &= ()(042)(153) = (042)(153) = \phi(0 + 2), \\ &\;\,\vdots \\ \phi(2)\phi(1) &= (042)(153)(024)(135) = () = \phi(0) = \phi(2 + 1), \\ \phi(2)\phi(2) &= (042)(153)(042)(153) = (024)(135) = \phi(1) = \phi(2 + 2). \end{align*} We will meet this homomorphism again in the form of group action \( \alpha \) in the next section.

Group Action

Let \( G \) be a group with identity element \( e. \) Let \( X \) be a set. A right action of \( G \) on \( X \) is a map \[ \alpha : X \times G \to X \] such that \begin{align*} \alpha(x, e) &= x, \\ \alpha(\alpha(x, g), h) &= \alpha(x, gh) \end{align*} for all \( x \in X \) and all \( g, h \in G. \) The two conditions above are called the identity and compatibility properties of the group action respectively. Note that in a right action, the product \( gh \) is applied left to right: \( g \) acts first and then \( h \) acts. If we denote \( \alpha(x, g) \) as \( x^g, \) then the notation for the two conditions can be simplified to \( x^e = x \) and \( (x^g)^h = x^{gh} \) for all \( g, h \in G. \)

Why Right Action?

We discuss right group actions here instead of left group actions because we want to use the notation \( \alpha(x, g) = x^g, \) which is quite convenient while studying permutations and graph automorphisms. It is perfectly possible to use left group actions to study permutations as well. However, we lose the benefit of the convenient \( x^g \) notation. In a left group action, the compatibility property is \( \alpha(g, \alpha(h, x)) = \alpha(gh, x) , \) so if we were to use the notation \( \alpha(g, x) = x^g, \) the compatibility property would look like \( (x^h)^g = x^{gh}. \) This reverses the order of exponents which can be confusing. Right group actions avoid this notational inconvenience.

Example 1

Let \( G = \mathbb{Z}_3 \) be the group under addition modulo \( 3 . \) Let \( X = \{ 0, 1, 2, 3, 4, 5 \}. \) Define an action \( \alpha \) of \( G \) on \( X \) by \[ \alpha(x, g) = x^g = (x + 2g) \bmod 6. \] Each \( g \in G \) acts as a permutation of \( X. \) For example, the element \( 0 \in \mathbb{Z}_3 \) acts as the identity permutation. The element \( 1 \in \mathbb{Z}_3 \) acts as the permutation \( (0 2 4)(1 3 5). \) The element \( 2 \in \mathbb{Z}_3 \) acts as the permutation \( (0 4 2)(1 5 3). \) The following table shows how each \( g \in G \) permutes \( X. \) \[ \begin{array}{c|ccc} x_{\downarrow} \backslash g_{\rightarrow} & 0 & 1 & 2 \\ \hline 0 & 0 & 2 & 4 \\ 1 & 1 & 3 & 5 \\ 2 & 2 & 4 & 0 \\ 3 & 3 & 5 & 1 \\ 4 & 4 & 0 & 2 \\ 5 & 5 & 1 & 3 \\ \end{array} \] From the table we see that each \( g \in G \) permutes the elements of \( \{ 0, 2, 4 \} \) among themselves. Similarly, the elements of \( \{ 1, 3, 5 \} \) are permuted among themselves. These sets \( \{0, 2, 4 \} \) and \( \{ 1, 3, 5 \} \) are called the orbits of the action. The concept of orbits is formally introduced in its own section further below.

Example 2

Now let \( G = \mathbb{Z}_6 \) be the group under addition modulo \( 6. \) Let \( X = \{ 0, 1, \dots, 8 \}. \) Define an action \( \beta \) of \( G \) on \( X \) by \[ \beta(x, g) = x^g = (x + 3g) \bmod 9. \] Now the table for the action looks like this: \[ \begin{array}{c|cccccc} x_{\downarrow} \backslash g_{\rightarrow} & 0 & 1 & 2 & 3 & 4 & 5 \\ \hline 0 & 0 & 3 & 6 & 0 & 3 & 6 \\ 1 & 1 & 4 & 7 & 1 & 4 & 7 \\ 2 & 2 & 5 & 8 & 2 & 5 & 8 \\ 3 & 3 & 6 & 0 & 3 & 6 & 0 \\ 4 & 4 & 7 & 1 & 4 & 7 & 1 \\ 5 & 5 & 8 & 2 & 5 & 8 & 2 \\ 6 & 6 & 0 & 3 & 6 & 0 & 3 \\ 7 & 7 & 1 & 4 & 7 & 1 & 4 \\ 8 & 8 & 2 & 5 & 8 & 2 & 5 \end{array} \] This action splits \( X \) into three orbits \( \{ 0, 3, 6 \}, \) \( \{ 1, 4, 7 \} \) and \( \{ 2, 5, 8 \}. \)

Group Actions Induce Permutations

Earlier, we saw an example of a group action and observed that each element of the group acts as a permutation. That was not merely a coincidence. It is indeed a general property of group actions. Whenever a group \( G \) acts on a set \( X, \) each element \( g \in G \) determines a bijection \( X \to X. \) In other words, every element of \( G \) acts as a permutation of \( X. \) Let us see why this must be the case.

Consider the group action \( \alpha : X \times G \to X. \) Fix \( g \in G \) and let \( x \) vary over \( X \) to obtain the map \[ \alpha_g : X \to X; \; x \mapsto \alpha(x, g). \] We show that \( \alpha_g \) is a bijection. First we prove injectivity. Let \( e \) be the identity element of \( G. \) Let \( x, y \in X. \) Then \begin{align*} \alpha_g(x) = \alpha_g(y) & \implies \alpha(x, g) = \alpha(y, g) \\ & \implies \alpha(\alpha(x, g), g^{-1}) = \alpha(\alpha(y, g), g^{-1}) \\ & \implies \alpha(x, gg^{-1}) = \alpha(y, gg^{-1}) \\ & \implies \alpha(x, e) = \alpha(y, e) \\ & \implies x = y. \end{align*} The \( x^g \) notation allows us to write the above proof more conveniently as follows: \begin{align*} \alpha_g(x) = \alpha_g(y) & \implies \alpha(x, g) = \alpha(y, g) \\ & \implies (x^g)^{g^{-1}} = (y^g)^{g^{-1}} \\ & \implies x^{g g^{-1}} = y^{g g^{-1}} \\ & \implies x^e = y^e \\ & \implies x = y. \end{align*} This completes the proof of injectivity. Now we prove surjectivity. Let \( y \in X. \) Take \( x = \alpha(y, g^{-1}). \) Then \[ \alpha_g(x) = \alpha(x, g) = \alpha(\alpha(y, g^{-1}), g) = \alpha(y, g^{-1} g) = \alpha(y, e) = y. \] Again, if we write \( x = y^{g^{-1}}, \) the above step can be written more succinctly as \[ \alpha_g(x) = x^g = (y^{g^{-1}})^g = y^{(g^{-1} g)} = y^e = y. \] Thus every element \( y \in X \) has a preimage in \( X \) under \( \alpha_g. \) Hence \( \alpha_g \) is surjective. Since we have already shown that \( \alpha_g \) is injective, we now conclude that \( \alpha_g \) is bijective. Therefore \( \alpha_g \) is a permutation of \( X. \) Stated symbolically, \[ \alpha_g \in \operatorname{Sym}(X). \] Note that \[ \alpha_g(x) = \alpha(x, g) = x^g. \] Thus both \( \alpha_g(x) \) and \( x^g \) serve as convenient shorthands for \( \alpha(x, g). \)

Group Actions Determine Permutation Representations

We have seen that each group element \( g \in G \) induces (acts as) a permutation of \( X. \) Precisely speaking, each \( g \in G \) determines a permutation \( \alpha_g \) of \( X. \) Now define a map \[ \phi: G \to \operatorname{Sym}(X); \; g \mapsto \alpha_g. \] We now show that this map is a homomorphism. This means that we want to show that \( \phi(gh) = \phi(g) \phi(h). \) Since \( \phi(g), \phi(h) \in \operatorname{Sym}(X), \) the right-hand side is a product of permutations of \( X. \) We first define the product of two permutations \( \pi, \rho : X \to X \) by \[ \pi \rho : X \to X; \; x \mapsto \rho(\pi(x)). \] In other words, \( \pi \rho = \rho \circ \pi. \) Now \begin{align*} \phi(gh)(x) & = \alpha_{gh}(x) \\ & = \alpha(x, gh) \\ & = \alpha(\alpha(x, g), h) \\ & = \alpha_h(\alpha_g(x)) \\ & = (\alpha_h \circ \alpha_g)(x) \\ & = (\alpha_g \alpha_h)(x) \\ & = (\phi(g) \phi(h))(x). \end{align*} Since the above equality holds for all \( x \in X, \) we conclude that \[ \phi(gh) = \phi(g) \phi(h). \] Hence \( \phi \) is a group homomorphism from \( G \) to \( \operatorname{Sym}(X). \) Therefore \( \phi \) is a permutation representation of \( G \) on \( X. \) It maps each group element \( g \in G \) to a permutation \( \alpha_g \in \operatorname{Sym}(X). \)

Note the multiple levels of abstraction here. The group action \( \alpha : X \times G \to X \) determines a permutation representation \( \phi : G \to \operatorname{Sym}(X). \) Each element \( g \in G \) together with the group action \( \alpha \) determines a permutation \( \alpha_g : X \to X. \)

Also note that \( \phi(g)(x) = \alpha_g(x) = \alpha(g, x) = x^g. \) In fact, \( \phi(g) = \alpha_g. \)

Permutation Representations Determine Group Actions

Consider a permutation representation \( \phi : G \to \operatorname{Sym}(X). \) Define a map \[ \alpha : X \times G \to X; \; (x, g) \mapsto \phi(g)(x). \] First we verify the identity property of group actions. Since \( \phi \) is a homomorphism, it preserves the identity element. Therefore \( \phi(e) \) is the identity permutation. Hence \[ \alpha(x, e) = \phi(e)(x) = x \] Now we verify the compatibility property of the action. For all \( g, h \in G \) and \( x \in X, \) we have \begin{align*} \alpha(\alpha(x, g), h) & = \alpha(\phi(g)(x), h) \\ & = \phi(h)(\phi(g)(x)) \\ & = (\phi(h) \circ \phi(g))(x) \\ & = (\phi(g)\phi(h))(x) \\ & = \phi(gh)(x) \\ & = \alpha(x, gh). \end{align*} This completes the proof of the fact that every permutation representation determines a group action.

Bijection Between Group Actions and Permutation Representations

There is a bijection between the group actions \( \alpha : X \times G \to X \) and permutation representations \( \phi : G \to \operatorname{Sym}(X). \) We now show that these two constructions are inverses of each other.

Given a right action \( \alpha : X \times G \to X, \) define \[ \phi_{\alpha} : G \to \operatorname{Sym}(X) \quad \text{by} \quad \phi_{\alpha}(g)(x) = \alpha(x, g). \] Given a permutation representation \( \phi : G \to \operatorname{Sym}(X), \) define \[ \alpha_{\phi} : X \times G \to X \quad \text{by} \quad \alpha_{\phi}(x, g) = \phi(g)(x). \] We now show that these two constructions undo each other. Take an arbitrary group action \( \alpha : X \times G \to X \) and construct the corresponding permutation representation \( \phi_{\alpha}. \) Then take this permutation representation and construct the group action \( \alpha_{\phi_{\alpha}}. \) But \[ \alpha_{\phi_{\alpha}}(x, g) = \phi_{\alpha}(g)(x) = \alpha(x, g). \] Therefore \( \alpha_{\phi_{\alpha}} = \alpha. \) Similarly, starting with the permutation representation \( \phi, \) we get \[ \phi_{\alpha_{\phi}}(g)(x) = \alpha_{\phi}(x, g) = \phi(g)(x). \] Therefore \( \phi_{\alpha_{\phi}} = \phi. \) Hence there is a bijection between group actions \( \alpha : X \times G \to X \) and permutation representations: \( \phi : G \to \operatorname{Sym}(X) . \) In fact, a group action and the corresponding permutation representation contain the same information, namely how the elements \( g \in G \) acts as permutations of \( X. \) For this reason, many advanced texts do not make any distinction between the group action and its permutation representation. They often use them interchangeably even though technically they have different domains.

Orbits

Let \( G \) act on a set \( X. \) For an element \( x \in X, \) the orbit of \( x \) under the action of \( G \) is the set of all elements of \( X \) that can be reached from \( x \) by the action of elements of \( G. \) Symbolically, the orbit of \( x \) is the set \[ x^G = \{ x^g : g \in G \}. \] In other words, the orbit of \( x \) contains every element of \( X \) that \( x \) can be moved to by the group action. If \( y \in x^G, \) then there exists some \( g \in G \) such that \( y = x^g . \)

The orbits of a group action partition the set \( X. \) That is, every element of \( X \) lies in exactly one orbit and two orbits are either identical or disjoint. Thus the group action decomposes the set \( X \) into disjoint subsets (the orbits), each consisting of elements that can be transformed into one another by the action of \( G. \)

Stabilisers

Let \( G \) be a group acting on a set \( X. \) For an element \( x \in X, \) the stabiliser of \( x \) is the set \[ G_x = \{ g \in G : x^g = x \}. \] The stabiliser \( G_x \) consists of all elements of \( G \) that fix the element \( x. \) The stabiliser \( G_x \) is a subgroup of \( G. \) Indeed, the identity element \( e \in G \) satisfies \( x^e = x, \) so \( e \in G_x. \) If \( g, h \in G_x, \) then \( x^{gh} = (x^g)^h = x. \) If \( g \in G_x, \) then \( x^{g^{-1}} = (x^g)^{g^{-1}} = x. \)

Intuitively, the stabiliser measures how much symmetry of the group action leaves the element \( x \) unchanged. The larger the stabiliser, the more elements of \( G \) fix \( x. \)

Orbit-Stabiliser Theorem

Let \( G \) be a group acting on a set \( X. \) The orbit-stabiliser theorem states that for any \( x \in X, \) \[ \lvert G_x \rvert \cdot \lvert x^G \rvert = \lvert G \rvert. \] Stated differently, the index of the stabiliser \( G_x \) in the group \( G \) is given by \[ [ G : G_x ] = \lvert G_x \backslash G \rvert = \lvert G \rvert / \lvert G_x \rvert = \lvert x^G \rvert. \] There is a bijection between the right cosets of \( G_x \) and the elements of \( x^G. \) Demonstrating this bijection proves the above equation. We will work with right cosets of \( G_x. \) Define \[ \phi : G_x \backslash G \to x^G; \; G_x g \mapsto x^g. \] We want to show that \( \phi \) is a bijection. But first we need to show that \( \phi \) is well defined. A coset \( G_x g \in G_x \backslash G \) can also be written as \[ G_x g = G_x h \] for some \( h \in G. \) If \( x^g \ne x^h, \) then \( \phi \) would not be well defined, since \( \phi \) must assign each coset in \( G_x \backslash G \) to exactly one element in the orbit \( x^G \) in order to be a function. This can be shown using the following equivalences: \begin{align*} G_x g = G_x h & \iff hg^{-1} \in G_x \\ & \iff x = x^{h g^{-1}} \\ & \iff x^g = x^h \\ \end{align*} This proves two things at once. The fact that \[ G_x g = G_x h \implies x^g = x^h \] proves that when the same coset is written using two different representatives, the image does not change. Therefore \( \phi \) is well defined. Further \[ x^g = x^h \implies G_x g = G_x h \] proves that \( \phi \) is injective. To show that \( \phi \) is surjective, let \( y \in x^G. \) Then \( y = x^g \) for some \( g \in G. \) Since \( \phi(G_x g) = x^g, \) we get \[ \phi(G_x g) = y. \] Thus every element of \( x^G \) is the image of some right coset \( G_x g \) under \( \phi. \) This completes the proof of a bijection between the right cosets of \( G_x \) and the elements of \( x^G. \) Therefore \( \lvert G_x \backslash G \rvert = \lvert x^G \rvert \) and hence \( \lvert G \rvert / \lvert G_x \rvert = \lvert x^G \rvert , \) which establishes the orbit-stabiliser theorem.

Faithful Actions

Let \( G \) act on a set \( X. \) The action is called faithful if distinct elements of \( G \) induce distinct permutations of \( X. \) In other words, the only element of \( G \) that acts as the identity permutation of \( X \) is the identity element \( e \in G. \) Symbolically, the action is faithful if \[ g \ne e \implies \exists x \in X, \; x^g \ne x. \] Equivalently, \[ \forall x \in X, \; x^g = x \implies g = e. \] The action is faithful if the only element of \( G \) that fixes every element of \( X \) is the identity, i.e. \[ \bigcap_{x \in X} G_x = \{ e \}. \] Recall that every group action determines a permutation representation \( \phi : G \to \operatorname{Sym}(X). \) From this point of view, the action is faithful precisely when the permutation representation is faithful, that is, when the homomorphism \( \phi \) is injective (or equivalently when \( \ker(\phi) = \{ e \} \)). In other words, the action is faithful if and only if the associated homomorphism \( \phi \) is a monomorphism.

Semiregular Actions

A group action of \( G \) on \( X \) is called semiregular if no non-identity element of \( G \) fixes any element of \( X. \) In other words, whenever \( g \ne e, \) the permutation of \( X \) induced by \( g \) moves every element of \( X. \) Symbolically, \[ g \ne e \implies \forall x \in X, \; x^g \ne x. \] Equivalently, \[ \exists x \in X, \; x^g = x \implies g = e. \] The action is semiregular if \[ \forall x \in X, \; G_x = \{ e \}. \] This is a stronger property than faithfulness. Faithfulness only guarantees that when \( g \ne e, \) the element \( g \) moves at least one element of \( X. \) But semiregularity guarantees that when \( g \ne e, \) the element \( g \) moves every element of \( X . \) Therefore every semiregular action is faithful, but not every faithful action is semiregular.

Transitive Actions

Let \( G \) act on a set \( X. \) The action is called transitive if there is only one orbit. In other words, the action is transitive if every element of \( X \) can be reached from any other element by the action of some element of \( G. \) Symbolically, the action is transitive if \[ \forall x, y \in X \; \exists g \in G, \; x^g = y. \] Equivalently, the action is transitive if \[ x^G = X \] for some (and hence every) \( x \in X. \)

Conjugacy

Let \( G \) be a group. Let \( x, g \in G. \) The element \[ g^{-1} x g \] is called a conjugate of \( x \) by \( g. \) Any element \( y \in G \) that can be written as \( g^{-1} x g \) for some \( g \in G \) is said to be a conjugate of \( x. \) The conjugacy class of \( x \) in \( G \) is the set \[ x^G = \{ g^{-1} x g : g \in G \}. \] In other words, the conjugacy class of \( x \) is the set of all elements of \( G \) that are conjugate to \( x. \) At first, reusing the orbit notation \( x^G \) for the conjugacy class may seem like an abuse of notation. However, we will see in the next section that the conjugacy class is precisely the orbit of \( x \) under the action of \( G \) on itself by conjugation. Thus \( x^G \) is in fact a natural and accurate notation for the conjugacy class.

Conjugation as Group Action

Conjugation can be seen as an action of a group on itself. Define the map \[ \alpha : G \times G \to G; \; (x, g) \mapsto g^{-1} x g. \] Note that \[ \alpha(x, e) = e^{-1} x e = x \] and \[ \alpha(\alpha(x, g), h) = h^{-1} (g^{-1} x g) h = (gh)^{-1} x (gh) = \alpha(x, gh). \] Therefore \( \alpha \) satisfies the two defining properties of a right group action. The conjugacy class \( x^G \) is precisely the orbit of \( x \) under the conjugation action. Therefore the orbits of the conjugation action of \( G \) on itself are the conjugacy classes of \( G. \)

Right Conjugation vs Left Conjugation

We observed above that the conjugation action is a right action of a group on itself. Let \( x, g \in G \) and let \[ y = g^{-1} x g. \] Now let \( h = g^{-1}. \) Then we can write the above equation as \[ y = h x h^{-1}. \] According to the previous section, \( y \) is the conjugate of \( x \) by \( g. \) However, many texts call \( y \) the conjugate of \( x \) by \( h. \) Both are valid perspectives. In both perspectives, \( x \) and \( y \) are conjugates of each other. Precisely,

  • In the first perspective, we have \( y = g^{-1} x g \) and we say that \( y \) is a conjugate of \( x \) by \( g. \) A corollary is that \( x \) is a conjugate of \( y \) by \( g^{-1}. \)
  • In the second perspective, we have \( y = h x h^{-1} \) and we say that \( y \) is a conjugate of \( x \) by \( h. \) A corollary is that \( x \) is a conjugate of \( y \) by \( h^{-1}. \)

Although in both perspectives, \( x \) and \( y \) are conjugates of each other, the group element by which one is conjugated to the other is different. This leads to different group actions as well.

When we say that \( y = g^{-1} x g \) is a conjugate of \( x \) by \( g, \) the group action \[ \alpha : G \times G \to G; \; (x, g) \mapsto g^{-1} x g. \] is a right group action as demonstrated in the previous section. But when we say that \( y = h x h^{-1} \) is a conjugate of \( x \) by \( h, \) the conjugation action is no longer a right group action because the compatibility property is violated: \[ \alpha(\alpha(x, g), h) = h ( g x g^{-1} ) h^{-1} = (hg) x (hg)^{-1} = \alpha(x, hg). \] We get \( \alpha(x, hg) \) instead of the required \( \alpha(x, gh) . \) So with the second perspective, the group action is no longer a right action. Instead it is a left action since \[ \alpha(g, \alpha(h, x)) = g (h x h^{-1}) g^{-1} = (gh) x (gh)^{-1} = \alpha(gh, x). \] In this post we will work only with the first perspective because we will use right actions throughout.

Conjugate Subgroups

Let \( G \) be a group. Let \( H \le G. \) Define \[ g^{-1} H g = \{ g^{-1} h g : h \in H \}. \] We say that \( g^{-1} H g \) is a conjugate of \( H \) by \( g. \)

Conjugacy of Stabilisers

Let \( G \) be a group acting on a set \( X. \) Let \( x \in X \) and \( g \in G. \) Then \[ g^{-1} G_x g = G_{x^g}. \] That is, \( G_{x^g} \) is a conjugate of \( G_x \) by \( g. \) This result can be summarised as follows: stabilisers of elements in the same orbit are conjugate. Or more explicitly: the stabiliser of \( x^g \) is a conjugate of the stabiliser of \( x \) by \( g. \) The proof is straightforward. Let \( h \in G. \) Then \begin{align*} h \in g^{-1} G_x g & \iff g^{-1} (g h g^{-1}) g \in g^{-1} G_x g \\ & \iff g h g^{-1} \in G_x \\ & \iff x^{g h g^{-1}} = x \\ & \iff (x^g)^h = x^g \\ & \iff h \in G_{x^g}. \end{align*} Therefore \( g^{-1} G_x g = G_{x^g}. \)

Algebraic Graph Theory

Stabiliser Index

In a vertex-transitive graph \( \Gamma, \) for any \( x \in V(\Gamma) \) and all \( y \in V(\Gamma), \) there exists \( g \in G \) such that \( x^g = y. \) Therefore \( x^G = V(\Gamma). \) Thus by the orbit-stabiliser theorem, \[ [ G : G_x ] = \lvert G_x \backslash G \rvert = \lvert x^G \rvert = \lvert V(\Gamma) \rvert. \]

Strongly Connected Directed Graph

A path in a directed graph \( \Gamma \) is a sequence of vertices \( v_0, \dots, v_r \) of distinct vertices such that \( (v_{i - 1}, v_i) \) is an arc of \( \Gamma \) for \( i = 1, \dots, r . \)

A directed graph is strongly connected if for every ordered pair of vertices \( (u, v) \) there is a path from \( u \) to \( v . \)

Shunting

Let \( \alpha = ( \alpha_0, \dots, \alpha_s ) \) and \( \beta = ( \beta_0, \dots, \beta_s ) \) be two \( s \)-arcs in a graph \( \Gamma. \) We say that \( \beta \) is a successor of \( \alpha \) if \( \beta_i = \alpha_{i + 1} \) for \( 0 \le i \le s - 1. \) We also say that \( \alpha \) can be shunted onto \( \beta. \)

In section 4.2 of Godsil and Royle, there is a rather technical setup which first defines \( X^{(s)} \) as the directed graph with the \( s \)-arcs of a graph \( X \) as its vertices such that \( (\alpha, \beta) \) is an arc of \( X^{(s)} \) if and only if \( \alpha \) can be shunted onto \( \beta \) in \( X. \) Then it goes on to show that if \( X \) is a connected graph with a minimum degree two and \( X \) is not a cycle, then \( X^{(s)} \) is strongly connected for all \( s \ge 0. \)

That is a very technical way of saying that in a connected graph \( X \) that is not a cycle and has a minimum degree two, any \( s \)-arc \( \alpha \) can be sent to any \( s \)-arc \( \beta \) by repeated shunting. The proof is quite technical too and pretty long, so I'll omit it here.

Automorphisms Preserve Successor Relation

We will obtain a nifty result here that will prove to be very useful in the next section. Let \( S(\gamma) \) denote the set of all successors of the \( s \)-arc \( \gamma \) of a graph. Let \( g \) be an automorphism of the graph. Then \[ \delta \in S(\gamma) \iff \delta^g \in S(\gamma^g). \] This follows directly from the fact that automorphisms preserve adjacency, so they must preserve successor relation as well. A corollary of this is that for an automorphism \( h, \) we have \[ \delta^{h^{-1}} \in S(\gamma) \iff \delta \in S(\gamma^h). \] This is the form that will be useful soon.

Test of \( s \)-arc Transitivity

The results in the previous two sections lead to a remarkably simple proof of the fact that the Petersen graph is \( 3 \)-arc transitive. Let us see how.

Let \( P \) be the Petersen graph whose vertices are the \( 2 \)-subsets of \( \{ 1, 2, 3, 4, 5 \} \) with adjacency given by disjointness of the \( 2 \)-subsets. Then \( \operatorname{Aut}(P) \cong S_5 \) since any permutation of \( \{ 1, 2, 3, 4, 5 \} \) induces a permutation of the vertices that preserves disjointness and hence adjacency. We will use the shorthand \( ab \) to represent each vertex \( \{ a, b \} \) of \( P. \) Consider the \( 3 \)-arc \[ \alpha = (12, 34, 15, 23). \] It has exactly two successors, namely \[ \beta_1 = (34, 15, 23, 14), \quad \beta_2 = (34, 15, 23, 45). \] Let \( g_1 = (13)(245) \) and \( g_2 = (13524). \) Then \begin{align*} \alpha^{g_1} & = (12, 34, 15, 23)^{(13)(245)} = (34, 15, 23, 14) = \beta_1, \\ \alpha^{g_2} & = (12, 34, 15, 23)^{(13524)} = (34, 51, 23, 45) = \beta_2. \end{align*} Let \( H = \langle g_1, g_2 \rangle \le \operatorname{Aut}(P). \) Consider an \( s \)-arc \( \alpha^h \) for some \( h \in H. \) Let \( \delta \in S(\alpha^h). \) Then by the result in the previous section, we get \[ \delta^{h^{-1}} \in S(\alpha) = \{ \beta_1, \beta_2 \} = \{ \alpha^{g_1}, \alpha^{g_2} \}. \] Therefore \[ \delta \in \{ \alpha^{g_1 h}, \alpha^{g_2 h} \}. \] Thus \[ \delta \in \alpha^{H}. \] We started with an \( s \)-arc \( \alpha^h \in \alpha^H \) and showed that its successors \( \delta \) also lie in \( \alpha^H. \) Thus the orbit \( \alpha^H \) is closed under taking successors.

Now by the shunting result discussed previously, \( \alpha \) can be sent to any \( 3 \)-arc of \( P \) by repeated shunting. Therefore all \( 3 \)-arcs of \( P \) belong to \( \alpha^H. \) Therefore the automorphisms in \( H \) can send any \( 3 \)-arc of \( P \) to any other thus making \( P \) \( 3 \)-arc transitive.

Moore Graphs

Graphs with diameter \( d \) and girth \( 2d + 1 \) are known as Moore graphs.

There are an infinite number of Moore graphs with diameter \( 1 \) since the complete graphs \( K_n, \) where \( n \ge 3, \) have diameter \( 1 \) and girth \( 3. \)

There are three known Moore graphs of diameter \( 2. \) They are \( C_5, \) \( J(5, 2, 0) \) also known as the Petersen graph and the Hoffman-Singleton graph. They are respectively \( 2 \)-regular, \( 3 \)-regular and \( 7 \)-regular. There is a famous result that proves that a Moore graph must be \( 2 \)-regular, \( 3 \)-regular, \( 7 \)-regular or \( 57 \)-regular. It is unknown currently whether a \( 57 \)-regular Moore graph of diameter \( 2 \) exists.

There are infinitely many Moore graphs of diameter \( d \ge 3 \) because the odd cycles \( C_{2d + 1} \) are \( 2 \)-regular graphs with diameter \( d \) and girth \( 2d + 1 \) for all \( d \ge 1. \) However, there are no \( k \)-regular Moore graphs for diameter \( d \ge 3 \) when \( k \ge 3. \)

Generalised Polygons

Bipartite graphs with diameter \( d \) and girth \( 2d \) are known as generalised polygons. This is easy to understand. If we take a classical \( d \)-gon and create the incidence graph of its vertices and edges, then the incidence graph is the cycle \( C_{2d} \) which has diameter \( d \) and girth \( 2d. \)

The converse is not always true. For example, the Heawood graph which has diameter \( d = 3 \) and girth \( 2d = 6. \) It is the incidence graph of Fano plane, which is a projective plane rather than a classical \( d \)-gon.

Although a generalised polygon is not always the incidence graph of a classical polygon, the idea behind the definition comes from a simple observation. If we take a classical \( d \)-gon and form the incidence graph of its vertices and edges, we obtain the cycle \( C_{2d}. \) This graph is bipartite and has diameter \( d \) and girth \( 2d. \) The definition of a generalised polygon abstracts these properties. Any bipartite graph with diameter \( d \) and girth \( 2d \) is called a generalised polygon, even when it is not the incidence graph of a classical \( d \)-gon. In this way the definition allows much richer graphs than simple cycles.

Computing

Select Between Lines, Inclusive

Select text between two lines, including both lines:

sed '/pattern1/,/pattern2/!d'
sed -n '/pattern1/,/pattern2/p'

Here are some examples:

$ printf 'A\nB\nC\nD\nE\nF\nG\nH\n' | sed '/C/,/F/!d'
C
D
E
F
$ printf 'A\nB\nC\nD\nE\nF\nG\nH\n' | sed -n '/C/,/F/p'
C
D
E
F

Select Between Lines, Exclusive

Select text between two lines, excluding both lines:

sed '/pattern1/,/pattern2/!d; //d'

Here is an example usage:

$ printf 'A\nB\nC\nD\nE\nF\nG\nH\n' | sed '/C/,/F/!d; //d'
D
E

The negated command !d deletes everything not matched by the 2-address range /C/,/F/, i.e. it deletes everything before the line matching /C/ as well as everything after the line matching /F/. So we are left with only the lines from C to F, inclusive. Finally, // (the empty regular expression) reuses the most recently used regular expression. So when /C/,/F/ matches C, the command //d also matches C and deletes it. Similarly, F is deleted too. That's how we are left with the lines between C and F, exclusive.

Here are some excerpts from POSIX.1-2024 that help understand the !d and //d commands better:

A function can be preceded by a '!' character, in which case the function shall be applied if the addresses do not select the pattern space. Zero or more <blank> characters shall be accepted before the '!' character. It is unspecified whether <blank> characters can follow the '!' character, and conforming applications shall not follow the '!' character with <blank> characters.
If an RE is empty (that is, no pattern is specified) sed shall behave as if the last RE used in the last command applied (either as an address or as part of a substitute command) was specified.

Signing and Verification with SSH Key

Here are some minimal commands to demonstrate how we can sign some text using SSH key and then later verify it.

ssh-keygen -t ed25519 -f key
echo hello > hello.txt
ssh-keygen -Y sign -f key.pub -n file hello.txt
echo "jdoe $(cat key.pub)" > allowed.txt
ssh-keygen -Y verify -f allowed.txt -I jdoe -n file -s hello.txt.sig < hello.txt

Here are some examples that demonstrate what the outputs and signature file look like:

$ ssh-keygen -Y sign -f key.pub -n file hello.txt
Signing file hello.txt
Write signature to hello.txt.sig
$ cat hello.txt.sig
-----BEGIN SSH SIGNATURE-----
U1NIU0lHAAAAAQAAADMAAAALc3NoLWVkMjU1MTkAAAAgAwP6RnmFVrZO0m/nRIHyvr2S19
itsKegj9p/BZKqP1sAAAAEZmlsZQAAAAAAAAAGc2hhNTEyAAAAUwAAAAtzc2gtZWQyNTUx
OQAAAEB8ylqjCLgInF8DvROnLSm1UUWd0VuLPesI+1NhMrV9BjH5lf0w20kHunJW3qRIjw
Jfs9+q/e47KdlR8wBQaHYD
-----END SSH SIGNATURE-----
$ ssh-keygen -Y verify -f allowed.txt -I jdoe -n file -s hello.txt.sig < hello.txt
Good "file" signature for jdoe with ED25519 key SHA256:9ZJuUJNMy1UXo3AlQy8L7baD3LOfEbgQ30ELIt+8wWc

Block IP Address with nftables

Here is a sequence of commands to create an nftables rule from scratch to block an IP address:

$ sudo nft list ruleset
$ sudo nft add table inet filter
$ sudo nft list ruleset
table inet filter {
}
$ sudo nft add chain inet filter input { type filter hook input priority 0 \; }
$ sudo nft list ruleset
table inet filter {
        chain input {
                type filter hook input priority filter; policy accept;
        }
}
$ sudo nft add rule inet filter input ip saddr 172.236.0.216 drop
$ sudo nft list ruleset
table inet filter {
        chain input {
                type filter hook input priority filter; policy accept;
                ip saddr 172.236.0.216 drop
        }
}

Here is how to undo the above setup step by step:

$ sudo nft -a list ruleset
table inet filter { # handle 1
        chain input { # handle 1
                type filter hook input priority filter; policy accept;
                ip saddr 172.236.0.216 drop # handle 2
        }
}
$ sudo nft delete rule inet filter input handle 2
$ sudo nft list ruleset
table inet filter {
        chain input {
                type filter hook input priority filter; policy accept;
        }
}
$ sudo nft delete chain inet filter input
$ sudo nft list ruleset
table inet filter {
}
$ sudo nft delete table inet filter
$ sudo nft list ruleset
$

Finally, the following command deletes all rules, chains and tables. It wipes the entire ruleset, so use it with care.

$ sudo nft flush ruleset
$ sudo nft list ruleset
$

All outputs above were obtained using nftables v1.1.3 on Debian 13.2 (Trixie).

Debian Logrotate Setup

Observed on Debian 11.5 (Bullseye) that logrotate is set up on it via systemd. Here are some outputs that show what the setup is like:

$ sudo systemctl status logrotate.service
● logrotate.service - Rotate log files
     Loaded: loaded (/lib/systemd/system/logrotate.service; static)
     Active: inactive (dead) since Mon 2026-03-30 00:00:17 UTC; 19h ago
TriggeredBy:  logrotate.timer
       Docs: man:logrotate(8)
             man:logrotate.conf(5)
    Process: 2148235 ExecStart=/usr/sbin/logrotate /etc/logrotate.conf (code=exited, status=0/SUCCESS)
   Main PID: 2148235 (code=exited, status=0/SUCCESS)
        CPU: 574ms

Mar 30 00:00:16 spweb systemd[1]: Starting Rotate log files...
Mar 30 00:00:17 spweb systemd[1]: logrotate.service: Succeeded.
Mar 30 00:00:17 spweb systemd[1]: Finished Rotate log files.
$ sudo systemctl status logrotate.timer
● logrotate.timer - Daily rotation of log files
     Loaded: loaded (/lib/systemd/system/logrotate.timer; enabled; vendor preset: enabled)
     Active: active (waiting) since Mon 2026-01-19 19:19:34 UTC; 2 months 9 days ago
    Trigger: Tue 2026-03-31 00:00:00 UTC; 4h 7min left
   Triggers:  logrotate.service
       Docs: man:logrotate(8)
             man:logrotate.conf(5)

Warning: journal has been rotated since unit was started, output may be incomplete.
$ sudo systemctl list-timers logrotate
NEXT                        LEFT         LAST                        PASSED  UNIT            ACTIVATES
Tue 2026-03-31 00:00:00 UTC 4h 7min left Mon 2026-03-30 00:00:16 UTC 19h ago logrotate.timer logrotate.service

1 timers listed.
Pass --all to see loaded but inactive timers, too.
$ head /lib/systemd/system/logrotate.service
[Unit]
Description=Rotate log files
Documentation=man:logrotate(8) man:logrotate.conf(5)
RequiresMountsFor=/var/log
ConditionACPower=true

[Service]
Type=oneshot
ExecStart=/usr/sbin/logrotate /etc/logrotate.conf

$ cat /lib/systemd/system/logrotate.timer
[Unit]
Description=Daily rotation of log files
Documentation=man:logrotate(8) man:logrotate.conf(5)

[Timer]
OnCalendar=daily
AccuracySec=1h
Persistent=true

[Install]
WantedBy=timers.target
$ grep -vE '^#|^$' /etc/logrotate.conf
weekly
rotate 4
create
include /etc/logrotate.d
$ ls -l /etc/logrotate.d/
total 40
-rw-r--r-- 1 root root 120 Aug 21  2022 alternatives
-rw-r--r-- 1 root root 173 Jun 10  2021 apt
-rw-r--r-- 1 root root 130 Oct 14  2019 btmp
-rw-r--r-- 1 root root  82 May 26  2018 certbot
-rw-r--r-- 1 root root 112 Aug 21  2022 dpkg
-rw-r--r-- 1 root root 128 May  4  2021 exim4-base
-rw-r--r-- 1 root root 108 May  4  2021 exim4-paniclog
-rw-r--r-- 1 root root 329 May 29  2021 nginx
-rw-r--r-- 1 root root 374 May 20  2022 rsyslog
lrwxrwxrwx 1 root root  28 Mar 17 01:52 susam -> /opt/susam.net/etc/logrotate
-rw-r--r-- 1 root root 145 Oct 14  2019 wtmp

To force log rotation right now, execute:

sudo systemctl start logrotate.service

Read on website | #notes | #mathematics | #linux | #technology

]]>
Accessing Fork Commits via Original Repository https://susam.net/fork-commits-via-original-repo.html wfcar Sat, 28 Mar 2026 00:00:00 +0000 I ran a small experiment with Git hosting behaviour using two demo repositories:

  • cuppa: The original repository.
  • muppa: Fork of cuppa with questionable changes.

Here is a table with links to these repositories on Codeberg and GitHub:

Name Codeberg GitHub
cuppa codeberg.org/spxy/cuppa github.com/spxy/cuppa
muppa codeberg.org/spxy/muppa github.com/spxy/muppa

It is well known that GitHub lets us access a commit that exists only on the fork via the original repository using a direct commit URL. I wanted to find out if Codeberg behaves the same.

The commit f79ef5a exists only on the fork (muppa) but not on the original repo (cuppa). Let us see how the two hosting services handle direct URLs to this commit.

Name Codeberg GitHub
cuppa f79ef5a f79ef5a
muppa f79ef5a f79ef5a

If we look at the second row, both commit URLs for Codeberg and GitHub work because that is where the commit was actually created. The commit belongs to the fork named muppa.

Now if we look at the first row, the commit URL for Codeberg returns a 404 page. This reflects the fact that the commit f79ef5a does not exist on cuppa. However, GitHub returns a successful response and shows the commit. It shows the following warning at the top:

This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

There is no particular point to this experiment. I just wanted to know.

Read on website | #technology

]]>
Wander Console 0.3.0 https://susam.net/code/news/wander/0.3.0.html wnzth Wed, 25 Mar 2026 00:00:00 +0000 Wander 0.3.0 is the third release of Wander, a small, decentralised, self-hosted web console that lets visitors to your website explore interesting websites and pages recommended by a community of independent website owners. To try it, go to susam.net/wander/.

This release brings small but important bug fixes. The previous release, version 0.2.0 introduced a number of new features. Unfortunately, two of them caused issues for some users. A new feature in the previous release was the ignore list feature. The ignore list defines console URLs and page URLs that the console never uses while discovering page recommendations. While this feature works fine, due to a bug in the implementation, the Console dialog fails to load in consoles that do not define any ignore list. This has now been fixed.

There was another issue due to which the <iframe> that displays discovered websites and pages could not load certain websites. In particular, any website that relied on same-origin context to load its own resources failed to load in the console. This has been fixed as well. Please see codeberg.org/susam/wander/issues/7 for a detailed discussion on this issue.

Apart from these two important fixes, there are a few other minor fixes too pertaining to preventing horizontal scrolling in small devices and preventing duplicate recommendations from appearing too close to each other. Please see CHANGES.md for a detailed changelog.

To learn more about Wander, how it works and how to set it up, please read the project README at codeberg.org/susam/wander. To try it out right now, go to susam.net/wander/.

Read on website | #web | #technology

]]>
Wander Console 0.2.0 https://susam.net/code/news/wander/0.2.0.html wnztz Tue, 24 Mar 2026 00:00:00 +0000 Wander 0.2.0 is the second release of Wander, a small, decentralised, self-hosted web console that lets visitors to your website explore interesting websites and pages recommended by a community of independent personal website owners. To try it, go to susam.net/wander.

This release brings a number of improvements. When I released version 0.1.0, it was the initial version of the software I was using for my own website. Naturally, I was the only user initially and I only added trusted web pages to the recommendation list of my console. But ever since I announced this project on Hacker News, it has received a good amount of attention. It has been less than a week since I announced it there but over 30 people have set up a Wander console on their personal websites. There are now over a hundred web pages being recommended by this network of consoles. With the growth in the number of people who have set up Wander console, came several feature requests, most of which have been implemented already. This release makes these new features available.

Since Wander 0.2.0, the wander.js file of remote consoles is executed in a sandbox iframe to ensure that it has no side effects on the parent Wander console page. Similarly, the pages recommended by the network are also loaded into a sandbox iframe.

This release also brings several customisation features. Console owners can customise their Wander console by adding custom CSS or JavaScript. Console owners can also block certain URLs from ever being recommended on their console. This is especially important in providing a good wandering experience to visitors. Since this network is completely decentralised, console owners can add any web page they like to their console. Sometimes they inadvertently add pages that do not load successfully in the console due to frame embedding restrictions. This leads to an uneven wandering experience because these page recommendations occasionally make it to other consoles where they fail to load. Console owners can now block such URLs in their console to decrease the likelihood of these failed page loads. This helps make the wandering experience smoother.

Another significant feature in this release is the expanded Console dialog box. This dialog box now shows various details about the console and the current wandering session. For example, it shows the console's configuration: recommended pages, ignored URLs and linked consoles. It also shows a wandering history screen where you can see each link that was recommended to you along with the console that recommendation came from. There is another screen that shows all the consoles discovered during the discovery process. Those who care about how Wander works would find this dialog box quite useful. To check it out, go to my Wander console and explore.

To learn more about Wander, how it works and how to set it up, please read the project README at codeberg.org/susam/wander.

Read on website | #web | #technology

]]>
Wander Console https://susam.net/wander/ wtswb Wed, 18 Mar 2026 00:00:00 +0000 I have put together a small tool to explore the small web of personal websites. It is called Wander. Please visit susam.net/wander/ to try out my Wander console.

If you have your own website, please consider joining this community by hosting your own Wander console. To do so, visit codeberg.org/susam/wander and follow the instructions there. Thank you!

Read on website | #web | #technology

]]>
Wander Console 0.1.0 https://susam.net/code/news/wander/0.1.0.html wnzoz Wed, 18 Mar 2026 00:00:00 +0000 Wander 0.1.0 is the first release of Wander, a small, decentralised, self-hosted web console that lets visitors to your website explore interesting websites and pages recommended by a community of independent personal website owners.

Anyone with a personal website can take this tool and host an instance of a Wander console. Each Wander console loads personal websites and pages recommended by the Wander community. Further, each Wander console can link to other Wander consoles, forming a lightweight, decentralised network for browsing the small web of personal websites.

Setting up an instance of a Wander console involves copying just two static files from the Wander project at codeberg.org/susam/wander. The most interesting aspect of the Wander console is that discovery of new links from other consoles happens on the client side in the user's web browser. As a website owner, you do not need to set up any server-side components beyond a basic web server. In fact, you can host a Wander console on GitHub Pages or Codeberg Pages too.

To learn more about Wander, how it works and how to set it up, please read the project README at codeberg.org/susam/wander.

Read on website | #web | #technology

]]>
Git Checkout, Reset and Restore https://susam.net/git-checkout-reset-restore.html gcrrp Thu, 12 Mar 2026 00:00:00 +0000 I have always used the git checkout and git reset commands to reset my working tree or index but since Git 2.23 there has been a git restore command available for these purposes. In this post, I record how some of the 'older' commands I use map to the new ones. Well, the new commands aren't exactly new since Git 2.23 was released in 2019, so this post is perhaps six years too late. Even so, I want to write this down for future reference. It is worth noting that the old and new commands are not always equivalent. I'll talk more about this briefly as we discuss the commands. However, they can be used to perform similar tasks. Some of these tasks are discussed below.

Contents

Experimental Setup

To experiment quickly, we first create an example Git repository.

mkdir foo/; cd foo/; touch a b c
git init; git add a b c; git commit -m hello

Now we make changes to the files and stage some of the changes. We then add more unstaged changes to one of the staged files.

date | tee a b c d; git add a b d; echo > b

At this point, the working tree and index look like this:

$ git status
On branch main
Changes to be committed:
  (use "git restore --staged <file>..." to unstage)
        modified:   a
        modified:   b
        new file:   d

Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git restore <file>..." to discard changes in working directory)
        modified:   b
        modified:   c

File a has staged changes. File b has both staged and unstaged changes. File c has only unstaged changes. File d is a new staged file. In each experiment below, we will work with this setup.

All results discussed in this post were obtained using Git 2.47.3 on Debian 13.2 (Trixie).

Reset the Working Tree

As a reminder, we will always use the following command between experiments to ensure that we restore the experimental setup each time:

date | tee a b c d; git add a b d; echo > b

To discard the changes in the working tree and reset the files in the working tree from the index, I typically run:

git checkout .

However, the modern way to do this is to use the following command:

git restore .

Both commands leave the working tree and the index in the following state:

$ git status
On branch main
Changes to be committed:
  (use "git restore --staged <file>..." to unstage)
        modified:   a
        modified:   b
        new file:   d

Both commands operate only on the working tree. They do not alter the index. Therefore the staged changes remain intact in the index.

Reset the Index

Another common situation is when we have staged some changes but want to unstage them. First, we restore the experimental setup:

date | tee a b c d; git add a b d; echo > b

I normally run the following command to do so:

git reset

The modern way to do this is:

git restore -S .

Both commands leave the working tree and the index in the following state:

$ git status
On branch main
Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git restore <file>..." to discard changes in working directory)
        modified:   a
        modified:   b
        modified:   c

Untracked files:
  (use "git add <file>..." to include in what will be committed)
        d

no changes added to commit (use "git add" and/or "git commit -a")

The -S (--staged) option tells git restore to operate on the index (not the working tree) and reset the index entries for the specified files to match the version in HEAD. The unstaged changes remain intact as modified files in the working tree. With the -S option, no changes are made to the working tree.

From the arguments we can see that the old and new commands are not exactly equivalent. Without any arguments, the git reset command resets the entire index to HEAD, so all staged changes become unstaged. Similarly, when we run git restore -S without specifying a commit, branch or tag using the -s (--source) option, it defaults to resetting the index from HEAD. The . at the end ensures that all paths under the current directory are affected. When we run the command at the top-level directory of the repository, all paths are affected and the entire index gets reset. As a result, both the old and the new commands accomplish the same result.

Reset the Working Tree and Index

Once again, we restore the experimental setup.

date | tee a b c d; git add a b d; echo > b

This time we not only want to unstage the changes but also discard the changes in the working tree. In other words, we want to reset both the working tree and the index from HEAD. This is a dangerous operation because any uncommitted changes discarded in this manner cannot be restored using Git.

git reset --hard

The modern way to do this is:

git restore -WS .

The working tree is now clean:

$ git status
On branch main
nothing to commit, working tree clean

The -W (--worktree) option makes the command operate on the working tree. The -S (--staged) option resets the index as described in the previous section. As a result, this command unstages any changes and discards any modifications in the working tree.

Note that when neither of these options is specified, -W is implied by default. That's why the bare git restore . command in the previous section discards the changes in the working tree.

Summary

The following table summarises how the three pairs of commands discussed above affect the working tree and the index, assuming the commands are run at the top-level directory of a repository.

Old New Working Tree Index
git checkout . git restore . Reset to match the index. No change.
git reset git restore -S . No change. Reset to match HEAD.
git reset --hard git restore -SW . Reset to match HEAD. Reset to match HEAD.

The git restore command is meant to provide a clearer interface for resetting the working tree and the index. I still use the older commands out of habit. Perhaps I will adopt the new ones in another six years, but at least I have the mapping written down now.

Read on website | #technology | #how-to

]]>
HN Skins 0.4.0 https://susam.net/code/news/hnskins/0.4.0.html hnsfr Tue, 10 Mar 2026 00:00:00 +0000 HN Skins 0.4.0 is a minor update to HN Skins, a web browser userscript that adds custom themes to Hacker News and lets you browse HN with a variety of visual styles. This release introduces a small fix to preserve the commemorative black bar that occasionally appears at the top of the page.

When a notable figure in technology or science passes away, Hacker News places a thin black bar at the top of the page in tribute. Previously some skins could obscure this element. This update ensures that the bar remains visible and clearly noticeable. In dark themed skins, the black bar is rendered as a lighter shade of grey so that it maintains sufficient contrast and remains conspicuous.

Today Hacker News has a story about Tony Hoare passing away, which made me notice that the commemorative black bar was not rendered properly with some skins. This prompted me to investigate the issue and implement the fix included in this release.

Screenshots showing how the bar appears with different skins are available at susam.github.io/blob/img/hnskins/0.4.0/.

To install HN Skins, visit github.com/susam/hnskins and follow the instructions there.

Read on website | #web | #programming | #technology

]]>
HN Skins 0.3.0 https://susam.net/code/news/hnskins/0.3.0.html hnsth Sat, 07 Mar 2026 00:00:00 +0000 HN Skins 0.3.0 is a minor update to HN Skins, a web browser userscript that adds custom themes to Hacker News and allows you to browse HN with a variety of visual styles. This release includes fixes for a few issues that slipped through earlier versions. For example, the comment input textbox now uses the same font face and size as the rest of the active theme. The colour of visited links has also been slightly muted to make it easier to distinguish them from unvisited links. In addition, some skins have been renamed: Teletype is now called Courier and Nox is now called Midnight.

Further, the font face of several monospace based themes is now set to monospace instead of courier. This allows the browser's preferred monospace font to be used. The font face of the Courier skin (formerly known as Teletype) remains set to courier. This will never change because the sole purpose of this skin is to celebrate this legendary font.

To view screenshots of HN Skins or install it, visit github.com/susam/hnskins.

Read on website | #web | #programming | #technology

]]>
HN Skins 0.2.0 https://susam.net/code/news/hnskins/0.2.0.html hnskt Sun, 01 Mar 2026 00:00:00 +0000 HN Skins 0.2.0 is a minor update of HN Skins. It comes a day after its initial release in order to fine tune a few minor issues with the styles in the initial release. HN Skins is a web browser userscript that adds custom themes to Hacker News and allows you to browse HN with different visual styles.

This update removes excessive vertical space below the 'reply' links, sorts the skin options alphabetically in the selection dialog and fixes the background colour of the navigation bar in the Terminal skin by changing it from a dark grey to a dark green.

Soon after making this release, I discovered a few other minor issues, such as the Cafe and Terminal themes using Courier when I intended them to use the system monospace font. This has already been fixed in the development version currently available on GitHub. However, I will make a formal release later.

See the changelog for more details. To see some screenshots of HN Skins or to install it, visit github.com/susam/hnlinks and follow the instructions there.

Read on website | #web | #programming | #technology

]]>
HN Skins 0.1.0 https://susam.net/code/news/hnskins/0.1.0.html hnsko Sat, 28 Feb 2026 00:00:00 +0000 HN Skins 0.1.0 is the initial release of HN Skins, a browser userscript that adds custom themes to Hacker News (HN). It allows you to browse HN in style with a selection of visual skins.

To use HN Skins, first install a userscript manager such as Greasemonkey, Tampermonkey or Violentmonkey in your web browser. Once installed, you can install HN Skins from github.com/susam/hnskins.

The source code is available under the terms of the MIT licence. For usage instructions and screenshots, please visit github.com/susam/hnskins.

Read on website | #web | #programming | #technology

]]>
Feb '26 Notes https://susam.net/26b.html ntfts Fri, 27 Feb 2026 00:00:00 +0000 Since last month, I have been collecting brief notes on ideas and references that caught my attention during each month but did not make it into full articles. Some of these fragments may eventually grow into standalone posts, though most will probably remain as they are. At the very least, this approach allows me to keep a record of them.

Most of last month's notes grew out of my reading of Algebraic Graph Theory by Godsil and Royle. I am still exploring and learning this subject. This month, however, I dove into another book with the same title but this book is written by Norman Biggs. As a result, many of the notes that follow are drawn from Biggs's treatment of the topic.

Since I already had a good understanding of the subject from the earlier book, I decided to skip the first fourteen chapters of the new book. I began with Chapter 15, which discusses automorphisms of graphs and then moved on to the following chapters on graph symmetries. My main reason for picking up Biggs's book was to understand Tutte's well known result that any \( s \)-arc-transitive finite cubic graph must satisfy \( s \le 5. \) While I did not reach that chapter this month, I made substantial progress with the book. I hope to work through the proof of Tutte's theorem next month.

Contents

  1. Degree of Vertices in an Orbit
  2. Regular Non-Vertex-Transitive Graphs
  3. Vertex-Transitive But Not Edge-Transitive
  4. Edge-Transitivex But Not Vertex-Transitive
  5. Bipartiteness as a Necessary Condition
  6. Graph with an Automorphism Group
  7. Permutation Groups Need Not Be Automorphism Groups
  8. Symmetric Graphs

Degree of Vertices in an Orbit

If two vertices of a graph belong to the same orbit, then they have the same degree. In other words, for a graph \( X, \) if \( x, y \in V(X) \) and there is an automorphism \( \alpha \) such that \( \alpha(x) = y, \) then \( \deg(x) = \deg(y). \)

The proof is quite straightforward. Let \begin{align*} N(x) &= \{ v_1, \dots, v_r \}, \\ N(y) &= \{ w_1, \dots, w_s \} \end{align*} represent the neighbours of \( x \) and \( y \) respectively. Therefore we have \[ x \sim v_1, \; \dots, \; x \sim v_r. \] Since an automorphism preserves adjacency, we get \[ \alpha(x) \sim \alpha(v_1), \; \dots, \; \alpha(x) \sim \alpha(v_r). \] Substituting \( \alpha(x) = y, \) we get \[ y \sim \alpha(v_1), \; \dots, \; y \sim \alpha(v_r). \] Thus \[ \alpha(N(x)) = \{ \alpha(v_1), \; \dots, \; \alpha(v_r) \} \subseteq N(y). \] A similar argument works in reverse as well. By the definition of automorphism, if \( \alpha \) is an automorphism, so is \( \alpha^{-1}. \) From the definition of \( N(y) \) above, we have \[ y \sim w_1, \; \dots, \; y \sim w_s. \] Therefore \[ \alpha^{-1}(y) \sim \alpha^{-1}(w_1), \; \dots, \; \alpha^{-1}(y) \sim \alpha^{-1}(w_s). \] This is equivalent to \[ x \sim \alpha^{-1}(w_1), \; \dots, \; x \sim \alpha^{-1}(w_s). \] Thus \[ \alpha^{-1}(N(y)) = \{ \alpha^{-1}(w_1), \; \dots, \; \alpha^{-1}(w_s) \} \subseteq N(x) \] This can be rewritten as \[ \{ \alpha^{-1}(w_1), \; \dots, \; \alpha^{-1}(w_s) \} \subseteq \{ v_1, \dots, v_r \}. \] Therefore \[ N(y) = \{ w_1, \dots, w_s \} \subseteq \{ \alpha(v_1), \dots, \alpha(v_r) \} = \alpha(N(x)). \] We have shown that \( \alpha(N(x)) \subseteq N(y) \) and \( N(y) \subseteq \alpha(N(x)). \) Thus \[ \alpha(N(x)) = N(y). \] Thus \[ \lvert N(y) \rvert = \lvert \alpha(N(x)) \rvert = r. \] Therefore both \( x \) and \( y \) have \( r \) neighbours each. Hence \( \deg(x) = \deg(y). \)

Regular Non-Vertex-Transitive Graphs

The Frucht graph and the Folkman graph are examples of graphs that are \( k \)-regular but not vertex-transitive. In fact, the Folkman graph is a semi-symmetric graph, i.e. it is regular and edge-transitive but not vertex-transitive.

Vertex-Transitive But Not Edge-Transitive

The circular ladder graph \( CL_3, \) i.e. the triangular prism graph, is vertex-transitive but not edge-transitive.

Every vertex has the same local structure. Every vertex has degree \( 3 \) and it lies on exactly one of the two triangles and it has exactly one 'vertical' edge connecting it to the corresponding edge on the other triangle. Any vertex can be sent to any other by an automorphism.

Since triangle edges are in a triangle and vertical edges are in no triangle, no automorphism can send a triangle edge to a vertical edge or vice versa. Therefore the graph is not edge-transitive.

Edge-Transitivex But Not Vertex-Transitive

The complete bipartite graphs \( K_{m,n} \) with \( m \ne n \) are edge-transitive but not vertex-transitive.

Every edge connects one vertex from the \( m \)-part to one vertex from the \( n \)-part. Any permutation of vertices inside the \( m \)-part preserves adjacency. Similarly, any permutation of vertices inside the \( n \)-part preserves adjacency.

Take two arbitrary edges \[ uv, \; u'v' \in E(K_{m,n}) \] where \( u, u' \) are vertices that lie in the \( m \)-part and \( v, v' \) are vertices that lie in the \( n \)-part. Permute vertices within the \( m \)-part to send \( u \) to \( u'. \) Similarly, permute vertices within the \( n \)-part to send \( v \) to \( v'. \) This gives an automorphism that sends the edge \( uv \) to \( u'v'. \) In this manner we can find an automorphism that sends any edge to any other. Therefore, \( K_{m,n} \) is edge-transitive.

However, \( K_{m,n} \) is not vertex-transitive since no automorphism can send a vertex in the \( m \)-part to a vertex in the \( n \)-part since the vertices in the \( m \)-part have degree \( n \) and the vertices in the \( n \)-part have degree \( m. \)

Bipartiteness as a Necessary Condition

If a connected graph is edge-transitive but not vertex-transitive, then it must be bipartite.

Graph with an Automorphism Group

In 1938, Frucht proved that for every finite abstract group \( G, \) there exists a graph whose automorphism group is isomorphic to \( G . \)

Remarkably, this result remains valid even when we restrict our attention to cubic graphs. That is, for every finite abstract group \( G, \) there exists a cubic graph whose automorphism group is isomorphic to \( G. \) Moreover, the result has been extended to graphs satisfying various additional graph-theoretical properties, such as \( k \)-connectivity, \( k \)-regularity and prescribed chromatic number.

Permutation Groups Need Not Be Automorphism Groups

Consider the following specialised version of the problem discussed in the previous section: Given a permutation group on a set \( X, \) must there exist a graph with vertex set \( X \) whose automorphism group is precisely that permutation group?

The answer is no. Consider the cyclic group \( C_3 \) acting on \( X = \{ a, b, c \}. \) There is no graph \( \Gamma \) with \( V(\Gamma) = X \) and \( \operatorname{Aut}(\Gamma) \cong C_3. \) If we take \( \Gamma = K_3, \) then \( C_3 \subset S_3 = \operatorname{Aut}(K_3) \) but \( C_3 \ne \operatorname{Aut}(K_3) . \)

Symmetric Graphs

It is interesting that while we study graph symmetry through concepts such as graph automorphisms, vertex-transitivity, edge-transitivity, etc. the name symmetric graph is reserved for graphs that are \( 1 \)-arc-transitive. A vertex-transitive graph or an edge-transitive graph need not be \(1\)-arc-transitive and therefore need not be symmetric.

However, every \( s \)-arc-transitive graph is \(1 \)-arc-transitive for \( s \ge 1. \) Consequently, every \( s \)-arc-transitive graph is symmetric. Moreover, every distance-transitive graph is also \( 1 \)-arc-transitive and hence symmetric.

Formally, we say that a graph \( \Gamma \) is \( 1 \)-arc-transitive (or equivalently, symmetric) if for all \( 1 \)-arcs \( uv \) and \( u'v' \) of \( \Gamma, \) there is an automorphism \( \alpha \in \operatorname{Aut}(\Gamma) \) such that \( \alpha(uv) = u'v'. \)

Stated in more basic terms, we can say that \( \Gamma \) is symmetric if for all \( u, v, u', v' \in V(\Gamma) \) satisfying \( u \sim v \) and \( u' \sim v', \) there exists \( \alpha \in \operatorname{Aut}(\Gamma) \) such that \( \alpha(u) = u' \) and \( \alpha(v) = v'. \)

Switching gears now, we say that \( \Gamma \) is distance-transitive if for all \( u, v, u', v' \in V(\Gamma) \) satisfying \( d(u, v) = d(u', v'), \) there exists \( \alpha \in \operatorname{Aut}(\Gamma) \) such that \( \alpha(u) = u' \) and \( \alpha(v) = v'. \) Since all \( 1 \)-arcs \( uv \) and \( u'v' \) satisfy \( d(u, v) = d(u', v') = 1, \) distance-transitivity implies that there is an automorphism that sends \( uv \) to \( u'v'. \) Therefore a distance-transitive graph is also \( 1 \)-arc-transitive.

To summarise, a graph must possess a certain degree of symmetry in order to be called symmetric. It turns out that merely having a non-trivial automorphism group is not sufficient. Even being vertex-transitive or edge-transitive is not enough for a graph to be called symmetric. The graph needs to be at least \( 1 \)-arc-transitive to be called symmetric.

Another interesting aspect of this terminology is that the property of being asymmetric is not the exact opposite of being symmetric. For example, a vertex-transitive graph need not be symmetric. However, that does not make it asymmetric. A graph is called asymmetric if it has no non-trivial automorphisms, i.e. its automorphism group contains only the identity permutation. Thus, if a graph has at least two vertices and is vertex-transitive, it must admit a non-trivial automorphism that maps one vertex to another. So while such a vertex-transitive may not be symmetric, it isn't asymmetric either.

Read on website | #notes | #mathematics

]]>
Nerd Quiz #4 https://susam.net/code/news/nq/4.0.0.html nqfou Sun, 22 Feb 2026 00:00:00 +0000 Nerd Quiz #4 is the fourth instalment of Nerd Quiz, a single page HTML application that challenges you to measure your inner geek with a brief quiz. Each question in the quiz comes from everyday moments of reading, writing, thinking, learning and exploring.

This release introduces five new questions drawn from a range of topics, including computing history, graph theory and Unix. Visit Nerd Quiz to try the quiz.

A community discussion page is available here. You are very welcome to share your score or discuss the questions there.

Read on website | #web | #miscellaneous | #game

]]>
Deep Blue: Chess vs Programming https://susam.net/deep-blue.html dblue Sun, 15 Feb 2026 00:00:00 +0000 I remember how dismayed Kasparov was after losing the 1997 match to IBM's Deep Blue, although his views on Deep Blue became more balanced with time and he accepted that we had entered a new era in which computers would outperform grandmasters at chess.

Still, chess players can take comfort in the fact that chess is still played between humans. Players make their name and fame by beating other humans because playing against computers is no longer interesting as a competition.

Many software developers would like to have similar comfort. But that comfort is harder to find, because unlike chess, building prototypes or PoCs is not seen as a sport or art form. It is mostly seen as a utility. So while brain-coding a PoC may still be intellectually satisfying for the programmer, to most other people it only matters that the thing works. That means that programmers do not automatically get the same protected space that chess players have, where the human activity itself remains valued even after machines become stronger. The activity programmers enjoy may continue but the recognition and economic value attached to it may shrink.

So I think the big adjustment software developers have to make is this: The craft will still exist and we will still enjoy doing it but the credit and value will increasingly go to those who define problems well, connect systems, make good product decisions and make technology useful in messy real-world situations. It has already been this way for a while and will only become more so as time goes by.


This note reproduces a recent comment I posted in a Lobsters forum thread about LLM-assisted software development at at lobste.rs/s/qmjejh.

See also: Three Inverse Laws of AI and Robotics.

Read on website | #miscellaneous

]]>
Soju User Delete Hash https://susam.net/soju-user-delete-hash.html sudhs Sat, 14 Feb 2026 00:00:00 +0000 In my last post, I talked about switching from ZNC to Soju as my IRC bouncer. One thing that caught my attention while creating and deleting Soju users was that the delete command asks for a confirmation, like so:

$ sudo sojuctl user delete soju
To confirm user deletion, send "user delete soju 4664cd"
$ sudo sojuctl user delete soju 4664cd
deleted user "soju"

That confirmation token for a specific user never changes, no matter how many times we create or delete it. The confirmation token is not saved in the Soju database, as can be confirmed here:

$ sudo sqlite3 -table /var/lib/soju/main.db 'SELECT * FROM User'
+----+----------+--------------------------------------------------------------+-------+----------+------+--------------------------+---------+--------------------------+--------------+
| id | username |                           password                           | admin | realname | nick |        created_at        | enabled | downstream_interacted_at | max_networks |
+----+----------+--------------------------------------------------------------+-------+----------+------+--------------------------+---------+--------------------------+--------------+
| 1  | soju     | $2a$10$yRj/oYlR2Zwd8YQxZPuAQuNo2j7FVJWeNdIAHF2MinYkKLmBjtf0y | 0     |          |      | 2026-02-16T13:49:46.119Z | 1       |                          | -1           |
+----+----------+--------------------------------------------------------------+-------+----------+------+--------------------------+---------+--------------------------+--------------+

Surely, then, the confirmation token is derived from the user definition? Yes, indeed it is. This can be confirmed at the source code here. Quoting the most relevant part from the source code:

hashBytes := sha1.Sum([]byte(username))
hash := fmt.Sprintf("%x", hashBytes[0:3])

Indeed if we compute the same hash ourselves, we get the same token:

$ printf soju | sha1sum | head -c6
4664cd

This allows us to automate the two step Soju user deletion process in a single command:

sudo sojuctl user delete soju "$(printf soju | sha1sum | head -c6)"

But of course, the implementation of the confirmation token may change in future and Soju helpfully outputs the deletion command with the confirmation token when we first invoke it without the token, so it is perhaps more prudent to just take that output and feed it back to Soju, like so:

sudo sojuctl $(sudo sojuctl user delete soju | sed 's/.*"\(.*\)"/\1/')

Read on website | #shell | #irc | #technology | #how-to

]]>
From ZNC to Soju https://susam.net/from-znc-to-soju.html fztsj Thu, 12 Feb 2026 00:00:00 +0000 I have recently switched from ZNC to Soju as my IRC bouncer and I am already quite pleased with it. I usually run my bouncer on a Debian machine, where Soju is well packaged and runs smoothly right after installation. By contrast, the ZNC package included with Debian 13 (Trixie) and earlier fails to start after installation because of a missing configuration file. As a result, I was forced to maintain my own configuration file along with a necessary PEM bundle, copy them to the Debian system and carefully set the correct file permissions before I could run ZNC successfully. None of this is necessary with Soju, since installing it from the Debian package repository automatically sets up the configuration and certificate files. I no longer have to manage any configuration or certificate files myself.

Setup

It is quite straightforward to install and set up Soju on Debian. The following two commands install Soju:

sudo apt-get update
sudo apt-get -y install soju

Then setting up an IRC connection involves another two commands:

sudo sojuctl user create -username soju -password YOUR_SOJU_PASSWORD
sudo sojuctl user run soju network create -name bnc1 -addr irc.libera.chat -nick YOUR_NICK -pass YOUR_NICK_PASSWORD

Here, YOUR_SOJU_PASSWORD is a placeholder for a new password you must choose for your Soju user. Finally, we restart Soju as follows:

sudo systemctl restart soju

Database

What previously involved maintaining several files that had to be installed and configured on each machine running ZNC is now reduced to the two sojuctl commands above. Still, the configuration needs to live somewhere. In fact, the two sojuctl commands introduce earlier store the configuration in a SQLite database. Here is a glimpse of what the database looks like:

$ sudo sqlite3 /var/lib/soju/main.db '.tables'
Channel              MessageFTS_data      ReadReceipt
DeliveryReceipt      MessageFTS_docsize   User
Message              MessageFTS_idx       WebPushConfig
MessageFTS           MessageTarget        WebPushSubscription
MessageFTS_config    Network
$ sudo sqlite3 /var/lib/soju/main.db 'SELECT * from User'
1|soju|$2a$10$mM5Qcz8.OPMi9lyWDxPRh.bNxzq7jtLdxcoPl09AYTnqcmLmEqzSO|0|||2026-02-17T23:24:24.926Z|1||-1
$ sudo sqlite3 /var/lib/soju/main.db 'SELECT * from Network'
1|bnc1|1|irc.libera.chat|YOUR_NICK||||YOUR_NICK_PASSWORD|||||||1|1

Client Configuration

Finally, the IRC client can be configured to connect to port 6697 on the system running Soju. Here is an example of how this can be done in Irssi:

/network add -nick YOUR_NICK -user soju/bnc1 net1
/server add -tls -network bnc1 YOUR_SOJU_HOST 6697 YOUR_SOJU_PASSWORD
/connect net1

You can also set up multiple connections to IRC networks through the same Soju instance. All you need to do is repeat the sojuctl commands to create additional networks such as bnc2, bnc3 and so on, then repeat the configuration in your IRC client using new network names such as net2, net3, etc. These network names are entirely user defined, so you can choose any names you like. The names bnc2, net2 and so on are only examples.

Read on website | #irc | #technology | #how-to

]]>
Twenty Five Years of Computing https://susam.net/twenty-five-years-of-computing.html tfyoc Fri, 06 Feb 2026 00:00:00 +0000 Last year, I completed 20 years in professional software development. I wanted to write a post to mark the occasion back then, but couldn't find the time. This post is my attempt to make up for that omission. In fact, I have been involved in software development for a little longer than 20 years. Although I had my first taste of computer programming as a child, it was only when I entered university about 25 years ago that I seriously got into software development. So I'll start my stories from there. These stories are less about software and more about people. Unlike many posts of this kind, this one offers no wisdom or lessons. It only offers a collection of stories. I hope you'll like at least a few of them.

Contents

Viewing the Source

The first story takes place in 2001, shortly after I joined university. One evening, I went to the university computer laboratory to browse the World Wide Web. Out of curiosity, I typed susam.com into the address bar and landed on its home page. I remember the text and banner looking much larger back then. Display resolutions were lower, so the text and banner covered almost half the screen. I knew very little about the Internet then and I was just trying to make sense of it. I remember wondering what it would take to create my own website, perhaps at susam.com. That's when an older student who had been watching me browse over my shoulder approached and asked if I had created the website. I told him I hadn't and that I had no idea how websites were made. He asked me to move aside, took my seat and clicked View > Source in Internet Explorer. He then explained how websites are made of HTML pages and how those pages are simply text instructions.

Next, he opened Notepad and wrote a simple HTML page that looked something like this:

<BODY><FONT COLOR="RED">HELLO</FONT></BODY>

Yes, we had a FONT tag back then and it was common practice to write HTML tags in uppercase. He then opened the page in a web browser and showed how it rendered. After that, he demonstrated a few more features such as changing the font face and size, centring the text and altering the page's background colour. Although the tutorial lasted only about ten minutes, it made the Web feel far less mysterious and much more fascinating.

That person had an ulterior motive though. After the tutorial, he never returned the seat to me. He just continued browsing the Web and waited for me to leave. I was too timid to ask for my seat back. Seats were limited, so I returned to my dorm room both disappointed that I couldn't continue browsing that day and excited about all the websites I might create with this newfound knowledge. I could never register susam.com for myself though. That domain was always used by some business selling Turkish cuisines. Eventually, I managed to get the next best thing: a .net domain of my own. That brief encounter in the university laboratory set me on a lifelong path of creating and maintaining personal websites.

The Reset Vector

The second story also comes from my university days. One afternoon, I was hanging out with my mates in the computer laboratory. In front of me was an MS-DOS machine powered by an Intel 8086 microprocessor, on which I was writing a lift control program in assembly. In those days, it was considered important to deliberately practise solving made-up problems as a way of honing our programming skills. As I worked on my program, my mind drifted to a small detail about the 8086 microprocessor that we had recently learnt in a lecture. Our professor had explained that, when the 8086 microprocessor is reset, execution begins with CS:IP set to FFFF:0000. So I murmured to anyone who cared to listen, 'I wonder if the system will reboot if I jump to FFFF:0000.' I then opened DEBUG.EXE and jumped to that address.

C:\>DEBUG
-G =FFFF:0000

The machine rebooted instantly. One of my friends, who topped the class every semester, had been watching over my shoulder. As soon as the machine restarted, he exclaimed, 'How did you do that?' I explained that the reset vector is located at physical address FFFF0 and that the CS:IP value FFFF:0000 maps to that address in real mode. After that, I went back to working on my lift control program and didn't think much more about the incident.

About a week later, the same friend came to my dorm room. He sat down with a grave look on his face and asked, 'How did you know to do that? How did it occur to you to jump to the reset vector?' I must have said something like, 'It just occurred to me. I remembered that detail from the lecture and wanted to try it out.' He then said, 'I want to be able to think like that. I come top of the class every semester, but I don't think the way you do. I would never have thought of taking a small detail like that and testing it myself.' I replied that I was just curious to see whether what we had learnt actually worked in practice. He responded, 'And that's exactly it. It would never occur to me to try something like that. I feel disappointed that I keep coming top of the class, yet I am not curious in the same way you are. I've decided I don't want to top the class anymore. I just want to explore and experiment with what we learn, the way you do.'

That was all he said before getting up and heading back to his dorm room. I didn't take it very seriously at the time. I couldn't imagine why someone would willingly give up the accomplishment of coming first every semester. But he kept his word. He never topped the class again. He still ranked highly, often within the top ten, but he kept his promise of never finishing first again. To this day, I feel a mix of embarrassment and pride whenever I recall that incident. With a single jump to the processor's reset entry point, I had somehow inspired someone to step back from academic competition in order to have more fun with learning. Of course, there is no reason one cannot do both. But in the end, that was his decision, not mine.

Man in the Middle

In my first job after university, I was assigned to a technical support team where part of my work involved running an installer to deploy a specific component of an e-banking product for customers, usually large banks. As I learnt to use the installer, I realised how fragile it was. The installer, written in Python, often failed because of incorrect assumptions about the target environment and almost always required some manual intervention to complete successfully. During my first week on the project, I spent much of my time stabilising the installer and writing a step-by-step user guide explaining how to use it. The result was well received by both my seniors and management. To my surprise, the user guide received more praise than the improvements I made to the installer. While the first few weeks were productive, I soon realised I would not find the work fulfilling for long. I wrote to management a few times to ask whether I could transfer to a team where I could work on something more substantial.

My emails were initially met with resistance. After several rounds of discussion, someone who had heard about my situation reached out and suggested a team whose manager might be interested in interviewing me. The team was based in a different city. I was young and willing to relocate wherever I could find good work, so I immediately agreed to the interview.

This was in 2006, when video conferencing was not yet common. On the day of the interview, the hiring manager called me on my office desk phone. He began by introducing the team, which was called Archie, short for architecture. The team developed and maintained the web framework and core architectural components on which the entire e-banking product was built. The product had existed long before open source frameworks such as Spring or Django came into existence, so features such as API routing, authentication and authorisation layers, cookie management, etc. were all implemented in-house as Java Servlets and JavaServer Pages (JSP). Since the software was used in banking environments, it also had to pass security testing and regular audits to minimise the risk of serious flaws.

The interview began well. He asked several questions related to software security, such as what SQL injection is, how it can be prevented and how one might design a web framework that mitigates cross-site scripting attacks. He also asked me a few programming questions, most which I answered pretty well. Towards the end, however, he asked how we could prevent MITM attacks. I had never heard the term, so I admitted that I did not know what MITM meant. He then asked, 'Man in the middle?' but I still had no idea what that meant or whether it was even a software engineering concept. He replied, 'Learn everything you can about PKI and MITM. We need to build a digital signatures feature for one of our corporate banking products. That's the first thing we'll work on.'

Over the next few weeks, I studied RFCs and documentation related to public key infrastructure, public key cryptography standards and related topics. At first, the material felt intimidating, but after spending time each evening reading whatever relevant literature I could find, things gradually began to make sense. Concepts that initially seemed complex and overwhelming eventually felt intuitive and elegant. I relocated to the new city a few weeks later and delivered the digital signatures feature about a month after joining the team. We used the open source Bouncy Castle library to implement the feature. After that project, I worked on other parts of the product too. The most rewarding part was knowing that the code I was writing became part of a mature product used by hundreds of banks and millions of users. It was especially satisfying to see the work pass security testing and audits and be considered ready for release.

That was my first real engineering job. My manager also turned out to be an excellent mentor. Working with him helped me develop new skills and his encouragement gave me confidence that stayed with me for years. Nearly two decades have passed since then, yet the product is still in service and continues to be actively developed. In fact, in my current phase of life I sometimes encounter it as a customer. Occasionally, I open the browser's developer tools to view the page source where I can still see traces of the HTML generated by code I wrote almost twenty years ago.

Sphagetti Code

Around 2007 or 2008, I began working on a proof of concept for developing widgets for an OpenTV set-top box. The work involved writing code in a heavily trimmed-down version of C. One afternoon, while making good progress on a few widgets, I noticed that they would occasionally crash at random. I tried tracking down the bugs, but I was finding it surprisingly difficult to understand my own code. I had managed to produce some truly spaghetti code full of dubious pointer operations that were almost certainly responsible for the crashes, yet I could not pinpoint where exactly things were going wrong.

Ours was a small team of four people, each working on an independent proof of concept. The most senior person on the team acted as our lead and architect. Later that afternoon, I showed him my progress and explained that I was still trying to hunt down the bugs causing the widgets to crash. He asked whether he could look at the code. After going through it briefly and probably realising that it was a bit of a mess, he asked me to send him the code as a tarball, which I promptly did.

He then went back to his desk to study the code. I remember thinking that there was no way he was going to find the problem anytime soon. I had been debugging it for hours and barely understood what I had written myself; it was the worst spaghetti code I had ever produced. With little hope of a quick solution, I went back to debugging on my own.

Barely five minutes later, he came back to my desk and asked me to open a specific file. He then showed me exactly where the pointer bug was. It had taken him only a few minutes not only to read my tangled code but also to understand it well enough to identify the fault and point it out. As soon as I fixed that line, the crashes disappeared. I was genuinely in awe of his skill.

I have always loved computing and programming, so I had assumed I was already fairly good at it. That incident, however, made me realise how much further I still had to go before I could consider myself a good software developer. I did improve significantly in the years that followed and today I am far better at managing software complexity than I was back then.

Animated Television Widgets

In another project from that period, we worked on another set-top box platform that supported Java Micro Edition (Java ME) for widget development. One day, the same architect from the previous story asked whether I could add animations to the widgets. I told him that I believed it should be possible, though I'd need to test it to be sure. Before continuing with the story, I need to explain how the different stakeholders in the project were organised.

Our small team effectively played the role of the software vendor. The final product going to market would carry the brand of a major telecom carrier, offering direct-to-home (DTH) television services, with the set-top box being one of the products sold to customers. The set-top box was manufactured by another company. So the project was a partnership between three parties: our company as the software vendor, the telecom carrier and the set-top box manufacturer. The telecom carrier wanted to know whether widgets could be animated on screen with smooth slide-in and slide-out effects. That was why the architect approached me to ask whether it could be done.

I began working on animating the widgets. Meanwhile, the architect and a few senior colleagues attended a business meeting with all the partners present. During the meeting, he explained that we were evaluating whether widget animations could be supported. The set-top box manufacturer immediately dismissed the idea, saying, 'That's impossible. Our set-top box does not support animation.' When the architect returned and shared this with us, I replied, 'I do not understand. If I can draw a widget, I can animate it too. All it takes is clearing the widget and redrawing it at slightly different positions repeatedly. In fact, I already have a working version.' I then showed a demo of the animated widgets running on the emulator.

The following week, the architect attended another partners' meeting where he shared updates about our animated widgets. I was not personally present, so what follows is second-hand information passed on by those who were there. I learnt that the set-top box company reacted angrily. For some reason, they were unhappy that we had managed to achieve results using their set-top box and APIs that they had officially described as impossible. They demanded that we stop work on animation immediately, arguing that our work could not be allowed to contradict their official position. At that point, the telecom carrier's representative intervened and bluntly told the set-top box representative to just shut up. If the set-top box guy was furious, the telecom guy was even more so, 'You guys told us animation was not possible and these people are showing that it is! You manufacture the set-top box. How can you not know what it is capable of?'

Meanwhile, I continued working on the proof of concept. It worked very well in the emulator, but I did not yet have access to the actual hardware. The device was still in the process of being shipped to us, so all my early proof-of-concepts ran on the emulator. The following week, the architect planned to travel to the set-top box company's office to test my widgets on the real hardware.

At the time, I was quite proud of demonstrating results that even the hardware maker believed were impossible. When the architect eventually travelled to test the widgets on the actual device, a problem emerged. What looked like buttery smooth animation on the emulator appeared noticeably choppy on a real television. Over the next few weeks, I experimented with frame rates, buffering strategies and optimising the computation done in the the rendering loop. Each week, the architect travelled for testing and returned with the same report: the animation had improved somewhat, but it still remained choppy. The modest embedded hardware simply could not keep up with the required computation and rendering. In the end, the telecom carrier decided that no animation was better than poor animation and dropped the idea altogether. So in the end, the set-top box developers turned out to be correct after all.

Good Blessings

Back in 2009, after completing about a year at RSA Security, I began looking for work that felt more intellectually stimulating, especially projects involving mathematics and algorithms. I spoke with a few senior leaders about this, but nothing materialised for some time. Then one day, Dr Burt Kaliski, Chief Scientist at RSA Laboratories, asked to meet me to discuss my career aspirations. I have written about this in more detail in another post here: Good Blessings. I will summarise what followed.

Dr Kaliski met me and offered a few suggestions about the kinds of teams I might approach to find more interesting work. I followed his advice and eventually joined a team that turned out to be an excellent fit. I remained with that team for the next six years. During that time, I worked on parser generators, formal language specification and implementation, as well as indexing and querying engines of a petabyte-scale database. I learnt something new almost every day during those six years. It remains one of the most enjoyable periods of my career. I have especially fond memories of working on parser generators alongside remarkably skilled engineers from whom I learnt a lot.

Years later, I reflected on how that brief meeting with Dr Kaliski had altered the trajectory of my career. I realised I was not sure whether I had properly expressed my gratitude to him for the role he had played in shaping my path. So I wrote to thank him and explain how much that single conversation had influenced my life. A few days later, Dr Kaliski replied, saying he was glad to know that the steps I took afterwards had worked out well. Before ending his message, he wrote this heart-warming note:

‘One of my goals is to be able to provide encouragement to others who are developing their careers, just as others have invested in mine, passing good blessings from one generation to another.’

The CTF Scoreboard

This story comes from 2019. By then, I was no longer a twenty-something engineer just starting out. I was now a middle-aged staff engineer with years of experience building both low-level networking systems and database systems. Most of my work up to that point had been in C and C++. I was now entering a new phase of my career where I would be leading the development of microservices written in Go and Python. Like many people in this profession, computing has long been one of my favourite hobbies. So although my professional work for the previous decade had focused on C and C++, I had plenty of hobby projects in other languages, including Python and Go. As a result, switching gears from systems programming to application development was a smooth transition for me. I cannot even say that I missed working in C and C++. After all, who wants to spend their days occasionally chasing memory bugs in core dumps when you could be building features and delivering real value to customers?

In October 2019, during Cybersecurity Awareness Month, a Capture the Flag (CTF) event was organised at our office. The contest featured all kinds of technical puzzles, ranging from SQL injection challenges to insecure cryptography problems. Some challenges also involved reversing binaries and exploiting stack overflow issues.

I am usually rather intimidated by such contests. The whole idea of competitive problem-solving under time pressure tends to make me nervous. But one of my colleagues persuaded me to participate in the CTF. And, somewhat to my surprise, I turned out to be rather good at it. Within about eight hours, I had solved roughly 90% of the puzzles. I finished at the top of the scoreboard.

Scoreboard of a Capture the Flag (CTF) event
CTF Scoreboard

In my younger days, I was generally known to be a good problem solver. I was often consulted when thorny problems needed solving and I usually managed to deliver results. I also enjoyed solving puzzles. I had a knack for them and happily spent hours, sometimes days, working through obscure mathematical or technical puzzles and sharing detailed write-ups with friends of the nerd variety. Seen in that light, my performance at the CTF probably should not have surprised me. Still, I was very pleased. It was reassuring to know that I could still rely on my systems programming experience to solve obscure challenges.

During the course of the contest, my performance became something of a talking point in the office. Colleagues occasionally stopped by my desk to appreciate my progress in the CTF. Two much younger colleagues, both engineers I admired for their skill and professionalism, were discussing the results nearby. They were speaking softly, but I could still overhear parts of their conversation. Curious, I leaned slightly and listened a bit more carefully. I wanted to know what these two people, whom I admired a lot, thought about my performance.

One of them remarked on how well I was doing in the contest. The other replied, 'Of course he is doing well. He has more than ten years of experience in C.' At that moment, I realised that no matter how well I solved those puzzles, the result would naturally be credited to experience. In my younger days, when I solved tricky problems like these, people would sometimes call me smart. Now people simply saw it as a consequence of my experience. Not that I particularly care for labels such as 'smart' anyway, but it did make me realise how things had changed. I was now simply the person with many years of experience. Solving technical puzzles that involved disassembling binaries, tracing execution paths and reconstructing program logic was expected rather than remarkable.

I continue to sharpen my technical skills to this day. While my technical results may now simply be attributed to experience, I hope I can continue to make a good impression through my professionalism, ethics and kindness towards the people I work with. If those leave a lasting impression, that is good enough for me.

Read on website | #technology | #programming

]]>
Jan '26 Notes https://susam.net/26a.html ntjts Thu, 29 Jan 2026 00:00:00 +0000 In these monthly notes, I jot down ideas and references I encountered during the month that I did not have time to expand into their own posts. A few of these may later develop into independent posts but most of them will likely not. In any case, this format ensures that I record them here. I spent a significant part of this month studying the book Algebraic Graph Theory by Godsil and Royle, so many of the notes here are about it. There are a few non-mathematical, technical notes towards the end.

Contents

  1. Cayley Graphs
  2. Vertex-Transitive Graphs
  3. Arc-Transitive Graphs
  4. Bipartite Graphs and Cycle Parity
  5. Tutte's Theorem
  6. Tutte's 8-Cage
  7. Linear Congruential Generator
  8. Numbering Lines

Cayley Graphs

Let \( G \) be a group and let \( C \subseteq G \) such that \( C \) is closed under taking inverses and does not contain the identity, i.e. \[ \forall x \in C, \; x^{-1} \in C, \qquad e \notin C. \] Then the Cayley graph \( X(G, C) \) is the graph with the vertex set \( V(X(G, C)) \) and edge set \( E(X(G, C)) \) defined by \begin{align*} V(X(G, C)) &= G, \\ E(X(G, C)) &= \{ gh : hg^{-1} \in C \}. \end{align*} The set \( C \) is known as the connection set.

Vertex-Transitive Graphs

A graph \( X \) is vertex-transitive if its automorphism group acts transitively on its set of vertices \( V(X). \) Intuitively, this means that no vertex has a special role. We can 'move' the graph around so that any chosen vertex becomes any other vertex. In other words, all vertices are indistinguishable. The graph looks the same from each vertex.

The \( k \)-cube \( Q_k \) is vertex-transitive. So are the Cayley graphs \( X(G, C). \) However the path graph \( P_3 \) is not vertex-transitive since no automorphism can send the middle vertex of valency \( 2 \) to an end vertex of valency \( 1. \)

Arc-Transitive Graphs

The cube \( Q_3 \) is \( 2 \)-arc-transitive but not \( 3 \)-arc-transitive. In \( Q_3, \) a \( 3 \)-arc belonging to a \( 4 \)-cycle cannot be sent to a \( 3 \)-arc that does not belong to a \( 4 \)-cycle. This is easy to explain. The end vertices of a \( 3 \)-arc belonging to a \( 4 \)-cycle are adjacent but the end vertices of a \( 3 \)-arc not belonging to a \( 4 \)-cycle are not adjacent. Therefore, no automorphism can map the end vertices of the first \( 3 \)-arc to those of the second \( 3 \)-arc.

For intuition, imagine that a traveller stands on a vertex and chooses an edge to move along. They do this \( s \) times thereby walking along an arc of length \( s, \) also known as an \( s \)-arc. By the definition of \( s \)-arcs, the traveller is not allowed to backtrack from one vertex to the previous one immediately. In an \( s \)-arc-transitive graph, these arcs look the same no matter which vertex they start from or which edges they choose. In the cube, this is indeed true for \( s = 2. \) All arcs of length \( 2 \) are indistinguishable. No matter which arc of length \( 2 \) the traveller has walked along, the graph would look the same from their perspective at each vertex along the arc. However, this no longer holds good for arcs of length \( 3 \) since there are two distinct kinds of arcs of length \( 3. \) The first kind ends at a distance of \( 1 \) from the starting vertex of the arc (when the arc belongs to a \( 4 \)-cycle). The second kind ends at a distance \( 3 \) from the starting vertex of the arc (when the arc does not belong to a \( 4 \)-cycle). Therefore the cube is not \( 3 \)-arc-transitive.

Bipartite Graphs and Cycle Parity

A graph is bipartite if and only if it contains no cycles of odd length. Equivalently, every cycle in a bipartite graph has even length. Conversely, if every cycle in a graph has even length, then the graph is bipartite.

Tutte's Theorem

For any \( s \)-arc-transitive cubic graph, \( s \le 5. \) This was demonstrated by W. T. Tutte in 1947. A proof can be found in Chapter 18 of Algebraic Graph Theory by Norman Biggs.

In 1973, Richward Weiss established a more general theorem that proves that for any \( s \)-arc-transitive graph, \( s \le 7. \) The bound is weaker but it applies to all graphs rather than only to cubic ones.

Tutte's 8-Cage

The book Algebraic Graph Theory by Godsil and Royle offers the following two descriptions of Tutte's 8-cage on 30 vertices:

Take the cube and an additional vertex \( \infty. \) In each set of four parallel edges, join the midpoint of each pair of opposite edges by an edge, then join the midpoint of the two new edges by an edge, and finally join the midpoint of this edge to \( \infty. \)
Construct a bipartite graph \( T \) with the fifteen edges as one colour class and the fifteen \( 1 \)-factors as the other, where each edge is adjacent to the three \( 1 \)-factors that contain it.

It can be shown that both descriptions construct a cubic bipartite graph on \( 30 \) vertices of girth \( 8. \) It can be further shown that there is a unique cubic bipartite graph on \( 30 \) vertices with girth \( 8. \) As a result both descriptions above construct the same graph.

Linear Congruential Generator

Here is a simple linear congruential generator (LCG) implementation in JavaScript:

function srand (seed) {
  let x = seed
  return function () {
    x = (1664525 * x + 1013904223) % 4294967296
    return x
  }
}

Here is an example usage:

> const rand = srand(0)
undefined
> rand()
1013904223
> rand()
1196435762
> rand()
3519870697

Numbering Lines

Both BSD and GNU cat can number output lines with the -n option. For example:

$ printf 'foo\nbar\nbaz\n' | cat -n
     1  foo
     2  bar
     3  baz

However I have always used nl for this. For example:

$ printf 'foo\nbar\nbaz\n' | nl
     1  foo
     2  bar
     3  baz

While nl is specified in POSIX, the cat -n option is not.

Read on website | #notes | #mathematics | #programming | #javascript | #shell

]]>