<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Patrick Van der Spiegel's Posts</title><link>http://patrick.vanderspie.gl/posts/</link><description>Recent content in Posts on Patrick Van der Spiegel</description><generator>Hugo</generator><language>en-GB</language><lastBuildDate>Fri, 06 Feb 2026 00:00:00 +0000</lastBuildDate><atom:link href="http://patrick.vanderspie.gl/posts/index.xml" rel="self" type="application/rss+xml"/><item><title>Your work is not just content</title><link>http://patrick.vanderspie.gl/posts/content/</link><pubDate>Fri, 06 Feb 2026 00:00:00 +0000</pubDate><guid>http://patrick.vanderspie.gl/posts/content/</guid><description>&lt;div class="quote-block">
 &lt;blockquote class="quote-content">Social media platforms provide value to independent creatives by allowing them to fully focus on creating instead of having to figure out how to share their work. Sadly, however, these same platforms reduce creative artists to content creators. When we label artwork — be it short- or long-form writing, illustration, photography, music, or video — as content, we turn unique works into interchangeable slop for consumption by users. I believe that the corporate web&amp;rsquo;s focus on user engagement plays a large role in this degradation, and that the independent web might offer a solution.&lt;/blockquote>
&lt;/div>
&lt;ul>
&lt;li>Keep reading at &lt;em>&lt;strong>&lt;a href="https://goodinternetmagazine.com/your-work-is-not-just-content/">Good Internet&lt;/a>&lt;/strong>&lt;/em>!&lt;/li>
&lt;/ul></description></item><item><title>Visualising Order in Sequences</title><link>http://patrick.vanderspie.gl/posts/nested-words/</link><pubDate>Tue, 13 Jan 2026 00:00:00 +0000</pubDate><guid>http://patrick.vanderspie.gl/posts/nested-words/</guid><description>&lt;h2 id="sequences">Sequences&lt;/h2>
&lt;h3 id="linear-order">Linear Order&lt;/h3>
&lt;p>In a sequence, each element directly follows at most one element and precedes at most one other, forming a linear chain. We can visualise this through nodes of &lt;a href="https://en.wikipedia.org/wiki/Directed_graph">directed graphs&lt;/a> that are connected using directed edges that represent the linear order between consecutive elements.&lt;/p>
&lt;p>The English word &amp;ldquo;&lt;code>sequence&lt;/code>&amp;rdquo; (which is an ordered sequence of the eight consecutive elements &lt;code>s&lt;/code>, &lt;code>e&lt;/code>, &lt;code>q&lt;/code>, &lt;code>u&lt;/code>, &lt;code>e&lt;/code>, &lt;code>n&lt;/code>, &lt;code>c&lt;/code>, &lt;code>e&lt;/code>), for example, can be visualised using a directed graph:&lt;/p>

&lt;img src="http://patrick.vanderspie.gl/posts/nested-words/nw-sequence.svg" class="invert-dark" loading="lazy" />


&lt;p>Full sentences can also be seen as sequences of ordered symbols. As an example, we have the sentence &amp;ldquo;&lt;code>This is a visualisation of linear order&lt;/code>&amp;rdquo;. Spaces are also considered elements of the sequence, but we slightly reduce the opacity of their nodes for readability:&lt;/p>

&lt;img src="http://patrick.vanderspie.gl/posts/nested-words/nw-sentence.svg" class="invert-dark" loading="lazy" />


&lt;p>We can drop the node labels to visualise the same sentence as:&lt;/p>

&lt;img src="http://patrick.vanderspie.gl/posts/nested-words/nw-large-sequence.svg" class="invert-dark" loading="lazy" />


&lt;p>Order in sequences matters, and these sequences are not restricted to written language. Other examples include sequences of actions (&lt;em>e.g., the sequence of actions that you perform to pour yourself a cup of coffee, as you can&amp;rsquo;t brew coffee before turning on the machine&lt;/em>) or music (&lt;em>e.g., the consecutive notes that make up a specific melody, as swapping the notes around gives us another melody&lt;/em>).&lt;/p>
&lt;hr>
&lt;h3 id="hierarchically-nested-matchings">Hierarchically Nested Matchings&lt;/h3>
&lt;p>Many structures also have a natural hierarchical organisation (&lt;em>e.g., we can group words into sentences, paragraphs, sections, and so on&lt;/em>). For this reason, we also want a way to describe and visualise hierarchical structure.&lt;/p>
&lt;p>This is the visualisation of the linear order in the sequence &amp;ldquo;&lt;code>nested word&lt;/code>&amp;rdquo;:&lt;/p>

&lt;img src="http://patrick.vanderspie.gl/posts/nested-words/nw-nested-word.svg" class="invert-dark" loading="lazy" />


&lt;p>We know that the sequence &amp;ldquo;&lt;code>nested word&lt;/code>&amp;rdquo; can naturally be split into two contiguous subsequences: &amp;ldquo;&lt;code>nested&lt;/code>&amp;rdquo; and &amp;ldquo;&lt;code>word&lt;/code>&amp;rdquo;. If we add symbols &amp;ldquo;&lt;code>⟨&lt;/code>&amp;rdquo; and &amp;ldquo;&lt;code>⟩&lt;/code>&amp;rdquo; that respectively denote the start and end of a word, we get the following:&lt;/p>

&lt;img src="http://patrick.vanderspie.gl/posts/nested-words/grouped-words.svg" class="invert-dark" loading="lazy" />


&lt;p>We name the &amp;ldquo;start of a word&amp;rdquo; a &lt;strong>call position&lt;/strong> and &amp;ldquo;the end of that same word&amp;rdquo; its matching &lt;strong>return position&lt;/strong>. All other elements are &lt;strong>internal positions&lt;/strong>. These matches give rise to different &lt;strong>nestings&lt;/strong>.&lt;/p>
&lt;p>We represent call and return positions using differently-coloured nodes, and represent matches using dashed directed edges from nodes representing call positions to nodes representing matching return positions:&lt;/p>

&lt;img src="http://patrick.vanderspie.gl/posts/nested-words/nw-grouped-words.svg" class="invert-dark" loading="lazy" />


&lt;h2 id="nested-words">Nested Words&lt;/h2>
&lt;p>More formally, a &lt;strong>nested word&lt;/strong>&lt;label for="sidenote-6" class="sidenote-number">&lt;/label>&lt;input type="checkbox" id="sidenote-6" class="sidenote-toggle">&lt;span class="sidenote">&lt;a href="https://www.cis.upenn.edu/~alur/nw.html">Nested Words - Rajeev Alur&lt;/a>&lt;/span> is a sequence of elements $w = s_{1} \ldots s_{\ell}$ together with a &lt;strong>matching relation&lt;/strong>, which is a set of matches $i \rightsquigarrow j$ that connect call positions $i$ to their return positions $j$.&lt;/p>
&lt;p>&lt;strong>For all matches $i \rightsquigarrow j$:&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Nestings only go forward:&lt;/strong> $i &amp;lt; j$&lt;/li>
&lt;/ul>

&lt;img src="http://patrick.vanderspie.gl/posts/nested-words/forward.svg" class="invert-dark" loading="lazy" />


&lt;ul>
&lt;li>&lt;strong>No two nestings share positions:&lt;/strong> $|\{ i \mid i \rightsquigarrow j \}| \leq 1$ and $|\{ j \mid i \rightsquigarrow j \}| \leq 1$&lt;/li>
&lt;/ul>

&lt;img src="http://patrick.vanderspie.gl/posts/nested-words/sharing.svg" class="invert-dark" loading="lazy" />


&lt;ul>
&lt;li>&lt;strong>No two nestings cross:&lt;/strong> if $i \rightsquigarrow j$ and $i&amp;rsquo; \rightsquigarrow j&amp;rsquo;$, we cannot have $i &amp;lt; i&amp;rsquo; &amp;lt; j &amp;lt; j'$&lt;/li>
&lt;/ul>

&lt;img src="http://patrick.vanderspie.gl/posts/nested-words/crossing.svg" class="invert-dark" loading="lazy" />


&lt;h3 id="visualising-nestings">Visualising Nestings&lt;/h3>
&lt;p>If we visualise &amp;ldquo;&lt;code>this is a visualisation of linear order&lt;/code>&amp;rdquo;, we get:&lt;/p>

&lt;img src="http://patrick.vanderspie.gl/posts/nested-words/nw-words.svg" class="invert-dark" loading="lazy" />


&lt;p>Trying out different &lt;a href="https://graphviz.org/docs/layouts/">graph layouts&lt;/a>, we get:&lt;/p>

&lt;img src="http://patrick.vanderspie.gl/posts/nested-words/nw-new-layout.svg" class="invert-dark" loading="lazy" />


&lt;p>Visualising the palindromic phrase&lt;label for="sidenote-12" class="sidenote-number">&lt;/label>&lt;input type="checkbox" id="sidenote-12" class="sidenote-toggle">&lt;span class="sidenote">&lt;a href="https://en.wikipedia.org/wiki/List_of_English_palindromic_phrases">List of English palindromic phrases - Wikipedia&lt;/a>&lt;/span> &amp;ldquo;&lt;code>⟨⟨⟨Lived⟩ ⟨on⟩ ⟨decaf⟩;⟩ ⟨⟨faced⟩ ⟨no⟩ ⟨devil⟩.⟩⟩&lt;/code>&amp;rdquo;, for example, gives us some nice symmetries (as we have two groups, each with three words, that each respectively have five, two, and five elements):&lt;/p>

&lt;img src="http://patrick.vanderspie.gl/posts/nested-words/nw-decaf.svg" class="invert-dark" loading="lazy" />


&lt;hr>
&lt;h2 id="nested-substitution-rules">Nested Substitution Rules&lt;/h2>
&lt;p>Reading through Stephen Wolfram&amp;rsquo;s &lt;a href="https://writings.stephenwolfram.com/2026/01/what-is-ruliology/">ruliology&lt;/a>, I wondered what kind of nested structures we could obtain using simple substitution rules (like repeatedly changing internal positions labelled &lt;code>a&lt;/code> with a nesting &lt;code>⟨aaa⟩&lt;/code>).&lt;/p>
&lt;h3 id="example-1">Example #1&lt;/h3>
&lt;h4 id="initial-condition">Initial Condition&lt;/h4>
&lt;pre tabindex="0">&lt;code>a
&lt;/code>&lt;/pre>&lt;h4 id="replacement-rule">Replacement Rule&lt;/h4>
&lt;pre tabindex="0">&lt;code>a → ⟨aaa⟩
&lt;/code>&lt;/pre>&lt;h4 id="looping-visualisation-6-steps">Looping Visualisation (6 steps)&lt;/h4>
&lt;div class="svg-sequence-container" id="svg-seq-posts-nested-words-animation-1">&lt;img src="http://patrick.vanderspie.gl/posts/nested-words/animation-1/animation-step-000.svg" alt="Animated nested word visualisation" class="svg-sequence-frame invert-dark active" loading="lazy" />&lt;img src="http://patrick.vanderspie.gl/posts/nested-words/animation-1/animation-step-001.svg" alt="Animated nested word visualisation" class="svg-sequence-frame invert-dark" loading="lazy" />&lt;img src="http://patrick.vanderspie.gl/posts/nested-words/animation-1/animation-step-002.svg" alt="Animated nested word visualisation" class="svg-sequence-frame invert-dark" loading="lazy" />&lt;img src="http://patrick.vanderspie.gl/posts/nested-words/animation-1/animation-step-003.svg" alt="Animated nested word visualisation" class="svg-sequence-frame invert-dark" loading="lazy" />&lt;img src="http://patrick.vanderspie.gl/posts/nested-words/animation-1/animation-step-004.svg" alt="Animated nested word visualisation" class="svg-sequence-frame invert-dark" loading="lazy" />&lt;img src="http://patrick.vanderspie.gl/posts/nested-words/animation-1/animation-step-005.svg" alt="Animated nested word visualisation" class="svg-sequence-frame invert-dark" loading="lazy" />&lt;img src="http://patrick.vanderspie.gl/posts/nested-words/animation-1/animation-step-000.svg" alt="" class="svg-sequence-afterimage invert-dark" loading="lazy" aria-hidden="true" />&lt;img src="http://patrick.vanderspie.gl/posts/nested-words/animation-1/animation-step-001.svg" alt="" class="svg-sequence-afterimage invert-dark" loading="lazy" aria-hidden="true" />&lt;img src="http://patrick.vanderspie.gl/posts/nested-words/animation-1/animation-step-002.svg" alt="" class="svg-sequence-afterimage invert-dark" loading="lazy" aria-hidden="true" />&lt;img src="http://patrick.vanderspie.gl/posts/nested-words/animation-1/animation-step-003.svg" alt="" class="svg-sequence-afterimage invert-dark" loading="lazy" aria-hidden="true" />&lt;img src="http://patrick.vanderspie.gl/posts/nested-words/animation-1/animation-step-004.svg" alt="" class="svg-sequence-afterimage invert-dark" loading="lazy" aria-hidden="true" />&lt;img src="http://patrick.vanderspie.gl/posts/nested-words/animation-1/animation-step-005.svg" alt="" class="svg-sequence-afterimage invert-dark" loading="lazy" aria-hidden="true" />&lt;/div>
&lt;style>
#svg-seq-posts-nested-words-animation-1 {
 position: relative;
 display: inline-block;
}
#svg-seq-posts-nested-words-animation-1 .svg-sequence-frame,
#svg-seq-posts-nested-words-animation-1 .svg-sequence-afterimage {
 position: absolute;
 top: 0;
 left: 0;
 opacity: 0;
}
#svg-seq-posts-nested-words-animation-1 .svg-sequence-frame {
 animation: 6s steps(1) infinite;
 z-index: 2;
}
#svg-seq-posts-nested-words-animation-1 .svg-sequence-afterimage {
 animation: 6s steps(1) infinite;
 z-index: 1;
}
#svg-seq-posts-nested-words-animation-1 .svg-sequence-frame:first-child {
 position: relative;
}#svg-seq-posts-nested-words-animation-1 .svg-sequence-frame:nth-child(1) {
 animation-name: svg-frame-svg-seq-posts-nested-words-animation-1-0;
}
@keyframes svg-frame-svg-seq-posts-nested-words-animation-1-0 {
 0%, 0% { opacity: 0; }
 0%, 8.333333333333334% { opacity: 1; }
 8.333333333333334%, 91.66666666666667% { opacity: 0; }
 91.66666666666667%, 100% { opacity: 1; }
 100%, 100% { opacity: 0; }
}#svg-seq-posts-nested-words-animation-1 .svg-sequence-frame:nth-child(2) {
 animation-name: svg-frame-svg-seq-posts-nested-words-animation-1-1;
}
@keyframes svg-frame-svg-seq-posts-nested-words-animation-1-1 {
 0%, 8.333333333333334% { opacity: 0; }
 8.333333333333334%, 16.666666666666668% { opacity: 1; }
 16.666666666666668%, 83.33333333333334% { opacity: 0; }
 83.33333333333334%, 91.66666666666667% { opacity: 1; }
 91.66666666666667%, 100% { opacity: 0; }
}#svg-seq-posts-nested-words-animation-1 .svg-sequence-frame:nth-child(3) {
 animation-name: svg-frame-svg-seq-posts-nested-words-animation-1-2;
}
@keyframes svg-frame-svg-seq-posts-nested-words-animation-1-2 {
 0%, 16.666666666666668% { opacity: 0; }
 16.666666666666668%, 25% { opacity: 1; }
 25%, 75% { opacity: 0; }
 75%, 83.33333333333334% { opacity: 1; }
 83.33333333333334%, 100% { opacity: 0; }
}#svg-seq-posts-nested-words-animation-1 .svg-sequence-frame:nth-child(4) {
 animation-name: svg-frame-svg-seq-posts-nested-words-animation-1-3;
}
@keyframes svg-frame-svg-seq-posts-nested-words-animation-1-3 {
 0%, 25% { opacity: 0; }
 25%, 33.333333333333336% { opacity: 1; }
 33.333333333333336%, 66.66666666666667% { opacity: 0; }
 66.66666666666667%, 75% { opacity: 1; }
 75%, 100% { opacity: 0; }
}#svg-seq-posts-nested-words-animation-1 .svg-sequence-frame:nth-child(5) {
 animation-name: svg-frame-svg-seq-posts-nested-words-animation-1-4;
}
@keyframes svg-frame-svg-seq-posts-nested-words-animation-1-4 {
 0%, 33.333333333333336% { opacity: 0; }
 33.333333333333336%, 41.66666666666667% { opacity: 1; }
 41.66666666666667%, 58.333333333333336% { opacity: 0; }
 58.333333333333336%, 66.66666666666667% { opacity: 1; }
 66.66666666666667%, 100% { opacity: 0; }
}#svg-seq-posts-nested-words-animation-1 .svg-sequence-frame:nth-child(6) {
 animation-name: svg-frame-svg-seq-posts-nested-words-animation-1-5;
}
@keyframes svg-frame-svg-seq-posts-nested-words-animation-1-5 {
 0%, 41.66666666666667% { opacity: 0; }
 41.66666666666667%, 50% { opacity: 1; }
 50%, 50% { opacity: 0; }
 50%, 58.333333333333336% { opacity: 1; }
 58.333333333333336%, 100% { opacity: 0; }
}#svg-seq-posts-nested-words-animation-1 .svg-sequence-afterimage:nth-child(7) {
 animation-name: svg-afterimage-svg-seq-posts-nested-words-animation-1-0;
}
@keyframes svg-afterimage-svg-seq-posts-nested-words-animation-1-0 {
 0%, 8.333333333333334% { opacity: 0; }
 8.333333333333334%, 16.666666666666668% { opacity: 0.1; }
 16.666666666666668%, 83.33333333333334% { opacity: 0; }
 83.33333333333334%, 91.66666666666667% { opacity: 0.1; }
 91.66666666666667%, 100% { opacity: 0; }
}#svg-seq-posts-nested-words-animation-1 .svg-sequence-afterimage:nth-child(8) {
 animation-name: svg-afterimage-svg-seq-posts-nested-words-animation-1-1;
}
@keyframes svg-afterimage-svg-seq-posts-nested-words-animation-1-1 {
 0%, 16.666666666666668% { opacity: 0; }
 16.666666666666668%, 25% { opacity: 0.1; }
 25%, 75% { opacity: 0; }
 75%, 83.33333333333334% { opacity: 0.1; }
 83.33333333333334%, 100% { opacity: 0; }
}#svg-seq-posts-nested-words-animation-1 .svg-sequence-afterimage:nth-child(9) {
 animation-name: svg-afterimage-svg-seq-posts-nested-words-animation-1-2;
}
@keyframes svg-afterimage-svg-seq-posts-nested-words-animation-1-2 {
 0%, 25% { opacity: 0; }
 25%, 33.333333333333336% { opacity: 0.1; }
 33.333333333333336%, 66.66666666666667% { opacity: 0; }
 66.66666666666667%, 75% { opacity: 0.1; }
 75%, 100% { opacity: 0; }
}#svg-seq-posts-nested-words-animation-1 .svg-sequence-afterimage:nth-child(10) {
 animation-name: svg-afterimage-svg-seq-posts-nested-words-animation-1-3;
}
@keyframes svg-afterimage-svg-seq-posts-nested-words-animation-1-3 {
 0%, 33.333333333333336% { opacity: 0; }
 33.333333333333336%, 41.66666666666667% { opacity: 0.1; }
 41.66666666666667%, 58.333333333333336% { opacity: 0; }
 58.333333333333336%, 66.66666666666667% { opacity: 0.1; }
 66.66666666666667%, 100% { opacity: 0; }
}#svg-seq-posts-nested-words-animation-1 .svg-sequence-afterimage:nth-child(11) {
 animation-name: svg-afterimage-svg-seq-posts-nested-words-animation-1-4;
}
@keyframes svg-afterimage-svg-seq-posts-nested-words-animation-1-4 {
 0%, 41.66666666666667% { opacity: 0; }
 41.66666666666667%, 50% { opacity: 0.1; }
 50%, 50% { opacity: 0; }
 50%, 58.333333333333336% { opacity: 0.1; }
 58.333333333333336%, 100% { opacity: 0; }
}#svg-seq-posts-nested-words-animation-1 .svg-sequence-afterimage:nth-child(12) {
 animation-name: svg-afterimage-svg-seq-posts-nested-words-animation-1-5;
}
@keyframes svg-afterimage-svg-seq-posts-nested-words-animation-1-5 {
 0%, 50% { opacity: 0; }
 50%, 58.333333333333336% { opacity: 0.1; }
 58.333333333333336%, 41.66666666666667% { opacity: 0; }
 41.66666666666667%, 50% { opacity: 0.1; }
 50%, 100% { opacity: 0; }
}&lt;/style>
&lt;hr>
&lt;h3 id="example-2">Example #2&lt;/h3>
&lt;h4 id="initial-condition-1">Initial Condition&lt;/h4>
&lt;pre tabindex="0">&lt;code>ab
&lt;/code>&lt;/pre>&lt;h4 id="replacement-rule-1">Replacement Rule&lt;/h4>
&lt;pre tabindex="0">&lt;code>a → bb
b → ⟨aa⟩
&lt;/code>&lt;/pre>&lt;h4 id="looping-visualisation-9-steps">Looping Visualisation (9 steps)&lt;/h4>
&lt;div class="svg-sequence-container" id="svg-seq-posts-nested-words-animation-2">&lt;img src="http://patrick.vanderspie.gl/posts/nested-words/animation-2/animation-step-000.svg" alt="Animated nested word visualisation" class="svg-sequence-frame invert-dark active" loading="lazy" />&lt;img src="http://patrick.vanderspie.gl/posts/nested-words/animation-2/animation-step-001.svg" alt="Animated nested word visualisation" class="svg-sequence-frame invert-dark" loading="lazy" />&lt;img src="http://patrick.vanderspie.gl/posts/nested-words/animation-2/animation-step-002.svg" alt="Animated nested word visualisation" class="svg-sequence-frame invert-dark" loading="lazy" />&lt;img src="http://patrick.vanderspie.gl/posts/nested-words/animation-2/animation-step-003.svg" alt="Animated nested word visualisation" class="svg-sequence-frame invert-dark" loading="lazy" />&lt;img src="http://patrick.vanderspie.gl/posts/nested-words/animation-2/animation-step-004.svg" alt="Animated nested word visualisation" class="svg-sequence-frame invert-dark" loading="lazy" />&lt;img src="http://patrick.vanderspie.gl/posts/nested-words/animation-2/animation-step-005.svg" alt="Animated nested word visualisation" class="svg-sequence-frame invert-dark" loading="lazy" />&lt;img src="http://patrick.vanderspie.gl/posts/nested-words/animation-2/animation-step-006.svg" alt="Animated nested word visualisation" class="svg-sequence-frame invert-dark" loading="lazy" />&lt;img src="http://patrick.vanderspie.gl/posts/nested-words/animation-2/animation-step-007.svg" alt="Animated nested word visualisation" class="svg-sequence-frame invert-dark" loading="lazy" />&lt;img src="http://patrick.vanderspie.gl/posts/nested-words/animation-2/animation-step-008.svg" alt="Animated nested word visualisation" class="svg-sequence-frame invert-dark" loading="lazy" />&lt;img src="http://patrick.vanderspie.gl/posts/nested-words/animation-2/animation-step-000.svg" alt="" class="svg-sequence-afterimage invert-dark" loading="lazy" aria-hidden="true" />&lt;img src="http://patrick.vanderspie.gl/posts/nested-words/animation-2/animation-step-001.svg" alt="" class="svg-sequence-afterimage invert-dark" loading="lazy" aria-hidden="true" />&lt;img src="http://patrick.vanderspie.gl/posts/nested-words/animation-2/animation-step-002.svg" alt="" class="svg-sequence-afterimage invert-dark" loading="lazy" aria-hidden="true" />&lt;img src="http://patrick.vanderspie.gl/posts/nested-words/animation-2/animation-step-003.svg" alt="" class="svg-sequence-afterimage invert-dark" loading="lazy" aria-hidden="true" />&lt;img src="http://patrick.vanderspie.gl/posts/nested-words/animation-2/animation-step-004.svg" alt="" class="svg-sequence-afterimage invert-dark" loading="lazy" aria-hidden="true" />&lt;img src="http://patrick.vanderspie.gl/posts/nested-words/animation-2/animation-step-005.svg" alt="" class="svg-sequence-afterimage invert-dark" loading="lazy" aria-hidden="true" />&lt;img src="http://patrick.vanderspie.gl/posts/nested-words/animation-2/animation-step-006.svg" alt="" class="svg-sequence-afterimage invert-dark" loading="lazy" aria-hidden="true" />&lt;img src="http://patrick.vanderspie.gl/posts/nested-words/animation-2/animation-step-007.svg" alt="" class="svg-sequence-afterimage invert-dark" loading="lazy" aria-hidden="true" />&lt;img src="http://patrick.vanderspie.gl/posts/nested-words/animation-2/animation-step-008.svg" alt="" class="svg-sequence-afterimage invert-dark" loading="lazy" aria-hidden="true" />&lt;/div>
&lt;style>
#svg-seq-posts-nested-words-animation-2 {
 position: relative;
 display: inline-block;
}
#svg-seq-posts-nested-words-animation-2 .svg-sequence-frame,
#svg-seq-posts-nested-words-animation-2 .svg-sequence-afterimage {
 position: absolute;
 top: 0;
 left: 0;
 opacity: 0;
}
#svg-seq-posts-nested-words-animation-2 .svg-sequence-frame {
 animation: 5s steps(1) infinite;
 z-index: 2;
}
#svg-seq-posts-nested-words-animation-2 .svg-sequence-afterimage {
 animation: 5s steps(1) infinite;
 z-index: 1;
}
#svg-seq-posts-nested-words-animation-2 .svg-sequence-frame:first-child {
 position: relative;
}#svg-seq-posts-nested-words-animation-2 .svg-sequence-frame:nth-child(1) {
 animation-name: svg-frame-svg-seq-posts-nested-words-animation-2-0;
}
@keyframes svg-frame-svg-seq-posts-nested-words-animation-2-0 {
 0%, 0% { opacity: 0; }
 0%, 5.555555555555555% { opacity: 1; }
 5.555555555555555%, 94.44444444444444% { opacity: 0; }
 94.44444444444444%, 100% { opacity: 1; }
 100%, 100% { opacity: 0; }
}#svg-seq-posts-nested-words-animation-2 .svg-sequence-frame:nth-child(2) {
 animation-name: svg-frame-svg-seq-posts-nested-words-animation-2-1;
}
@keyframes svg-frame-svg-seq-posts-nested-words-animation-2-1 {
 0%, 5.555555555555555% { opacity: 0; }
 5.555555555555555%, 11.11111111111111% { opacity: 1; }
 11.11111111111111%, 88.88888888888889% { opacity: 0; }
 88.88888888888889%, 94.44444444444444% { opacity: 1; }
 94.44444444444444%, 100% { opacity: 0; }
}#svg-seq-posts-nested-words-animation-2 .svg-sequence-frame:nth-child(3) {
 animation-name: svg-frame-svg-seq-posts-nested-words-animation-2-2;
}
@keyframes svg-frame-svg-seq-posts-nested-words-animation-2-2 {
 0%, 11.11111111111111% { opacity: 0; }
 11.11111111111111%, 16.666666666666664% { opacity: 1; }
 16.666666666666664%, 83.33333333333333% { opacity: 0; }
 83.33333333333333%, 88.88888888888889% { opacity: 1; }
 88.88888888888889%, 100% { opacity: 0; }
}#svg-seq-posts-nested-words-animation-2 .svg-sequence-frame:nth-child(4) {
 animation-name: svg-frame-svg-seq-posts-nested-words-animation-2-3;
}
@keyframes svg-frame-svg-seq-posts-nested-words-animation-2-3 {
 0%, 16.666666666666664% { opacity: 0; }
 16.666666666666664%, 22.22222222222222% { opacity: 1; }
 22.22222222222222%, 77.77777777777777% { opacity: 0; }
 77.77777777777777%, 83.33333333333333% { opacity: 1; }
 83.33333333333333%, 100% { opacity: 0; }
}#svg-seq-posts-nested-words-animation-2 .svg-sequence-frame:nth-child(5) {
 animation-name: svg-frame-svg-seq-posts-nested-words-animation-2-4;
}
@keyframes svg-frame-svg-seq-posts-nested-words-animation-2-4 {
 0%, 22.22222222222222% { opacity: 0; }
 22.22222222222222%, 27.77777777777778% { opacity: 1; }
 27.77777777777778%, 72.22222222222221% { opacity: 0; }
 72.22222222222221%, 77.77777777777777% { opacity: 1; }
 77.77777777777777%, 100% { opacity: 0; }
}#svg-seq-posts-nested-words-animation-2 .svg-sequence-frame:nth-child(6) {
 animation-name: svg-frame-svg-seq-posts-nested-words-animation-2-5;
}
@keyframes svg-frame-svg-seq-posts-nested-words-animation-2-5 {
 0%, 27.77777777777778% { opacity: 0; }
 27.77777777777778%, 33.33333333333333% { opacity: 1; }
 33.33333333333333%, 66.66666666666666% { opacity: 0; }
 66.66666666666666%, 72.22222222222221% { opacity: 1; }
 72.22222222222221%, 100% { opacity: 0; }
}#svg-seq-posts-nested-words-animation-2 .svg-sequence-frame:nth-child(7) {
 animation-name: svg-frame-svg-seq-posts-nested-words-animation-2-6;
}
@keyframes svg-frame-svg-seq-posts-nested-words-animation-2-6 {
 0%, 33.33333333333333% { opacity: 0; }
 33.33333333333333%, 38.888888888888886% { opacity: 1; }
 38.888888888888886%, 61.11111111111111% { opacity: 0; }
 61.11111111111111%, 66.66666666666666% { opacity: 1; }
 66.66666666666666%, 100% { opacity: 0; }
}#svg-seq-posts-nested-words-animation-2 .svg-sequence-frame:nth-child(8) {
 animation-name: svg-frame-svg-seq-posts-nested-words-animation-2-7;
}
@keyframes svg-frame-svg-seq-posts-nested-words-animation-2-7 {
 0%, 38.888888888888886% { opacity: 0; }
 38.888888888888886%, 44.44444444444444% { opacity: 1; }
 44.44444444444444%, 55.55555555555556% { opacity: 0; }
 55.55555555555556%, 61.11111111111111% { opacity: 1; }
 61.11111111111111%, 100% { opacity: 0; }
}#svg-seq-posts-nested-words-animation-2 .svg-sequence-frame:nth-child(9) {
 animation-name: svg-frame-svg-seq-posts-nested-words-animation-2-8;
}
@keyframes svg-frame-svg-seq-posts-nested-words-animation-2-8 {
 0%, 44.44444444444444% { opacity: 0; }
 44.44444444444444%, 50% { opacity: 1; }
 50%, 50% { opacity: 0; }
 50%, 55.55555555555556% { opacity: 1; }
 55.55555555555556%, 100% { opacity: 0; }
}#svg-seq-posts-nested-words-animation-2 .svg-sequence-afterimage:nth-child(10) {
 animation-name: svg-afterimage-svg-seq-posts-nested-words-animation-2-0;
}
@keyframes svg-afterimage-svg-seq-posts-nested-words-animation-2-0 {
 0%, 5.555555555555555% { opacity: 0; }
 5.555555555555555%, 11.11111111111111% { opacity: 0.1; }
 11.11111111111111%, 88.88888888888889% { opacity: 0; }
 88.88888888888889%, 94.44444444444444% { opacity: 0.1; }
 94.44444444444444%, 100% { opacity: 0; }
}#svg-seq-posts-nested-words-animation-2 .svg-sequence-afterimage:nth-child(11) {
 animation-name: svg-afterimage-svg-seq-posts-nested-words-animation-2-1;
}
@keyframes svg-afterimage-svg-seq-posts-nested-words-animation-2-1 {
 0%, 11.11111111111111% { opacity: 0; }
 11.11111111111111%, 16.666666666666664% { opacity: 0.1; }
 16.666666666666664%, 83.33333333333333% { opacity: 0; }
 83.33333333333333%, 88.88888888888889% { opacity: 0.1; }
 88.88888888888889%, 100% { opacity: 0; }
}#svg-seq-posts-nested-words-animation-2 .svg-sequence-afterimage:nth-child(12) {
 animation-name: svg-afterimage-svg-seq-posts-nested-words-animation-2-2;
}
@keyframes svg-afterimage-svg-seq-posts-nested-words-animation-2-2 {
 0%, 16.666666666666664% { opacity: 0; }
 16.666666666666664%, 22.22222222222222% { opacity: 0.1; }
 22.22222222222222%, 77.77777777777777% { opacity: 0; }
 77.77777777777777%, 83.33333333333333% { opacity: 0.1; }
 83.33333333333333%, 100% { opacity: 0; }
}#svg-seq-posts-nested-words-animation-2 .svg-sequence-afterimage:nth-child(13) {
 animation-name: svg-afterimage-svg-seq-posts-nested-words-animation-2-3;
}
@keyframes svg-afterimage-svg-seq-posts-nested-words-animation-2-3 {
 0%, 22.22222222222222% { opacity: 0; }
 22.22222222222222%, 27.77777777777778% { opacity: 0.1; }
 27.77777777777778%, 72.22222222222221% { opacity: 0; }
 72.22222222222221%, 77.77777777777777% { opacity: 0.1; }
 77.77777777777777%, 100% { opacity: 0; }
}#svg-seq-posts-nested-words-animation-2 .svg-sequence-afterimage:nth-child(14) {
 animation-name: svg-afterimage-svg-seq-posts-nested-words-animation-2-4;
}
@keyframes svg-afterimage-svg-seq-posts-nested-words-animation-2-4 {
 0%, 27.77777777777778% { opacity: 0; }
 27.77777777777778%, 33.33333333333333% { opacity: 0.1; }
 33.33333333333333%, 66.66666666666666% { opacity: 0; }
 66.66666666666666%, 72.22222222222221% { opacity: 0.1; }
 72.22222222222221%, 100% { opacity: 0; }
}#svg-seq-posts-nested-words-animation-2 .svg-sequence-afterimage:nth-child(15) {
 animation-name: svg-afterimage-svg-seq-posts-nested-words-animation-2-5;
}
@keyframes svg-afterimage-svg-seq-posts-nested-words-animation-2-5 {
 0%, 33.33333333333333% { opacity: 0; }
 33.33333333333333%, 38.888888888888886% { opacity: 0.1; }
 38.888888888888886%, 61.11111111111111% { opacity: 0; }
 61.11111111111111%, 66.66666666666666% { opacity: 0.1; }
 66.66666666666666%, 100% { opacity: 0; }
}#svg-seq-posts-nested-words-animation-2 .svg-sequence-afterimage:nth-child(16) {
 animation-name: svg-afterimage-svg-seq-posts-nested-words-animation-2-6;
}
@keyframes svg-afterimage-svg-seq-posts-nested-words-animation-2-6 {
 0%, 38.888888888888886% { opacity: 0; }
 38.888888888888886%, 44.44444444444444% { opacity: 0.1; }
 44.44444444444444%, 55.55555555555556% { opacity: 0; }
 55.55555555555556%, 61.11111111111111% { opacity: 0.1; }
 61.11111111111111%, 100% { opacity: 0; }
}#svg-seq-posts-nested-words-animation-2 .svg-sequence-afterimage:nth-child(17) {
 animation-name: svg-afterimage-svg-seq-posts-nested-words-animation-2-7;
}
@keyframes svg-afterimage-svg-seq-posts-nested-words-animation-2-7 {
 0%, 44.44444444444444% { opacity: 0; }
 44.44444444444444%, 50% { opacity: 0.1; }
 50%, 50% { opacity: 0; }
 50%, 55.55555555555556% { opacity: 0.1; }
 55.55555555555556%, 100% { opacity: 0; }
}#svg-seq-posts-nested-words-animation-2 .svg-sequence-afterimage:nth-child(18) {
 animation-name: svg-afterimage-svg-seq-posts-nested-words-animation-2-8;
}
@keyframes svg-afterimage-svg-seq-posts-nested-words-animation-2-8 {
 0%, 50% { opacity: 0; }
 50%, 55.55555555555556% { opacity: 0.1; }
 55.55555555555556%, 44.44444444444444% { opacity: 0; }
 44.44444444444444%, 50% { opacity: 0.1; }
 50%, 100% { opacity: 0; }
}&lt;/style>
&lt;hr>
&lt;h2 id="visualising-this-post">Visualising this post&lt;/h2>
&lt;p>Finally, we can apply nested word visualisation to this post itself by treating the post&amp;rsquo;s structure as a nested word:&lt;/p>

&lt;img src="http://patrick.vanderspie.gl/posts/nested-words/nw-post.svg" class="invert-dark" loading="lazy" />

</description></item><item><title>Scrambled Stations</title><link>http://patrick.vanderspie.gl/posts/scrambled-stations/</link><pubDate>Fri, 08 Aug 2025 00:00:00 +0000</pubDate><guid>http://patrick.vanderspie.gl/posts/scrambled-stations/</guid><description>&lt;div class="alfphabet-display">
Scrambled Stations
&lt;/div>
&lt;div class="custom-title-date">
 
 
 
 &lt;time datetime="2025-08-08T00:00:00&amp;#43;00:00">August 8, 2025&lt;/time>
 
&lt;/div>
&lt;h2 id="gehusselde-stationsnaam-ns">Gehusselde stationsnaam (NS)&lt;/h2>
&lt;p>The Dutch NS railway operator has the &lt;a href="https://www.ns.nl/dagje-uit/ontspanning/puzzel.html">&lt;em>&amp;ldquo;gehusselde stationsnaam&amp;rdquo;&lt;/em>&lt;/a> game on their on-board train information displays: a puzzle where you have to figure out the hidden train station through an anagram of that station&amp;rsquo;s name. These puzzles are also on their &lt;a href="https://www.instagram.com/ns_online/p/CwejiNRqAJ7/">Instagram page&lt;/a>: the puzzle &lt;em>&amp;ldquo;Los Pichor Pathir&amp;rdquo;&lt;/em>, for example, matches the Schiphol Airport railway station when unscrambled. As I barely know any Dutch railway station names, I was curious if something similar was feasibile for Belgian railway stations using Flemish dialect words.&lt;/p>
&lt;hr>
&lt;h2 id="retrieving-wordlists">Retrieving wordlists&lt;/h2>
&lt;p>We first retrieve two wordlists: a list of Belgian railway stations, and a wordlist of Flemish dialect words. Afterwards, we use these wordlists to figure out if there are any single- or multi-word anagrams of one wordlist inside the other.&lt;/p>
&lt;h3 id="belgian-railway-stations">Belgian railway stations&lt;/h3>
&lt;p>There&amp;rsquo;s a &lt;a href="https://github.com/iRail/stations/">complete list of Belgian railway stations&lt;/a> available on GitHub, maintained by &lt;a href="https://github.com/irail">iRail&lt;/a>. We can &lt;code>curl&lt;/code> the &lt;code>stations.csv&lt;/code> file from that repository.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-text" data-lang="text">&lt;span style="display:flex;">&lt;span>curl --output railway_stations.csv \ 
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>https://raw.githubusercontent.com/iRail/stations/refs/heads/master/stations.csv
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>This list of railway stations also contains lots of non-Belgian (often Dutch, German, and French) stations, which I assume are included as they are reachable by train from Belgium. We can filter out these non-Belgian stations using the &lt;code>country-code&lt;/code> column.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>$ xan filter &lt;span style="color:#666;font-style:italic">&amp;#39;col(&amp;#34;country-code&amp;#34;) ne &amp;#34;be&amp;#34;&amp;#39;&lt;/span> railway_stations.csv | &lt;span style="color:#666;font-style:italic">\
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#666;font-style:italic">&lt;/span> xan &lt;span style="font-weight:bold;text-decoration:underline">select&lt;/span> &lt;span style="color:#666;font-style:italic">&amp;#39;name,country-code&amp;#39;&lt;/span> | &lt;span style="color:#666;font-style:italic">\
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#666;font-style:italic">&lt;/span> xan view -l 5
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>┌───┬─────────────────────┬──────────────┐
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>│ - │ name │ country-code │
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>├───┼─────────────────────┼──────────────┤
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>│ 0 │ &lt;span style="">&amp;#39;&lt;/span>s Hertogenbosch │ nl │
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>│ 1 │ Aachen Hbf │ de │
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>│ 2 │ Agde │ fr │
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>│ 3 │ Aime-la-Plagne │ fr │
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>│ 4 │ Aix-en-Provence TGV │ fr │
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>│ … │ … │ … │
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>└───┴─────────────────────┴──────────────┘
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Belgian railway stations sometimes have both a Dutch and French name. These names are mostly similar (like &lt;em>&amp;ldquo;Brussel-Centraal&amp;rdquo;&lt;/em> in Dutch and &lt;em>&amp;ldquo;Bruxelles-Central&amp;rdquo;&lt;/em> in French), but sometimes completely different (like &lt;a href="https://nl.wikipedia.org/wiki/Diesdelle#Geschiedenis">&lt;em>&amp;ldquo;Diesdelle&amp;rdquo;&lt;/em> and &lt;em>&amp;ldquo;Vivier d&amp;rsquo;Oie&amp;rdquo;&lt;/em>&lt;/a>). For railway stations in Brussels, these variants are separated by a slash in the &lt;code>name&lt;/code> column of our &lt;code>CSV&lt;/code>.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>$ xan filter &lt;span style="color:#666;font-style:italic">&amp;#34;&amp;#39;/&amp;#39; in col(&amp;#39;name&amp;#39;)&amp;#34;&lt;/span> railway_stations.csv | &lt;span style="color:#666;font-style:italic">\ &lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> xan &lt;span style="font-weight:bold;text-decoration:underline">select&lt;/span> &lt;span style="color:#666;font-style:italic">&amp;#34;name&amp;#34;&lt;/span> | &lt;span style="color:#666;font-style:italic">\
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#666;font-style:italic">&lt;/span> xan view -l 5
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>┌───┬────────────────────────────────────┐
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>│ - │ name │
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>├───┼────────────────────────────────────┤
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>│ 0 │ Arcaden/Arcades │
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>│ 1 │ Boondaal/Boondael │
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>│ 2 │ Bosvoorde/Boitsfort │
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>│ 3 │ Brussel-Centraal/Bruxelles-Central │
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>│ 4 │ Brussel-Congres/Bruxelles-Congrès │
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>│ … │ … │
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>└───┴────────────────────────────────────┘
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>We filter out non-Belgian railway stations, select the names of the remaining stations, split these names on forward slashes so that we get both the Dutch and French name on separate lines where appropriate, and write these lines to a separate text file.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-text" data-lang="text">&lt;span style="display:flex;">&lt;span>xan filter &amp;#39;col(&amp;#34;country-code&amp;#34;) eq &amp;#34;be&amp;#34;&amp;#39; railway_stations.csv | \ 
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>xan select name | \ 
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>tr &amp;#39;/&amp;#39; &amp;#39;\n&amp;#39; &amp;gt; railway_stations.txt
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="flemish-dialect-words">Flemish dialect words&lt;/h3>
&lt;p>As far as I can tell, the &lt;a href="https://www.vlaamswoordenboek.be/">&amp;ldquo;Flemish dictionary&amp;rdquo;&lt;/a> does not provide a downloadable wordlist. They do, however, have a page per letter of the alphabet containing all words starting with that letter. Luckily, these letter pages follow the same structure and contain all words on a single page, so by looping through only 26 pages (one for each letter of the alphabet), we can extract all listed words using &lt;code>grep&lt;/code> and &lt;code>sed&lt;/code>.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>&lt;span style="font-weight:bold;text-decoration:underline">for&lt;/span> letter in {a..z}; &lt;span style="font-weight:bold;text-decoration:underline">do&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> curl -s https://www.vlaamswoordenboek.be/definities/begintmet/&lt;span style="color:#666;font-weight:bold;font-style:italic">$letter&lt;/span> | &lt;span style="color:#666;font-style:italic">\
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#666;font-style:italic">&lt;/span> grep -o &lt;span style="color:#666;font-style:italic">&amp;#39;&amp;lt;a href=&amp;#34;/definities/term/[^&amp;#34;]*&amp;#34;&amp;gt;[^&amp;lt;]*&amp;lt;/a&amp;gt; &amp;lt;br /&amp;gt;&amp;#39;&lt;/span> | &lt;span style="color:#666;font-style:italic">\
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#666;font-style:italic">&lt;/span> sed &lt;span style="color:#666;font-style:italic">&amp;#39;s/&amp;lt;a href=&amp;#34;[^&amp;#34;]*&amp;#34;&amp;gt;\([^&amp;lt;]*\)&amp;lt;\/a&amp;gt; &amp;lt;br \/&amp;gt;/\1/&amp;#39;&lt;/span> &amp;gt;&amp;gt; flemish_wordlist.txt
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="font-weight:bold;text-decoration:underline">done&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;hr>
&lt;h2 id="finding-anagrams">Finding anagrams&lt;/h2>
&lt;p>We first look at how to find &lt;strong>single-word anagrams&lt;/strong>&amp;mdash;where single Flemish dialect words are anagrams of certain railway stations&amp;mdash;using &lt;em>&amp;ldquo;alphabetic maps&amp;rdquo;&lt;/em>. We pair sets of railway stations with their anagram set, for example:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-text" data-lang="text">&lt;span style="display:flex;">&lt;span>{&amp;#39;Ternat&amp;#39;} = {&amp;#39;ratten&amp;#39;}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{&amp;#39;Drongen&amp;#39;} = {&amp;#39;gronden&amp;#39;}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{&amp;#39;Aalst&amp;#39;} = {&amp;#39;slaat&amp;#39;, &amp;#39;staal&amp;#39;}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;div class="alfphabet-display alfphabet-scale-medium">
Ratten 🐀
&lt;/div>
&lt;p>Afterwards, we look at possible ways of finding &lt;strong>multi-word anagrams&lt;/strong>&amp;mdash;where multiple words combine to make up an anagram. Some examples, of which &lt;em>&amp;quot;&lt;a href="https://www.vlaamswoordenboek.be/definities/term/houten">houten&lt;/a> &lt;a href="https://www.vlaamswoordenboek.be/definities/term/afritser">afritser&lt;/a>&amp;quot;&lt;/em> (&amp;ldquo;wooden playground slide&amp;rdquo;) is my favourite:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-text" data-lang="text">&lt;span style="display:flex;">&lt;span>{&amp;#39;Athus-Frontiere&amp;#39;} = {&amp;#39;houten&amp;#39;} + {&amp;#39;afritser&amp;#39;}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{&amp;#39;Mechelen-Nekkerspoel&amp;#39;} = {&amp;#39;schoempelen&amp;#39;} + {&amp;#39;krekelen&amp;#39;}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{&amp;#39;Dave-Saint-Martin&amp;#39;} = {&amp;#39;mandataris&amp;#39;} + {&amp;#39;in vet&amp;#39;}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{&amp;#39;Mortsel-Deurnesteenweg&amp;#39;} = {&amp;#39;lutter&amp;#39;} + {&amp;#39;messe&amp;#39;} + {&amp;#39;onderwegen&amp;#39;}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;div class="alfphabet-display alfphabet-scale-medium">
Houten Afritser 🛝
&lt;/div>
&lt;h3 id="single-word-anagrams">Single-word anagrams&lt;/h3>
&lt;p>When alphabetically ordering all characters within words, two distinct words result in the same ordering only if they are anagrams of each other. In Python, for example:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>&amp;gt;&amp;gt;&amp;gt; &lt;span style="font-weight:bold;font-style:italic">sorted&lt;/span>(&lt;span style="color:#666;font-style:italic">&amp;#34;undefinability&amp;#34;&lt;/span>) == &lt;span style="font-weight:bold;font-style:italic">sorted&lt;/span>(&lt;span style="color:#666;font-style:italic">&amp;#34;unidentifiably&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="font-weight:bold;text-decoration:underline">True&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Thus, to find anagrams of Belgian railway stations in Flemish dialect words, we build two &lt;em>alphabetic maps&lt;/em>: dictionaries that map ordered characters to a set of &amp;ldquo;intra-wordlist anagrams&amp;rdquo;.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>&lt;span style="font-weight:bold;text-decoration:underline">from&lt;/span> &lt;span style="color:#666;font-weight:bold;font-style:italic">collections&lt;/span> &lt;span style="font-weight:bold;text-decoration:underline">import&lt;/span> defaultdict
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="font-weight:bold;text-decoration:underline">def&lt;/span> &lt;span style="color:#666;font-weight:bold;font-style:italic">get_key&lt;/span>(word):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>	&lt;span style="font-weight:bold;text-decoration:underline">return&lt;/span> &lt;span style="color:#666;font-style:italic">&amp;#34;&amp;#34;&lt;/span>.join(&lt;span style="font-weight:bold;font-style:italic">sorted&lt;/span>(&lt;span style="font-weight:bold;font-style:italic">filter&lt;/span>(&lt;span style="font-weight:bold;font-style:italic">str&lt;/span>.isalpha, word.lower())))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="font-weight:bold;text-decoration:underline">def&lt;/span> &lt;span style="color:#666;font-weight:bold;font-style:italic">group&lt;/span>(words):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>	groups = defaultdict(&lt;span style="font-weight:bold;font-style:italic">set&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>	&lt;span style="font-weight:bold;text-decoration:underline">for&lt;/span> word &lt;span style="font-weight:bold">in&lt;/span> words:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>		groups[get_key(word)].add(word)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>	&lt;span style="font-weight:bold;text-decoration:underline">return&lt;/span> groups
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="font-weight:bold;text-decoration:underline">with&lt;/span> &lt;span style="font-weight:bold;font-style:italic">open&lt;/span>(&lt;span style="color:#666;font-style:italic">&amp;#39;railway_stations.txt&amp;#39;&lt;/span>, &lt;span style="color:#666;font-style:italic">&amp;#39;r&amp;#39;&lt;/span>) &lt;span style="font-weight:bold;text-decoration:underline">as&lt;/span> f:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> stations = &lt;span style="font-weight:bold;font-style:italic">set&lt;/span>(line.strip() &lt;span style="font-weight:bold;text-decoration:underline">for&lt;/span> line &lt;span style="font-weight:bold">in&lt;/span> f)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> grouped_stations = group(stations)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> 
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="font-weight:bold;text-decoration:underline">with&lt;/span> &lt;span style="font-weight:bold;font-style:italic">open&lt;/span>(&lt;span style="color:#666;font-style:italic">&amp;#39;flemish_wordlist.txt&amp;#39;&lt;/span>, &lt;span style="color:#666;font-style:italic">&amp;#39;r&amp;#39;&lt;/span>) &lt;span style="font-weight:bold;text-decoration:underline">as&lt;/span> f:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> wordlist = &lt;span style="font-weight:bold;font-style:italic">set&lt;/span>(line.strip() &lt;span style="font-weight:bold;text-decoration:underline">for&lt;/span> line &lt;span style="font-weight:bold">in&lt;/span> f)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> grouped_wordlist = group(wordlist)
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>To obtain the keys of these maps, we turn all characters of the words lowercase and filter out non-alphabetic characters before sorting. We loop through the set of words from each wordlist and build two separate alphabetic maps. Using these alphabetic maps, we can look for anagrams within the wordlists themselves. It turns out, for example, that only two pairs of stations are anagrams of each other:&lt;/p>
&lt;div class="alfphabet-display alfphabet-scale-small">
Mollem = Lommel
&lt;/div>
&lt;div class="alfphabet-display alfphabet-scale-small">
Diegem = Idegem
&lt;/div>
&lt;p>To find the &amp;ldquo;inter-wordlist anagrams&amp;rdquo;&amp;mdash;which is what we were originally interested in&amp;mdash;we look at the intersection of the keys between alphabetic maps. Every common key corresponds to pairs of anagrams:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>&lt;span style="font-weight:bold;text-decoration:underline">for&lt;/span> key &lt;span style="font-weight:bold">in&lt;/span> (grouped_stations.keys() &amp;amp; grouped_wordlist.keys()):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>	grouped_stations[key], grouped_wordlist[key]
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>This results in the following set of single-word anagrams. (As there&amp;rsquo;s a small overlap between our wordlists, we also get a couple of trivial anagrams in return.)&lt;/p>

&lt;div class="details-wrapper">
 &lt;details id="details-6">
 &lt;summary class="details-summary">Details&lt;/summary>
 &lt;div class="details-content">
 &lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-text" data-lang="text">&lt;span style="display:flex;">&lt;span>({&amp;#39;Jette&amp;#39;}, {&amp;#39;tetje&amp;#39;})
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>({&amp;#39;Temse&amp;#39;}, {&amp;#39;smete&amp;#39;})
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>({&amp;#39;Lierde&amp;#39;}, {&amp;#39;leider&amp;#39;})
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>({&amp;#39;Leman&amp;#39;}, {&amp;#39;lamen&amp;#39;})
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>({&amp;#39;Ternat&amp;#39;}, {&amp;#39;ratten&amp;#39;})
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>({&amp;#39;Kijkuit&amp;#39;}, {&amp;#39;kijkuit&amp;#39;})
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>({&amp;#39;Boom&amp;#39;}, {&amp;#39;boom&amp;#39;})
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>({&amp;#39;Godinne&amp;#39;}, {&amp;#39;doening&amp;#39;})
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>({&amp;#39;Bleret&amp;#39;}, {&amp;#39;bretel&amp;#39;})
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>({&amp;#39;Muizen&amp;#39;}, {&amp;#39;muizen&amp;#39;})
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>({&amp;#39;Asse&amp;#39;}, {&amp;#39;asse&amp;#39;})
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>({&amp;#39;Landen&amp;#39;}, {&amp;#39;landen&amp;#39;})
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>({&amp;#39;Weerde&amp;#39;}, {&amp;#39;weerde&amp;#39;})
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>({&amp;#39;Mol&amp;#39;}, {&amp;#39;mol&amp;#39;})
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>({&amp;#39;Geel&amp;#39;}, {&amp;#39;leeg&amp;#39;, &amp;#39;Geel&amp;#39;})
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>({&amp;#39;Sint-Niklaas&amp;#39;}, {&amp;#39;Sint-Niklaas&amp;#39;})
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>({&amp;#39;Drongen&amp;#39;}, {&amp;#39;gronden&amp;#39;})
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>({&amp;#39;Welle&amp;#39;}, {&amp;#39;welle&amp;#39;})
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>({&amp;#39;Ruisbroek&amp;#39;}, {&amp;#39;ruisbroek&amp;#39;})
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>({&amp;#39;Zele&amp;#39;}, {&amp;#39;zeel&amp;#39;})
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>({&amp;#39;Meiser&amp;#39;}, {&amp;#39;misere&amp;#39;, &amp;#39;remise&amp;#39;, &amp;#39;eremis&amp;#39;})
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>({&amp;#39;Spa&amp;#39;}, {&amp;#39;SAP&amp;#39;, &amp;#39;pas&amp;#39;})
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>({&amp;#39;Menen&amp;#39;}, {&amp;#39;menne&amp;#39;, &amp;#39;nemen&amp;#39;})
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>({&amp;#39;Lens&amp;#39;}, {&amp;#39;snel&amp;#39;})
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>({&amp;#39;Tielt&amp;#39;}, {&amp;#39;titel&amp;#39;})
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>({&amp;#39;Eupen&amp;#39;}, {&amp;#39;peune&amp;#39;})
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>({&amp;#39;Engis&amp;#39;}, {&amp;#39;signe&amp;#39;})
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>({&amp;#39;Eichem&amp;#39;}, {&amp;#39;chemie&amp;#39;})
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>({&amp;#39;Ekeren&amp;#39;}, {&amp;#39;ne keer&amp;#39;, &amp;#39;nekeer&amp;#39;})
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>({&amp;#39;Niel&amp;#39;}, {&amp;#39;lein&amp;#39;})
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>({&amp;#39;Manage&amp;#39;}, {&amp;#39;gemaan&amp;#39;})
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>({&amp;#39;Ronet&amp;#39;}, {&amp;#39;tenor&amp;#39;})
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>({&amp;#39;Lot&amp;#39;}, {&amp;#39;lot&amp;#39;, &amp;#39;tol&amp;#39;})
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>({&amp;#39;Essen&amp;#39;}, {&amp;#39;se-n-se&amp;#39;, &amp;#39;sense&amp;#39;})
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>({&amp;#39;Lobbes&amp;#39;}, {&amp;#39;belbos&amp;#39;})
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>({&amp;#39;Merode&amp;#39;}, {&amp;#39;moedre&amp;#39;})
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>({&amp;#39;Blankenberge&amp;#39;}, {&amp;#39;Blankenberge&amp;#39;})
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>({&amp;#39;Luttre&amp;#39;}, {&amp;#39;lutter&amp;#39;})
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>({&amp;#39;Lede&amp;#39;}, {&amp;#39;Deel&amp;#39;, &amp;#39;elde&amp;#39;, &amp;#39;leed&amp;#39;})
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>({&amp;#39;Wavre&amp;#39;}, {&amp;#39;verwa&amp;#39;})
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>({&amp;#39;Eine&amp;#39;}, {&amp;#39;eine&amp;#39;})
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>({&amp;#39;Tienen&amp;#39;}, {&amp;#39;ineten&amp;#39;, &amp;#39;tienen&amp;#39;})
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>({&amp;#39;Aalst&amp;#39;}, {&amp;#39;slaat&amp;#39;, &amp;#39;staal&amp;#39;})
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>({&amp;#39;Coo&amp;#39;}, {&amp;#39;C.O.O.&amp;#39;})
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>({&amp;#39;Mollem&amp;#39;, &amp;#39;Lommel&amp;#39;}, {&amp;#39;mollem&amp;#39;})
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>({&amp;#39;Simonis&amp;#39;}, {&amp;#39;simonis&amp;#39;})
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>({&amp;#39;Puurs&amp;#39;}, {&amp;#39;Pruus&amp;#39;})
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>
 &lt;div class="details-collapse" onclick="document.getElementById('details-6').open = false;">
 &lt;em>&lt;u>Click to collapse&lt;/u>&lt;/em>
 &lt;/div>
 &lt;/div>
 &lt;/details>
 &lt;div class="details-preview">
 &lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-text" data-lang="text">&lt;span style="display:flex;">&lt;span>({&amp;#39;Jette&amp;#39;}, {&amp;#39;tetje&amp;#39;})
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>({&amp;#39;Temse&amp;#39;}, {&amp;#39;smete&amp;#39;})
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>({&amp;#39;Lierde&amp;#39;}, {&amp;#39;leider&amp;#39;})
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>({&amp;#39;Leman&amp;#39;}, {&amp;#39;lamen&amp;#39;})
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>({&amp;#39;Ternat&amp;#39;}, {&amp;#39;ratten&amp;#39;})
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>({&amp;#39;Kijkuit&amp;#39;}, {&amp;#39;kijkuit&amp;#39;})
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>({&amp;#39;Boom&amp;#39;}, {&amp;#39;boom&amp;#39;})
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>({&amp;#39;Godinne&amp;#39;}, {&amp;#39;doening&amp;#39;})
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>({&amp;#39;Bleret&amp;#39;}, {&amp;#39;bretel&amp;#39;})
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>({&amp;#39;Muizen&amp;#39;}, {&amp;#39;muizen&amp;#39;})
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>({&amp;#39;Asse&amp;#39;}, {&amp;#39;asse&amp;#39;})
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>({&amp;#39;Landen&amp;#39;}, {&amp;#39;landen&amp;#39;})
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>({&amp;#39;Weerde&amp;#39;}, {&amp;#39;weerde&amp;#39;})
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>({&amp;#39;Mol&amp;#39;}, {&amp;#39;mol&amp;#39;})
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>({&amp;#39;Geel&amp;#39;}, {&amp;#39;leeg&amp;#39;, &amp;#39;Geel&amp;#39;})
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>({&amp;#39;Sint-Niklaas&amp;#39;}, {&amp;#39;Sint-Niklaas&amp;#39;})
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>({&amp;#39;Drongen&amp;#39;}, {&amp;#39;gronden&amp;#39;})
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>({&amp;#39;Welle&amp;#39;}, {&amp;#39;welle&amp;#39;})
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>({&amp;#39;Ruisbroek&amp;#39;}, {&amp;#39;ruisbroek&amp;#39;})
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>({&amp;#39;Zele&amp;#39;}, {&amp;#39;zeel&amp;#39;})
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>({&amp;#39;Meiser&amp;#39;}, {&amp;#39;misere&amp;#39;, &amp;#39;remise&amp;#39;, &amp;#39;eremis&amp;#39;})
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>({&amp;#39;Spa&amp;#39;}, {&amp;#39;SAP&amp;#39;, &amp;#39;pas&amp;#39;})
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>({&amp;#39;Menen&amp;#39;}, {&amp;#39;menne&amp;#39;, &amp;#39;nemen&amp;#39;})
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>({&amp;#39;Lens&amp;#39;}, {&amp;#39;snel&amp;#39;})
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>({&amp;#39;Tielt&amp;#39;}, {&amp;#39;titel&amp;#39;})
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>({&amp;#39;Eupen&amp;#39;}, {&amp;#39;peune&amp;#39;})
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>({&amp;#39;Engis&amp;#39;}, {&amp;#39;signe&amp;#39;})
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>({&amp;#39;Eichem&amp;#39;}, {&amp;#39;chemie&amp;#39;})
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>({&amp;#39;Ekeren&amp;#39;}, {&amp;#39;ne keer&amp;#39;, &amp;#39;nekeer&amp;#39;})
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>({&amp;#39;Niel&amp;#39;}, {&amp;#39;lein&amp;#39;})
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>({&amp;#39;Manage&amp;#39;}, {&amp;#39;gemaan&amp;#39;})
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>({&amp;#39;Ronet&amp;#39;}, {&amp;#39;tenor&amp;#39;})
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>({&amp;#39;Lot&amp;#39;}, {&amp;#39;lot&amp;#39;, &amp;#39;tol&amp;#39;})
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>({&amp;#39;Essen&amp;#39;}, {&amp;#39;se-n-se&amp;#39;, &amp;#39;sense&amp;#39;})
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>({&amp;#39;Lobbes&amp;#39;}, {&amp;#39;belbos&amp;#39;})
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>({&amp;#39;Merode&amp;#39;}, {&amp;#39;moedre&amp;#39;})
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>({&amp;#39;Blankenberge&amp;#39;}, {&amp;#39;Blankenberge&amp;#39;})
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>({&amp;#39;Luttre&amp;#39;}, {&amp;#39;lutter&amp;#39;})
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>({&amp;#39;Lede&amp;#39;}, {&amp;#39;Deel&amp;#39;, &amp;#39;elde&amp;#39;, &amp;#39;leed&amp;#39;})
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>({&amp;#39;Wavre&amp;#39;}, {&amp;#39;verwa&amp;#39;})
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>({&amp;#39;Eine&amp;#39;}, {&amp;#39;eine&amp;#39;})
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>({&amp;#39;Tienen&amp;#39;}, {&amp;#39;ineten&amp;#39;, &amp;#39;tienen&amp;#39;})
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>({&amp;#39;Aalst&amp;#39;}, {&amp;#39;slaat&amp;#39;, &amp;#39;staal&amp;#39;})
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>({&amp;#39;Coo&amp;#39;}, {&amp;#39;C.O.O.&amp;#39;})
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>({&amp;#39;Mollem&amp;#39;, &amp;#39;Lommel&amp;#39;}, {&amp;#39;mollem&amp;#39;})
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>({&amp;#39;Simonis&amp;#39;}, {&amp;#39;simonis&amp;#39;})
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>({&amp;#39;Puurs&amp;#39;}, {&amp;#39;Pruus&amp;#39;})
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>
 &lt;div class="fade-overlay">&lt;/div>
 &lt;/div>
 &lt;div class="details-toggle" onclick="document.getElementById('details-6').open = true;">
 &lt;em>&lt;u>Click to expand&lt;/u>&lt;/em>
 &lt;/div>
&lt;/div>

&lt;style>
.details-wrapper {
 position: relative;
}

.details-wrapper details {
 margin-top: 0;
 margin-bottom: 0;
}

.details-content {
 margin-top: 0.75rem;
}

.details-preview {
 position: relative;
 max-height: 200px;
 overflow: hidden;
 margin-top: 0.75rem;
 margin-bottom: 0.5rem;
}

.details-preview pre {
 margin-top: 0 !important;
}

.details-content pre {
 margin-bottom: 0 !important;
}

.details-preview .fade-overlay {
 position: absolute;
 bottom: 0;
 left: 0;
 right: 0;
 height: 60px;
 background: linear-gradient(transparent, var(--background, #f8f9fa));
 pointer-events: none;
}

.details-toggle {
 text-align: center;
 margin-top: 0.25rem;
 font-size: 0.7em;
 color: var(--text-color, #aaa);
 cursor: pointer;
 opacity: 0.6;
}

.details-summary {
 display: none;
}

.details-collapse {
 text-align: center;
 margin-top: 0.25rem;
 font-size: 0.7em;
 color: var(--text-color, #aaa);
 cursor: pointer;
 opacity: 0.6;
}

.details-wrapper details[open] + .details-preview {
 display: none;
}

.details-wrapper details[open] + .details-preview + .details-toggle {
 display: none;
}

.details-wrapper details:not([open]) .details-content {
 display: none;
}

 
@media (prefers-color-scheme: dark) {
 .details-preview .fade-overlay {
 background: linear-gradient(transparent, var(--background, #1a1a1a));
 }
 
 .details-toggle {
 color: var(--text-color, #888);
 }
 
 .details-collapse {
 color: var(--text-color, #888);
 }
}
&lt;/style>
&lt;h3 id="multi-word-anagrams">Multi-word anagrams&lt;/h3>
&lt;p>Above, we only look at one-to-one mappings where a shuffling of one Flemish dialect word gives us a Belgian railway station name. What if we want to have a combination of two (or more) words that, when shuffled together, gives us a station name?&lt;/p>
&lt;h4 id="bruteforce">Bruteforce&lt;/h4>
&lt;p>A bruteforce approach to multi-word anagrams is straightforward: for anagrams of one word list that are made up of $n$ words of the other wordlist, we can take all unordered combinations of $n$ keys of one alphabetic map with replacement, combine them, and check if this combined key is present in the other alphabetic map:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>&lt;span style="font-weight:bold;text-decoration:underline">from&lt;/span> &lt;span style="color:#666;font-weight:bold;font-style:italic">itertools&lt;/span> &lt;span style="font-weight:bold;text-decoration:underline">import&lt;/span> combinations_with_replacement &lt;span style="font-weight:bold;text-decoration:underline">as&lt;/span> cwr
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="font-weight:bold;text-decoration:underline">def&lt;/span> &lt;span style="color:#666;font-weight:bold;font-style:italic">combine_keys&lt;/span>(*keys):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>	&lt;span style="font-weight:bold;text-decoration:underline">return&lt;/span> &lt;span style="color:#666;font-style:italic">&amp;#34;&amp;#34;&lt;/span>.join(&lt;span style="font-weight:bold;font-style:italic">sorted&lt;/span>(&lt;span style="color:#666;font-style:italic">&amp;#34;&amp;#34;&lt;/span>.join(keys)))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="font-weight:bold;text-decoration:underline">def&lt;/span> &lt;span style="color:#666;font-weight:bold;font-style:italic">n_agrams&lt;/span>(n):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>	keys = &lt;span style="font-weight:bold;font-style:italic">list&lt;/span>(grouped_wordlist)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>	&lt;span style="font-weight:bold;text-decoration:underline">for&lt;/span> key &lt;span style="font-weight:bold">in&lt;/span> cwr(keys, n):
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>		word_sets = (grouped_wordlist[k] &lt;span style="font-weight:bold;text-decoration:underline">for&lt;/span> k &lt;span style="font-weight:bold">in&lt;/span> key)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>		&lt;span style="font-weight:bold;text-decoration:underline">if&lt;/span> (combined_key := combine_keys(*key)) &lt;span style="font-weight:bold">in&lt;/span> grouped_stations:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>			stations = grouped_stations[combined_key]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>			&lt;span style="font-weight:bold;font-style:italic">print&lt;/span>(&lt;span style="color:#666;font-style:italic">f&lt;/span>&lt;span style="color:#666;font-style:italic">&amp;#34;&lt;/span>&lt;span style="color:#666;font-style:italic">{&lt;/span>stations&lt;span style="color:#666;font-style:italic">}&lt;/span>&lt;span style="color:#666;font-style:italic"> = &lt;/span>&lt;span style="color:#666;font-style:italic">{&lt;/span>&lt;span style="font-weight:bold;font-style:italic">list&lt;/span>(word_sets)&lt;span style="color:#666;font-style:italic">}&lt;/span>&lt;span style="color:#666;font-style:italic">&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>For $n = 2$, we get 1078 different combinations:&lt;/p>

&lt;div class="details-wrapper">
 &lt;details id="details-7">
 &lt;summary class="details-summary">Details&lt;/summary>
 &lt;div class="details-content">
 &lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-text" data-lang="text">&lt;span style="display:flex;">&lt;span>{&amp;#39;Diesdelle&amp;#39;} = {&amp;#39;dees&amp;#39;} + {&amp;#39;dille&amp;#39;}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{&amp;#39;Kapelle-op-den-Bos&amp;#39;} = {&amp;#39;bedelke&amp;#39;} + {&amp;#39;salonpop&amp;#39;}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{&amp;#39;Kapelle-op-den-Bos&amp;#39;} = {&amp;#39;poekele&amp;#39;, &amp;#39;poeleke&amp;#39;} + {&amp;#39;plonsbad&amp;#39;}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{&amp;#39;Kapelle-op-den-Bos&amp;#39;} = {&amp;#39;sloppel&amp;#39;} + {&amp;#39;bakendoe&amp;#39;}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{&amp;#39;Kortemark&amp;#39;} = {&amp;#39;karot&amp;#39;} + {&amp;#39;merk&amp;#39;}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{&amp;#39;Kortemark&amp;#39;} = {&amp;#39;krak&amp;#39;} + {&amp;#39;om ter&amp;#39;}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{&amp;#39;Kortemark&amp;#39;} = {&amp;#39;kram&amp;#39;} + {&amp;#39;kroet&amp;#39;, &amp;#39;krote&amp;#39;, &amp;#39;roket&amp;#39;}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{&amp;#39;Kortemark&amp;#39;} = {&amp;#39;kroam&amp;#39;, &amp;#39;kraom&amp;#39;} + {&amp;#39;trek&amp;#39;}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{&amp;#39;Kortemark&amp;#39;} = {&amp;#39;rakker&amp;#39;, &amp;#39;kraker&amp;#39;} + {&amp;#39;mot&amp;#39;}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{&amp;#39;Kortemark&amp;#39;} = {&amp;#39;ram&amp;#39;} + {&amp;#39;kroket&amp;#39;}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{&amp;#39;Landskouter&amp;#39;} = {&amp;#39;dals&amp;#39;} + {&amp;#39;konteur&amp;#39;}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{&amp;#39;Landskouter&amp;#39;} = {&amp;#39;dokteur&amp;#39;} + {&amp;#39;lans&amp;#39;}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{&amp;#39;Landskouter&amp;#39;} = {&amp;#39;duks&amp;#39;} + {&amp;#39;lantoer&amp;#39;}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{&amp;#39;Landskouter&amp;#39;} = {&amp;#39;dutsen&amp;#39;} + {&amp;#39;akrol&amp;#39;}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{&amp;#39;Landskouter&amp;#39;} = {&amp;#39;kadul&amp;#39;} + {&amp;#39;storen&amp;#39;, &amp;#39;roste(n)&amp;#39;, &amp;#39;rotsen&amp;#39;}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>...
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>
 &lt;div class="details-collapse" onclick="document.getElementById('details-7').open = false;">
 &lt;em>&lt;u>Click to collapse&lt;/u>&lt;/em>
 &lt;/div>
 &lt;/div>
 &lt;/details>
 &lt;div class="details-preview">
 &lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-text" data-lang="text">&lt;span style="display:flex;">&lt;span>{&amp;#39;Diesdelle&amp;#39;} = {&amp;#39;dees&amp;#39;} + {&amp;#39;dille&amp;#39;}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{&amp;#39;Kapelle-op-den-Bos&amp;#39;} = {&amp;#39;bedelke&amp;#39;} + {&amp;#39;salonpop&amp;#39;}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{&amp;#39;Kapelle-op-den-Bos&amp;#39;} = {&amp;#39;poekele&amp;#39;, &amp;#39;poeleke&amp;#39;} + {&amp;#39;plonsbad&amp;#39;}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{&amp;#39;Kapelle-op-den-Bos&amp;#39;} = {&amp;#39;sloppel&amp;#39;} + {&amp;#39;bakendoe&amp;#39;}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{&amp;#39;Kortemark&amp;#39;} = {&amp;#39;karot&amp;#39;} + {&amp;#39;merk&amp;#39;}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{&amp;#39;Kortemark&amp;#39;} = {&amp;#39;krak&amp;#39;} + {&amp;#39;om ter&amp;#39;}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{&amp;#39;Kortemark&amp;#39;} = {&amp;#39;kram&amp;#39;} + {&amp;#39;kroet&amp;#39;, &amp;#39;krote&amp;#39;, &amp;#39;roket&amp;#39;}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{&amp;#39;Kortemark&amp;#39;} = {&amp;#39;kroam&amp;#39;, &amp;#39;kraom&amp;#39;} + {&amp;#39;trek&amp;#39;}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{&amp;#39;Kortemark&amp;#39;} = {&amp;#39;rakker&amp;#39;, &amp;#39;kraker&amp;#39;} + {&amp;#39;mot&amp;#39;}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{&amp;#39;Kortemark&amp;#39;} = {&amp;#39;ram&amp;#39;} + {&amp;#39;kroket&amp;#39;}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{&amp;#39;Landskouter&amp;#39;} = {&amp;#39;dals&amp;#39;} + {&amp;#39;konteur&amp;#39;}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{&amp;#39;Landskouter&amp;#39;} = {&amp;#39;dokteur&amp;#39;} + {&amp;#39;lans&amp;#39;}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{&amp;#39;Landskouter&amp;#39;} = {&amp;#39;duks&amp;#39;} + {&amp;#39;lantoer&amp;#39;}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{&amp;#39;Landskouter&amp;#39;} = {&amp;#39;dutsen&amp;#39;} + {&amp;#39;akrol&amp;#39;}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{&amp;#39;Landskouter&amp;#39;} = {&amp;#39;kadul&amp;#39;} + {&amp;#39;storen&amp;#39;, &amp;#39;roste(n)&amp;#39;, &amp;#39;rotsen&amp;#39;}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>...
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>
 &lt;div class="fade-overlay">&lt;/div>
 &lt;/div>
 &lt;div class="details-toggle" onclick="document.getElementById('details-7').open = true;">
 &lt;em>&lt;u>Click to expand&lt;/u>&lt;/em>
 &lt;/div>
&lt;/div>

&lt;style>
.details-wrapper {
 position: relative;
}

.details-wrapper details {
 margin-top: 0;
 margin-bottom: 0;
}

.details-content {
 margin-top: 0.75rem;
}

.details-preview {
 position: relative;
 max-height: 200px;
 overflow: hidden;
 margin-top: 0.75rem;
 margin-bottom: 0.5rem;
}

.details-preview pre {
 margin-top: 0 !important;
}

.details-content pre {
 margin-bottom: 0 !important;
}

.details-preview .fade-overlay {
 position: absolute;
 bottom: 0;
 left: 0;
 right: 0;
 height: 60px;
 background: linear-gradient(transparent, var(--background, #f8f9fa));
 pointer-events: none;
}

.details-toggle {
 text-align: center;
 margin-top: 0.25rem;
 font-size: 0.7em;
 color: var(--text-color, #aaa);
 cursor: pointer;
 opacity: 0.6;
}

.details-summary {
 display: none;
}

.details-collapse {
 text-align: center;
 margin-top: 0.25rem;
 font-size: 0.7em;
 color: var(--text-color, #aaa);
 cursor: pointer;
 opacity: 0.6;
}

.details-wrapper details[open] + .details-preview {
 display: none;
}

.details-wrapper details[open] + .details-preview + .details-toggle {
 display: none;
}

.details-wrapper details:not([open]) .details-content {
 display: none;
}

 
@media (prefers-color-scheme: dark) {
 .details-preview .fade-overlay {
 background: linear-gradient(transparent, var(--background, #1a1a1a));
 }
 
 .details-toggle {
 color: var(--text-color, #888);
 }
 
 .details-collapse {
 color: var(--text-color, #888);
 }
}
&lt;/style>
&lt;p>For $n \geq 3$, however, things start to slow down drastically. This naive bruteforce approach could further be optimised through some tricks to disregard impossible candidates, but as $n$ increases this would be &lt;a href="https://en.wiktionary.org/wiki/dweilen_met_de_kraan_open">&lt;em>&amp;ldquo;mopping with the faucet running&amp;rdquo;&lt;/em>&lt;/a>, as we say in Dutch. For $n \geq 3$, we could use a specialised data structure.&lt;/p>
&lt;h4 id="anatrees">Anatrees&lt;/h4>
&lt;p>An &lt;a href="https://en.wikipedia.org/wiki/Anatree">anatree&lt;/a>&lt;label for="sidenote-8" class="sidenote-number">&lt;/label>&lt;input type="checkbox" id="sidenote-8" class="sidenote-toggle">&lt;span class="sidenote">Charles Reams. 2012. &lt;strong>Anatree: A Fast Data Structure for Anagrams.&lt;/strong> ACM J. Exp. Algorithmics 17, Article 1.1 (2012), 16 pages. &lt;a href="https://doi.org/10.1145/2133803.2133804">https://doi.org/10.1145/2133803.2133804&lt;/a>&lt;/span> is a directed edge-labelled tree that describes a set of words. Internal nodes of the anatree represent symbols of an alphabet, and the leaves represent subsets of the set of words. Edges are labelled with positive integers, including zero. Paths of nodes $n_1, \ldots, n_l$ from the root $n_1$ to a leaf $n_l$ along edges labelled with integers $e_1, \ldots, e_l$ arrive at a leaf $n_l$, representing the subset of words that contains exactly $e_i$ times the symbol $n_i$, for all nodes on its path.&lt;/p>

&lt;figure>
 &lt;img src="http://patrick.vanderspie.gl/posts/scrambled-stations/anatree.svg" alt="One possible anatree for the same set of words {&amp;#39;odd&amp;#39;, &amp;#39;goo&amp;#39;, &amp;#39;dog&amp;#39;, &amp;#39;fog&amp;#39;, &amp;#39;loo&amp;#39;} as used in the original anatree paper, using an own implementation of anatrees and Graphviz" class="invert-dark" loading="lazy" />
 &lt;figcaption>One possible anatree for the same set of words {&amp;#39;odd&amp;#39;, &amp;#39;goo&amp;#39;, &amp;#39;dog&amp;#39;, &amp;#39;fog&amp;#39;, &amp;#39;loo&amp;#39;} as used in the original anatree paper, using an own implementation of anatrees and Graphviz&lt;/figcaption>
&lt;/figure>


&lt;p>The anatree shown above is just one possible anatree for a certain set of words, as there is not just &lt;em>one&lt;/em> anatree for a given set. You could have different strategies to pick which character a certain node represents, each resulting in a different anatree. To generate the figure used above, at every step of construction, the most frequently occurring but not yet considered character of the remaining set of words is used as the symbol for the next node. Creating the anatree for the Flemish wordlist resulted in a tree consisting of 172.210 nodes, if we use the least frequent character first. If we use the most frequent character first, we get 127.534 nodes. Other strategies might lead to further reduction of the size of the anatree.&lt;/p>
&lt;p>One approach to find multi-word anagrams of one specific train station is starting with that station&amp;rsquo;s bag of letters and using it as a &amp;ldquo;budget&amp;rdquo; while traversing the anatree of dialect words. For a train station with two occurrences of the letter &lt;em>e&lt;/em>, for example, we could explore paths that contain at most two &lt;em>e&lt;/em>&amp;rsquo;s, subtracting &amp;ldquo;spent&amp;rdquo; &lt;em>e&lt;/em>&amp;rsquo;s from our available budget. After reaching a leaf node on a partial budget, we could explore other paths with the remaining budget. If we are able to reach another leaf with that remaining budget, we have found a multi-word anagram.&lt;/p>
&lt;p>I did not implement this multi-word anagram algorithm in the end, as I&amp;rsquo;m not sure on performance (you&amp;rsquo;d have to perform &lt;em>a lot&lt;/em> of different anatree traversals to find multi-word anagrams, and there are a lot of different ways in which you could &amp;ldquo;spend&amp;rdquo; your budget). Exploring this idea already took a bit more time than expected, and I was happy with my $n = 2$ anagram list.&lt;/p>
&lt;hr>
&lt;h2 id="side-notes">Side notes&lt;/h2>
&lt;h3 id="displaying-anagrams">Displaying anagrams&lt;/h3>
&lt;p>To display these anagrams (and eventually also the title of this post), I wanted to imitate the classic Belgian railway station name signs using &lt;code>CSS&lt;/code>. Looking for the typeface, I landed on a &lt;a href="https://www.hgbtf.net/viewtopic.php?t=13372">2017 thread on a Belgian train forum&lt;/a> where a poster named &lt;code>Nadieeh&lt;/code> found out that the Brussels-based graphic design studio &lt;a href="https://speculoos.com/">Speculoos&lt;/a> created &lt;em>Alfphabet&lt;/em>, a digitised version of the typeface I was looking for. I ended up downloading the typeface from the &lt;a href="https://osp.kitchen/foundry/alfphabet/tree/master/">osp.kitchen&lt;/a>.&lt;/p>
&lt;p>The &lt;code>FONTLOG.txt&lt;/code>, which is included with a download of &lt;em>Alfphabet&lt;/em>, also contains some typical Belgian history:
&lt;div class="quote-block">
 &lt;blockquote class="quote-content">The Alfphabet family is based on the Belgian road signage called &amp;lsquo;Alphabet&amp;rsquo; in French and &amp;lsquo;Alfabet&amp;rsquo; in Flemish. It was introduced in 1945 by 3M system working for the Marshall plan after the end of the war. In 1975, it was replaced by the Swiss SNV fonts, but is still in used randomly by the Belgian railroad and Charleroi&amp;rsquo;s metro. In the early nineties, &lt;strong>Pierre Huyghebaert was able to copy the original plates just before the split of the national office of the roads &amp;lsquo;Fond des Routes&amp;rsquo; in three regional entities and the burial of the documents deep into regional archives.&lt;/strong>&lt;/blockquote>
&lt;/div>&lt;div class="quote-source-link">
 &lt;a href="https://osp.kitchen/foundry/alfphabet/tree/master/FONTLOG.txt#project-detail-files" target="_blank" rel="noopener">Alfphabet FONTLOG.txt&lt;/a>
&lt;/div>&lt;/p>
&lt;p>For the background colour, I used a colour picker on a picture of an old railway station sign to end up with &lt;code>rgb(0, 33, 84)&lt;/code>, and with some trial and error managed to get a white rounded border to simulate old railway signage:&lt;/p>
&lt;div class="alfphabet-display alfphabet-scale-medium">
Alfphabet Typeface
&lt;/div>
&lt;h3 id="shortest-stations">Shortest stations&lt;/h3>
&lt;p>Going through the &lt;code>CSV&lt;/code> of train stations with a friend, we were curious about the shortest name for a station in Belgium, which turns out to be &lt;a href="https://en.wikipedia.org/wiki/Sy_railway_station">&lt;em>Sy&lt;/em>&lt;/a> (part of Ferrières near the Ourthe river):&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>$ awk &lt;span style="color:#666;font-style:italic">&amp;#39;{ print length, $0 }&amp;#39;&lt;/span> railway_stations.txt | sort -n | cut -d&lt;span style="color:#666;font-style:italic">&amp;#34; &amp;#34;&lt;/span> -f2- | head -n 5
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>Sy
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>Ans
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>Ath
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>Aye
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>Coo
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>The &lt;a href="https://nl.wikipedia.org/wiki/Station_Sy">Dutch Wikipedia page&lt;/a> claims that Sy is the shortest name for a train station in the whole Benelux.&lt;/p>
&lt;div class="alfphabet-display alfphabet-scale-small">
Sy
&lt;/div>
&lt;h3 id="bucket-lists">Bucket lists&lt;/h3>
&lt;p>I recently read Adam Aaronson&amp;rsquo;s &lt;a href="https://aaronson.org/blog/i-drank-every-cocktail">&amp;quot;&lt;em>I Drank Every Cocktail&lt;/em>&amp;quot;&lt;/a> about how he managed to drink all &lt;a href="https://en.wikipedia.org/wiki/List_of_IBA_official_cocktails">&amp;ldquo;IBA official cocktails&amp;rdquo;&lt;/a> spread over a couple of years; and I loved his story.&lt;/p>
&lt;p>Based on his journey, I wonder how long it would take me to visit all Belgian railway stations. Not just passing the station by train, but to actually visit and take a picture with one of the station name signs.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>$ xan filter &lt;span style="color:#666;font-style:italic">&amp;#39;col(&amp;#34;country-code&amp;#34;) eq &amp;#34;be&amp;#34;&amp;#39;&lt;/span> railway_stations.csv | &lt;span style="color:#666;font-style:italic">\ &lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> xan count
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> 
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>578
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Almost 600 stations spread over a couple of years is certainly not impossible, I guess. Who knows, maybe I&amp;rsquo;ll be able to post my own &lt;em>&amp;ldquo;I Visited Every Station&amp;rdquo;&lt;/em> eventually!&lt;/p></description></item><item><title>Cryptmanteaux</title><link>http://patrick.vanderspie.gl/posts/cryptmanteaux/</link><pubDate>Sun, 01 Jun 2025 00:00:00 +0000</pubDate><guid>http://patrick.vanderspie.gl/posts/cryptmanteaux/</guid><description>&lt;p>I recently learned through Wikipedia and Wiktionary that the word &lt;a href="https://en.wiktionary.org/wiki/electrocution">&lt;em>electrocution&lt;/em>&lt;/a> is a &lt;a href="https://en.wikipedia.org/wiki/Portmanteau">portmanteau&lt;/a> of &lt;em>electro-&lt;/em> and &lt;em>execution&lt;/em>.&lt;label for="sidenote-0" class="sidenote-number">&lt;/label>&lt;input type="checkbox" id="sidenote-0" class="sidenote-toggle">&lt;span class="sidenote">&lt;em>The New York Times&lt;/em> hating the word &lt;em>electrocution&lt;/em>, and Thomas Edison preferring alternative words like &lt;em>dynamort&lt;/em>, are also some fun facts found &lt;a href="https://en.wikipedia.org/wiki/Electrocution#Etymology">on Wikipedia&lt;/a>.&lt;/span> This makes sense in hindsight, but it never clicked before that the &lt;em>-cution&lt;/em> part of &lt;em>electrocution&lt;/em> comes from &lt;em>execution&lt;/em>. As I like short word puzzles (like &lt;a href="https://www.minutecryptic.com/">Minute Cryptic&lt;/a> and &lt;a href="https://www.nytimes.com/games/connections">Connections&lt;/a>), I wondered if there were any similar word games that use portmanteaux as their main mechanic.&lt;/p>
&lt;p>In cryptic crosswords, you find words based on clues which themselves are kind of word puzzles consisting of a definition (typically at the start or end of a clue) and the wordplay (giving instructions on how to get the answer based on the clue). As an easy example clue, which I &lt;a href="https://github.com/bodasadallah/decrypting-crosswords/blob/main/data/200_clues.csv">found online&lt;/a>:&lt;/p>
&lt;div class="quote-block">
 &lt;blockquote class="quote-content">Error concealed by city police (4)&lt;/blockquote>
&lt;/div>&lt;div class="quote-source-link">
 &lt;a href="https://github.com/bodasadallah/decrypting-crosswords/blob/main/data/200_clues.csv#L32" target="_blank" rel="noopener">decrypting-crosswords&lt;/a>
&lt;/div>
&lt;p>Here, &lt;em>&amp;ldquo;error&amp;rdquo;&lt;/em> is the definition: we&amp;rsquo;re looking for an alternative word for &lt;em>&amp;ldquo;error&amp;rdquo;&lt;/em>. The words &lt;em>&amp;ldquo;concealed by&amp;rdquo;&lt;/em> indicate that the four-letter word we&amp;rsquo;re looking for is hidden inside the words &lt;em>&amp;ldquo;ci&lt;strong>ty po&lt;/strong>lice&amp;rdquo;&lt;/em>. Other possible types of wordplay include anagrams, double definitions, homophones, and more (as seen on the &lt;a href="https://www.minutecryptic.com/guide">Minute Cryptic guide&lt;/a>.)&lt;/p>
&lt;hr>
&lt;p>Combining portmanteaux, cryptic crosswords, and Connections gives us a word game where you need to find two related portmanteaux through four cryptic clues that describe four base words. By correctly matching pairs of base words, you find the two hidden portmanteaux. Try it out, and check &lt;a href="#walkthrough">the walkthrough&lt;/a> afterwards!&lt;/p>


&lt;div class="word-game" id="game-crypt">
 &lt;style>
 .word-game {
 max-width: 900px;
 margin: 20px auto;
 padding: 20px;
 border: 2px solid #e1e5e9;
 border-radius: 12px;
 background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);
 font-family: var(--), Tahoma, Geneva, Verdana, sans-serif;
 box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
 }

 .word-game h3 {
 text-align: center;
 color: #2c3e50;
 margin-bottom: 20px;
 font-size: 1.5em;
 }

 .clues-container {
 display: grid;
 grid-template-columns: 1fr 1fr;
 gap: 15px;
 margin-bottom: 25px;
 }

 .clue-box {
 background: white;
 padding: 15px;
 border-radius: 8px;
 border-left: 4px solid var(--highlight, #3498db);
 box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
 transition: transform 0.2s ease;
 }

 .clue-box:hover {
 transform: translateY(-2px);
 box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
 }

 .clue-number {
 font-weight: bold;
 color: var(--highlight, #3498db);
 font-size: 0.8em;
 margin-bottom: 5px;
 }

 .clue-text {
 color: #2c3e50;
 line-height: 1;
 font-size: 0.8em;
 }

 .answers-section {
 background: white;
 padding: 20px;
 border-radius: 8px;
 margin-bottom: 20px;
 }

 .word-lengths-info {
 text-align: center;
 margin-bottom: 20px;
 padding: 12px;
 background: #f8f9fa;
 border: 1px solid #dee2e6;
 border-radius: 6px;
 color: #495057;
 font-size: 0.9em;
 font-weight: 500;
 }

 .answer-inputs {
 display: grid;
 grid-template-columns: 1fr 1fr;
 gap: 15px;
 margin-bottom: 20px;
 }

 .answer-group {
 display: flex;
 flex-direction: column;
 }

 .answer-group label {
 font-weight: bold;
 color: #2c3e50;
 margin-bottom: 8px;
 font-size: 0.8em;
 }

 .answer-group input {
 padding: 12px;
 border: 2px solid #ddd;
 border-radius: 6px;
 font-size: 0.8em;
 transition: border-color 0.2s ease;
 }

 .answer-group input:focus {
 outline: none;
 border-color: #3498db;
 box-shadow: 0 0 0 3px rgba(52, 152, 219, 0.1);
 }

 .answer-group input.correct {
 border-color: #27ae60;
 background-color: #d5f4e6;
 }

 .answer-group input.incorrect {
 border-color: #e74c3c;
 background-color: #fdf2f2;
 }

 .game-controls {
 text-align: center;
 }

 .check-btn {
 background: linear-gradient(135deg, var(--highlight, #3498db), color-mix(in srgb, var(--highlight, #3498db) 80%, black));
 color: white;
 border: none;
 padding: 10px 20px;
 font-size: 0.8em;
 border-radius: 6px;
 cursor: pointer;
 transition: all 0.2s ease;
 margin-right: 10px;
 }

 .check-btn:hover {
 background: linear-gradient(135deg, color-mix(in srgb, var(--highlight, #3498db) 80%, black), color-mix(in srgb, var(--highlight, #3498db) 65%, black));
 transform: translateY(-1px);
 }

 .reset-btn {
 background: linear-gradient(135deg, #95a5a6, #7f8c8d);
 color: white;
 border: none;
 margin-top: 10px;
 padding: 10px 20px;
 font-size: 0.8em;
 border-radius: 6px;
 cursor: pointer;
 transition: all 0.2s ease;
 }

 .reset-btn:hover {
 background: linear-gradient(135deg, #7f8c8d, #6c7b7d);
 transform: translateY(-1px);
 }

 .feedback {
 margin-top: 15px;
 padding: 15px;
 border-radius: 6px;
 text-align: center;
 font-weight: bold;
 display: none;
 }

 .feedback.success {
 background-color: #d5f4e6;
 color: #27ae60;
 border: 1px solid #27ae60;
 }

 .feedback.partial {
 background-color: #fff3cd;
 color: #856404;
 border: 1px solid #ffc107;
 }

 .feedback.error {
 background-color: #fdf2f2;
 color: #e74c3c;
 border: 1px solid #e74c3c;
 }

 
 .dark .word-game {
 border: 2px solid #3a3a3a;
 background: linear-gradient(135deg, #2c2c2c 0%, #1e1e1e 100%);
 box-shadow: 0 4px 6px rgba(0, 0, 0, 0.3);
 }

 .dark .word-game h3 {
 color: var(--content-primary);
 }

 .dark .clue-box {
 background: #2a2a2a;
 border-left: 4px solid var(--highlight, #4a90e2);
 box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
 }

 .dark .clue-box:hover {
 box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3);
 }

 .dark .clue-number {
 color: var(--highlight, #4a90e2);
 }

 .dark .clue-text {
 color: var(--content-primary);
 }

 .dark .answers-section {
 background: #2a2a2a;
 }

 .dark .word-lengths-info {
 background: #333333;
 border: 1px solid #444444;
 color: var(--content-secondary);
 }

 .dark .answer-group label {
 color: var(--content-primary);
 }

 .dark .answer-group input {
 background: #333333;
 border: 2px solid #555555;
 color: var(--content-primary);
 }

 .dark .answer-group input:focus {
 border-color: #4a90e2;
 box-shadow: 0 0 0 3px rgba(74, 144, 226, 0.2);
 }

 .dark .answer-group input.correct {
 border-color: #4ade80;
 background-color: #1f2937;
 }

 .dark .answer-group input.incorrect {
 border-color: #f87171;
 background-color: #2d1b1b;
 }

 .dark .feedback.success {
 background-color: #1f2937;
 color: #4ade80;
 border: 1px solid #4ade80;
 }

 .dark .feedback.partial {
 background-color: #2d2618;
 color: #fbbf24;
 border: 1px solid #fbbf24;
 }

 .dark .feedback.error {
 background-color: #2d1b1b;
 color: #f87171;
 border: 1px solid #f87171;
 }

 @media (max-width: 600px) {
 .clues-container,
 .answer-inputs {
 grid-template-columns: 1fr;
 }
 
 .word-game {
 margin: 10px;
 padding: 15px;
 }
 }
 &lt;/style>

 
 
 &lt;div class="clues-container">
 &lt;div class="clue-box" id="clue-1">
 &lt;div class="clue-number">Clue 1&lt;/div>
 &lt;div class="clue-text">Fast brake ruined morning meal. (9)&lt;/div>
 &lt;/div>
 &lt;div class="clue-box" id="clue-2">
 &lt;div class="clue-number">Clue 2&lt;/div>
 &lt;div class="clue-text">Dairy product makes one grin! (6)&lt;/div>
 &lt;/div>
 &lt;div class="clue-box" id="clue-3">
 &lt;div class="clue-number">Clue 3&lt;/div>
 &lt;div class="clue-text">Noon activity sounds like lunging. (8)&lt;/div>
 &lt;/div>
 &lt;div class="clue-box" id="clue-4">
 &lt;div class="clue-number">Clue 4&lt;/div>
 &lt;div class="clue-text">Sandwiches for German citizens. (10)&lt;/div>
 &lt;/div>
 &lt;/div>

 &lt;div class="answers-section">
 &lt;div class="word-lengths-info">
 Find two portmanteaux: one with (9) letters and one with (13) letters!
 &lt;/div>

 &lt;div class="answer-inputs">
 &lt;div class="answer-group">
 &lt;label for="answer1-crypt">Answer 1:&lt;/label>
 &lt;input type="text" id="answer1-crypt" placeholder="Enter your first answer">
 &lt;/div>
 &lt;div class="answer-group">
 &lt;label for="answer2-crypt">Answer 2:&lt;/label>
 &lt;input type="text" id="answer2-crypt" placeholder="Enter your second answer">
 &lt;/div>
 &lt;/div>

 &lt;div class="game-controls">
 &lt;button class="check-btn" onclick="checkAnswers('crypt')">Check Answers&lt;/button>
 &lt;button class="reset-btn" onclick="resetGame('crypt')">Reset&lt;/button>
 &lt;/div>

 &lt;div class="feedback" id="feedback-crypt">&lt;/div>
 &lt;/div>

 &lt;script>
 
 window.gameAnswers = window.gameAnswers || {};
 window.gameAnswers['crypt'] = {
 answer1: 'brunching',
 answer2: 'cheeseburgers'
 };

 function checkAnswers(gameId) {
 const input1 = document.getElementById(`answer1-${gameId}`);
 const input2 = document.getElementById(`answer2-${gameId}`);
 const feedback = document.getElementById(`feedback-${gameId}`);
 
 const userAnswer1 = input1.value.toLowerCase().trim();
 const userAnswer2 = input2.value.toLowerCase().trim();
 
 const correctAnswers = window.gameAnswers[gameId];
 
 
 input1.className = '';
 input2.className = '';
 
 let correctCount = 0;
 
 
 if (userAnswer1 === correctAnswers.answer1 || userAnswer1 === correctAnswers.answer2) {
 input1.className = 'correct';
 correctCount++;
 } else if (userAnswer1 !== '') {
 input1.className = 'incorrect';
 }
 
 
 if ((userAnswer2 === correctAnswers.answer1 || userAnswer2 === correctAnswers.answer2) &amp;&amp; userAnswer2 !== userAnswer1) {
 input2.className = 'correct';
 correctCount++;
 } else if (userAnswer2 !== '') {
 input2.className = 'incorrect';
 }
 
 
 feedback.style.display = 'block';
 
 if (correctCount === 2) {
 feedback.className = 'feedback success';
 feedback.textContent = '🎉 Both answers are correct!';
 } else if (correctCount === 1) {
 feedback.className = 'feedback partial';
 feedback.textContent = '👍 One answer is correct!';
 } else {
 feedback.className = 'feedback error';
 feedback.textContent = '❌ Try again!';
 }
 }

 function resetGame(gameId) {
 const input1 = document.getElementById(`answer1-${gameId}`);
 const input2 = document.getElementById(`answer2-${gameId}`);
 const feedback = document.getElementById(`feedback-${gameId}`);
 
 input1.value = '';
 input2.value = '';
 input1.className = '';
 input2.className = '';
 feedback.style.display = 'none';
 }

 
 document.addEventListener('DOMContentLoaded', function() {
 const gameContainer = document.getElementById('game-crypt');
 const inputs = gameContainer.querySelectorAll('input[type="text"]');
 
 inputs.forEach(input => {
 input.addEventListener('keypress', function(e) {
 if (e.key === 'Enter') {
 checkAnswers('crypt');
 }
 });
 });
 });
 &lt;/script>
&lt;/div>

&lt;hr>
&lt;p>Coming up with a name for this game, I found the made-up portmanteau of &lt;em>cryptic&lt;/em> and &lt;em>portmanteau&lt;/em> to be appropriate: &lt;em>&amp;ldquo;cryptmanteau&amp;rdquo;&lt;/em>. I guess &lt;a href="https://www.themarginalian.org/2012/05/10/mark-twain-helen-keller-plagiarism-originality/">all ideas really are second-hand&lt;/a>, as Googling this name returns a &lt;a href="https://elsiitk.wordpress.com/wp-content/uploads/2015/03/galaxy-word-games-av-round.pdf">PDF file dating back to 2015&lt;/a>, containing a game where you are given two cryptic clues for two words that form a portmanteau when combined.&lt;/p>
&lt;p>










&lt;figure class="">

 &lt;div>
 &lt;img loading="lazy" alt="Google search results for cryptmanteau, with one result: a pdf titled GALAXY WORD GAMES from the English Literary Society" src=" /posts/cryptmanteaus/google.png">
 &lt;/div>

 
 
 &lt;div class="caption-container">
 &lt;figcaption> Google search results for cryptmanteau, with one result: a pdf titled GALAXY WORD GAMES from the English Literary Society &lt;/figcaption>
 &lt;/div>
 
 
&lt;/figure>&lt;/p>
&lt;p>The PDF linked above has some fun examples of the concept, like the one quoted below:&lt;/p>
&lt;div class="quote-block">
 &lt;blockquote class="quote-content">&lt;ol>
&lt;li>cerebrum puzzled Nabir (5)&lt;/li>
&lt;li>fan in man I acknowledge (6)&lt;/li>
&lt;/ol>
&lt;/blockquote>
&lt;/div>&lt;div class="quote-source-link">
 &lt;a href="https://elsiitk.wordpress.com/wp-content/uploads/2015/03/galaxy-word-games-av-round.pdf" target="_blank" rel="noopener">GALAXY WORD GAMES (AV round)&lt;/a>
&lt;/div>
&lt;p>The clues state that the first base word is a five-letter word related to &lt;em>&amp;ldquo;cerebrum&amp;rdquo;&lt;/em>, and that it is an anagram (hinted at by &lt;em>&amp;ldquo;puzzled&amp;rdquo;&lt;/em>) of the word &lt;em>&amp;ldquo;Nabir&amp;rdquo;&lt;/em>, and the second word is a six-letter word related to &lt;em>&amp;ldquo;fan&amp;rdquo;&lt;/em> that we can find &lt;em>&amp;ldquo;in&amp;rdquo;&lt;/em> &lt;em>&amp;quot;&lt;strong>man I ac&lt;/strong>knowledge&amp;quot;&lt;/em>. Combining &lt;em>&amp;ldquo;brain&amp;rdquo;&lt;/em> and &lt;em>&amp;ldquo;maniac&amp;rdquo;&lt;/em> gives us the portmanteau &lt;em>&amp;ldquo;brainiac&amp;rdquo;&lt;/em>.&lt;/p>
&lt;hr>
&lt;h2 id="walkthrough">Walkthrough&lt;/h2>
&lt;p>In &lt;a href="#game-crypt">the interactive example&lt;/a> above, we are looking for two related portmanteaux of length 9 and 13, and we are given four cryptic clues for their base words:&lt;/p>
&lt;blockquote>
&lt;ol>
&lt;li>Fast brake ruined morning meal. (9)&lt;/li>
&lt;li>Dairy product makes one grin! (6)&lt;/li>
&lt;li>Noon activity sounds like lunging. (8)&lt;/li>
&lt;li>Sandwiches for German citizens. (10)&lt;/li>
&lt;/ol>&lt;/blockquote>
&lt;ul>
&lt;li>Solving these cryptic clues gives us:&lt;/li>
&lt;/ul>
&lt;blockquote>
&lt;ol>
&lt;li>&lt;strong>Breakfast&lt;/strong> (morning meal that is an anagram of &amp;ldquo;fast brake&amp;rdquo;)&lt;/li>
&lt;li>&lt;strong>Cheese&lt;/strong> (exclamation when smiling for picture, also dairy product)&lt;/li>
&lt;li>&lt;strong>Lunching&lt;/strong> (noon activity that is a homophone of lunging)&lt;/li>
&lt;li>&lt;strong>Hamburgers&lt;/strong> (double definition: citizens of Hamburg, Germany + meat sandwiches)&lt;/li>
&lt;/ol>&lt;/blockquote>
&lt;ul>
&lt;li>Connecting the right base words gives us the two portmanteaux:&lt;/li>
&lt;/ul>
&lt;blockquote>
&lt;ul>
&lt;li>Breakfast + Lunching → &lt;strong>Brunching&lt;/strong> (9)&lt;/li>
&lt;li>Cheese + Hamburgers → &lt;strong>Cheeseburgers&lt;/strong> (13)&lt;/li>
&lt;/ul>&lt;/blockquote>
&lt;p>Part of the fun is that you might not need to find all four base words from scratch; maybe one from each pair combined with the other clue and figuring out the common theme could be enough to connect the dots.&lt;/p>
&lt;hr>
&lt;p>I&amp;rsquo;ll certainly be trying to come up with more cryptmanteaux in the future and posting them on my website; &lt;a href="http://patrick.vanderspie.gl/">send me a message&lt;/a> if you come up with some yourself!&lt;/p></description></item><item><title>"Using AI to Detect the Unusual"</title><link>http://patrick.vanderspie.gl/posts/fari-2024/</link><pubDate>Mon, 25 Nov 2024 00:00:00 +0000</pubDate><guid>http://patrick.vanderspie.gl/posts/fari-2024/</guid><description>&lt;blockquote>
&lt;p>This is an abbreviated transcript of my &lt;a href="https://www.fari.brussels/news-and-media-article/happy-hours">FARI &amp;ldquo;Happy Hour&amp;rdquo;&lt;/a> talk titled &amp;ldquo;Using AI to Detect the Unusual&amp;rdquo;, held at BeCentral on the 25th of November, 2024. The slides I used during my talk can be found &lt;a href="http://patrick.vanderspie.gl/posts/fari-2024/slides.pdf">here&lt;/a>. This transcript was created using &lt;a href="https://openai.com/index/whisper/">OpenAI&amp;rsquo;s Whisper model&lt;/a> and edited using GPT-4o.&lt;/p>&lt;/blockquote>
&lt;p>










&lt;figure class="">

 &lt;div>
 &lt;img loading="lazy" alt="" src=" /posts/fari-2024/presentation-photo.jpg">
 &lt;/div>

 
 
 
&lt;/figure>&lt;/p>
&lt;hr>
&lt;h1 id="introduction">Introduction&lt;/h1>
&lt;p>Tonight, I&amp;rsquo;m going to talk about how we can use artificial intelligence to detect the unusual. If I knew nothing about artificial intelligence, I would have three main questions regarding this title:&lt;/p>
&lt;ol>
&lt;li>&lt;strong>What is artificial intelligence and how do we use it in general?&lt;/strong>&lt;/li>
&lt;li>&lt;strong>What is the unusual, and how would we traditionally detect this without using artificial intelligence?&lt;/strong>&lt;/li>
&lt;li>&lt;strong>How would we then use AI to detect the unusual?&lt;/strong>&lt;/li>
&lt;/ol>
&lt;h1 id="what-is-artificial-intelligence">What is Artificial Intelligence?&lt;/h1>
&lt;p>Artificial intelligence (AI) is a field of research. It is a subset of mathematics and computer science, and it overlaps with philosophy and linguistics, depending on the specific subset of AI being studied. This interdisciplinary nature makes it a fascinating field because you often collaborate with a diverse group of people across various domains.&lt;/p>
&lt;p>AI has been researched since the 1950s, and although we&amp;rsquo;ve heard a lot about it in media recently, the increase in computing power over the last few decades has enabled us to perform remarkable tasks based on the research conducted during this time.&lt;/p>
&lt;h2 id="examples-of-ai-applications">Examples of AI Applications&lt;/h2>
&lt;p>I have some examples of how we use artificial intelligence:&lt;/p>
&lt;ul>
&lt;li>
&lt;p>&lt;strong>Search:&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>In 1996, an AI algorithm named Deep Blue managed to beat the world chess champion, Garry Kasparov.&lt;/li>
&lt;li>More recently, AlphaGo competed against the second-best player in Go, a game with greater complexity than chess. For those who haven&amp;rsquo;t seen it, I recommend the excellent documentary about it.&lt;/li>
&lt;li>Day-to-day applications like Google Maps and Waze help you navigate from point A to point B in the most efficient way possible. You might not think of this as AI, but it results from extensive AI research.&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>Classification:&lt;/strong> If you have a large dataset containing labeled images of cats and dogs, for instance, you can use AI to assign the correct label to new images as they come in. Google offers &amp;ldquo;teachable machines&amp;rdquo; that allows users to train classification models using their webcam, requiring no programming experience.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>Generative AI:&lt;/strong> Tools like ChatGPT for text generation and DALL-E for image generation have gained popularity recently. While impressive, it’s crucial to recognize that generative AI is not the only area of AI research. There are also significant studies in classification, search, and reinforcement learning, for example.&lt;/p>
&lt;/li>
&lt;/ul>
&lt;h2 id="what-ai-isnt">What AI Isn’t&lt;/h2>
&lt;p>Now that we&amp;rsquo;ve discussed what artificial intelligence is, it’s also important to clarify what AI isn’t.&lt;/p>
&lt;p>First, AI isn’t about creating conscious machines, a notion often propagated in science fiction. While artificial general intelligence—AI with capabilities akin to human intelligence—is a goal for some researchers, most focus on narrow AI tasks. When companies vaguely claim they &amp;ldquo;use an AI&amp;rdquo; for significant goals, it raises red flags.&lt;/p>
&lt;p>For example, it would be like a toothpaste manufacturer saying they used &amp;ldquo;a chemistry&amp;rdquo; to create better toothpaste—it doesn’t quite make sense. It’s important not to fall for the hype surrounding AI in the media. AI has nothing to do with Terminators or humanoid robots; it’s grounded in real-world applications.&lt;/p>
&lt;h3 id="examples-of-ai-misuse">Examples of AI Misuse&lt;/h3>
&lt;p>I&amp;rsquo;ve put together some examples of how AI can be misused:&lt;/p>
&lt;ul>
&lt;li>
&lt;p>&lt;strong>The &lt;em>Smartschool&lt;/em> Case:&lt;/strong> &lt;em>Smartschool&lt;/em> is a platform where parents can communicate with their child&amp;rsquo;s teachers and access their test results. Recently, the company behind the platform announced plans to use AI to predict student dropout risks. This admirable goal (&lt;em>trying to reduce student dropout&lt;/em>) was poorly executed, essentially reducing these students to test results and vaguely implementing AI in their product without public dialogue, leading to significant backlash.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>Social Media Companies:&lt;/strong> Social media platforms are designed to maximize user engagement for profit through targeted advertisement. However, this focus can pose risks to younger users: vulnerable youth are presented with content that romanticizes self-harm, for example. Internal Facebook reports revealed that toxic content on Instagram negatively affects teenagers, but the company plays this down in public.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>Dutch Child Care Benefit Scandal:&lt;/strong> Thousands of families in the Netherlands were wrongly accused of fraud when applying for child care benefits, based on algorithmic predictions. This led to severe financial hardships for these families, illustrating how false positives can have disastrous impacts.&lt;/p>
&lt;/li>
&lt;/ul>
&lt;h1 id="detecting-the-unusual">Detecting the Unusual&lt;/h1>
&lt;p>Let’s now discuss the unusual and how we detect it.&lt;/p>
&lt;h2 id="gathering-data">Gathering Data&lt;/h2>
&lt;p>Everything around us—every process, system, or activity—produces data, which we can collect through observation and measurement. Here are some examples:&lt;/p>
&lt;ol>
&lt;li>&lt;strong>Health Monitoring:&lt;/strong> Tracking heart rate and blood pressure.&lt;/li>
&lt;li>&lt;strong>Financial Activity:&lt;/strong> Every transaction you make with a credit card—transaction date, time, location, and amount.&lt;/li>
&lt;li>&lt;strong>Social Network Activity:&lt;/strong> Actions like sending messages, making friend requests, and liking posts.&lt;/li>
&lt;/ol>
&lt;h2 id="understanding-anomalies">Understanding Anomalies&lt;/h2>
&lt;p>Anomalies are deviations from what we consider to be normal behavior, and these deviations leave traces in our observations. We are interested in understanding the underlying causes of these deviations. For example:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Health Monitoring:&lt;/strong> An ECG can show if you have arrhythmia, indicating an underlying health issue.&lt;/li>
&lt;li>&lt;strong>Financial Activity:&lt;/strong> If you generally conduct transactions in Belgium, an unexpected transaction from Nigeria would likely be labeled as anomalous.&lt;/li>
&lt;li>&lt;strong>Social Network Activity:&lt;/strong> You can have spam messages or friend requests, also classifiable as anomalies.&lt;/li>
&lt;/ul>
&lt;h2 id="anomalies-vs-outliers">Anomalies vs. Outliers&lt;/h2>
&lt;p>It&amp;rsquo;s essential to distinguish between anomalies and outliers. Some sources claim they are the same, but this is misleading.&lt;/p>
&lt;p>Traditional examples of anomalies often show high peaks and obvious outliers in time series data. However, context matters significantly in determining whether a data point is an anomaly. For instance, a large peak in time series data could be normal if it occurs at regular intervals, suggesting that context is critical.&lt;/p>
&lt;h1 id="using-ai-to-detect-the-unusual">Using AI to Detect the Unusual&lt;/h1>
&lt;p>Now that we&amp;rsquo;ve reached the point of understanding what anomalies are, we can discuss using AI for detecting these unusual patterns.&lt;/p>
&lt;h2 id="anomaly-detection">Anomaly Detection&lt;/h2>
&lt;p>Anomaly detection refers to finding patterns in data that do not conform to expected behavior. The goal of identifying anomalies is to alert domain experts that something deviates from the norm and to provide actionable information to address the situation.&lt;/p>
&lt;p>For instance, in the medical field, detecting arrhythmia would alert healthcare professionals to investigate further.&lt;/p>
&lt;h2 id="the-role-of-context">The Role of Context&lt;/h2>
&lt;p>Detecting anomalies is highly context-dependent. While classification involves labeling items, anomaly detection focuses on recognizing deviations from expected patterns—often rare occurrences.&lt;/p>
&lt;p>Traditional approaches to identifying anomalies are rule-based, which can be cumbersome and require constant updating. Relying solely on these rules may lead to missed anomalies or false triggers.&lt;/p>
&lt;h2 id="a-rule-based-approach">A Rule-Based Approach&lt;/h2>
&lt;p>If we would have a manual rule-based configuration for anomaly detection:&lt;/p>
&lt;ul>
&lt;li>Some anomalies would be missed if predefined rules do not trigger: no rule for certain anomalies means that these anomalies go undetected&lt;/li>
&lt;li>Rule-based trigger check on a “per event” basis, no “bigger picture”: context matters, and anomalies can involve multiple events&lt;/li>
&lt;li>Expert time would be wasted on hard-coded rule design instead of tackling the root cause of anomalies&lt;/li>
&lt;/ul>
&lt;h2 id="challenges-in-anomaly-detection">Challenges in Anomaly Detection&lt;/h2>
&lt;p>In my research, I previously focused on developing a pattern-based series framework. Instead of relying on hard-coded rules, my framework allows us to apply various AI algorithms to learn a model of normal behavior and test new data against it. However, challenges exist, including:&lt;/p>
&lt;ol>
&lt;li>&lt;strong>Complexity of Normal Behavior:&lt;/strong> Context and variation can make defining &amp;rsquo;normal&amp;rsquo; difficult.&lt;/li>
&lt;li>&lt;strong>Domain-Specific Definitions of Anomalies:&lt;/strong> Definitions of anomalies can vary significantly depending on the domain (e.g., cybersecurity vs. healthcare).&lt;/li>
&lt;li>&lt;strong>Ambiguity of Artifacts:&lt;/strong> Different events might present similarly in data, making it challenging to differentiate between normal and anomalous behavior.&lt;/li>
&lt;li>&lt;strong>Impact of Mistakes:&lt;/strong> Algorithms can produce both false positives and false negatives, leading to significant consequences.&lt;/li>
&lt;/ol>
&lt;h1 id="conclusion">Conclusion&lt;/h1>
&lt;p>In summary:&lt;/p>
&lt;ol>
&lt;li>&lt;strong>AI is a research field:&lt;/strong> It focuses on narrow tasks and is not about conscious machines.&lt;/li>
&lt;li>&lt;strong>Anomaly detection aims to identify unusual patterns in various domains.&lt;/strong> These patterns are often context-dependent deviations from what is deemed normal.&lt;/li>
&lt;li>&lt;strong>Algorithms can make mistakes,&lt;/strong> resulting in false positives and negatives that can have disastrous consequences.&lt;/li>
&lt;/ol>
&lt;p>Thank you all for your attention. Slides and transcripts are available on my website. If you have any questions, please feel free to ask. Thank you!&lt;/p></description></item><item><title>"Hell's Hex Station" Write-Up</title><link>http://patrick.vanderspie.gl/posts/igctf-2024/</link><pubDate>Fri, 22 Nov 2024 00:00:00 +0000</pubDate><guid>http://patrick.vanderspie.gl/posts/igctf-2024/</guid><description>&lt;blockquote>
&lt;p>In this write-up, I go over the step-by-step solution of &lt;strong>&lt;em>&amp;ldquo;Hell&amp;rsquo;s Hex Station&amp;rdquo;&lt;/em>&lt;/strong>, a challenge I created for the 2024 edition of &lt;a href="https://ctf.infogroep.be/">Infogroep&amp;rsquo;s Capture the Flag&lt;/a>.&lt;/p>&lt;/blockquote>
&lt;h2 id="description">Description&lt;/h2>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-text" data-lang="text">&lt;span style="display:flex;">&lt;span>Mein Fraulein,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>I haven&amp;#39;t heard from you in a while. Won&amp;#39;t you write me?
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>

&lt;audio class="player" controls preload="none">

&lt;source src="http://patrick.vanderspie.gl/posts/igctf-2024/recording.wav" type="audio/mp3">

&lt;/audio>

&lt;/p>
&lt;h2 id="solution">Solution&lt;/h2>
&lt;p>The title of the challenge is a reference to &lt;a href="https://en.wikipedia.org/wiki/Rudolf_Hell">Rudolf Hell&lt;/a> (inventor of the &lt;em>&amp;ldquo;Hellschreiber&amp;rdquo;&lt;/em>), the &lt;a href="https://en.wikipedia.org/wiki/Hexadecimal">hexadecimal system&lt;/a>, and &lt;a href="https://en.wikipedia.org/wiki/Numbers_station">numbers stations&lt;/a>. The description is a reference to &lt;a href="https://youtu.be/OOxW4VNuHf0">a 2006 talk at DEF CON&lt;/a> and also references the recurring German theme of this challenge&lt;label for="sidenote-1" class="sidenote-number">&lt;/label>&lt;input type="checkbox" id="sidenote-1" class="sidenote-toggle">&lt;span class="sidenote">The &amp;ldquo;Mein Fraulein&amp;rdquo; description, Rudolf Hell being a German engineer, and the correct flag of this challenge containing a &lt;code>ß&lt;/code>.&lt;/span>.&lt;/p>
&lt;p>The provided &lt;code>.wav&lt;/code> file starts with the tune of &lt;a href="https://en.wikipedia.org/wiki/Lincolnshire_Poacher_(numbers_station)">the &amp;ldquo;Lincolnshire Poacher&amp;rdquo; numbers station&lt;/a> followed by a voice repeating &lt;code>&amp;quot;CP437&amp;quot;&lt;/code> and ends with beeping. Using &lt;code>exiftool&lt;/code> to look at the metadata of the audio file, we get the following output:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-text" data-lang="text">&lt;span style="display:flex;">&lt;span>$ exiftool recording.wav
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>⋯
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>Comment : FELDHELL freq=14071.500
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>Software : fldigi-4.1.06 (libsndfile-1.0.28)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>⋯
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>The &lt;code>Comment&lt;/code> and &lt;code>Software&lt;/code> values bring us to the solution of the challenge: the beeping that makes up the bulk of the &lt;code>.wav&lt;/code> file is &lt;a href="https://en.wikipedia.org/wiki/Hellschreiber">&amp;ldquo;Hellschreiber&amp;rdquo;&lt;/a> in Feld Hell mode, a technique developed in 1927 by Rudolf Hell and currently in use by Ham radio hobbyists. Hellschreiber modes are based on character scanning&lt;label for="sidenote-2" class="sidenote-number">&lt;/label>&lt;input type="checkbox" id="sidenote-2" class="sidenote-toggle">&lt;span class="sidenote">&lt;a href="http://www.w1hkj.com/modes/feld.htm">Feld Mode&lt;/a>&lt;/span>: we transmit characters as patterns of beeps, resembling a dot-matrix printer.&lt;/p>
&lt;p>As &lt;a href="https://en.wikipedia.org/wiki/Fldigi">&lt;code>fldigi&lt;/code>&lt;/a> contains Feld Hell as one of their &lt;a href="https://en.wikipedia.org/wiki/Fldigi#Supported_digital_modes">supported digital modes&lt;/a>, we can use it to decode the beeping in our file. After installing and opening &lt;code>fldigi&lt;/code>, we select the correct operational mode via &lt;code>&amp;quot;Op Mode → Hell → Feld Hell&amp;quot;&lt;/code> and load our &lt;code>.wav&lt;/code> file via &lt;code>&amp;quot;File → Audio → Playback&amp;quot;&lt;/code>.&lt;/p>
&lt;p>In the waterfall view on the bottom of the &lt;code>fldigi&lt;/code> window, we select the area that lights up when the beeping begins. Below, you can see a screenshot of the &lt;code>fldigi&lt;/code> program, around a minute after loading the &lt;code>.wav&lt;/code> file. A list of numbers in hexadecimal representation appear on screen:&lt;/p>
&lt;p>










&lt;figure class="">

 &lt;div>
 &lt;img loading="lazy" alt="A screenshot of the fldigi program, a minute after loading the provided .wav file. A list of numbers appeared on the screen." src=" /posts/igctf-2024/fldigi.png">
 &lt;/div>

 
 
 &lt;div class="caption-container">
 &lt;figcaption> A screenshot of the &lt;code>fldigi&lt;/code> program, a minute after loading the provided &lt;code>.wav&lt;/code> file. A list of numbers appeared on the screen. &lt;/figcaption>
 &lt;/div>
 
 
&lt;/figure>&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-text" data-lang="text">&lt;span style="display:flex;">&lt;span>49 47 43 54 46 7b 48 33 4c 4c e1 43 48 52 33 31 42 33 52 21 7d
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>In general, we could use a range of different tools to transform this list of hexadecimal numbers to a string of characters. The simplest way of doing this would be through &lt;a href="https://gchq.github.io/CyberChef/">CyberChef&lt;/a>. After giving our list of numbers as input, we can pick the &lt;code>&amp;quot;From Hex&amp;quot;&lt;/code> recipe (or choose &amp;ldquo;Magic&amp;rdquo; and provide &lt;code>IGCTF&lt;/code> as the crib) to obtain the following output:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-text" data-lang="text">&lt;span style="display:flex;">&lt;span>IGCTF{H3LLáCHR31B3R!}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>This is not the correct flag, however! The provided &lt;code>.wav&lt;/code> file specifically mentioned &lt;a href="https://en.wikipedia.org/wiki/Code_page_437">&lt;code>&amp;quot;CP437&amp;quot;&lt;/code>&lt;/a>, which is a specific character encoding. Using Python (or any other tool that allows us to use &lt;code>CP437&lt;/code>), we obtain &lt;strong>&lt;code>IGCTF{H3LLßCHR31B3R!}&lt;/code>&lt;/strong>, which is the correct flag:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>&amp;gt;&amp;gt;&amp;gt; &lt;span style="font-weight:bold;font-style:italic">bytes&lt;/span>.fromhex(hex_string).decode(&lt;span style="color:#666;font-style:italic">&amp;#34;cp437&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#666;font-style:italic">&amp;#39;IGCTF{H3LLßCHR31B3R!}&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div></description></item><item><title>Mindful use of LLMs</title><link>http://patrick.vanderspie.gl/posts/using-llms/</link><pubDate>Tue, 24 Sep 2024 00:00:00 +0000</pubDate><guid>http://patrick.vanderspie.gl/posts/using-llms/</guid><description>&lt;p>I believe large language models can bring value in use cases where they &lt;em>assist&lt;/em> with tasks like summarising, translating, and reformulating text. I have previously used these models for some quick translation work, and I also started using the OpenAI API to help me summarise the contents of my clipboard through &lt;a href="http://patrick.vanderspie.gl/notes/2024-09-09/">an Espanso shortcut&lt;/a>.&lt;/p>
&lt;p>Outside of summarising and translating texts, there are advantages for software developers as well. &lt;em>GitHub Copilot&lt;/em> drastically reduces the time needed to figure out popular APIs and successfully assists in writing proper documentation, for example. These tools could increase both the speed of development, codebase quality, and consistency as long as developers understand, reason about, and ultimately agree with every generated line of code and comment before committing their changes.&lt;/p>
&lt;p>I am also surprised about the quick adoption by a non-technical audience. LLMs would have seemed like magic to me only a couple of years ago, even though they are now commonplace—with friends and family members asking me to teach them &amp;ldquo;how to use ChatGPT&amp;rdquo; or enthusiastically telling me they already use it regularly. During a lightning talk on the risks of these models, for example, I asked the audience to raise their hands if they had already used ChatGPT in a professional setting. Almost everyone in the audience had done so recently.&lt;/p>
&lt;h2 id="flip-side-of-the-coin">Flip side of the coin&lt;/h2>
&lt;p>Assistance with writing and coding aside, I believe there is also a serious flip side of the coin. &lt;strong>Overreliance and excessive use of this technology could degrade writing proficiency and programming skills&lt;/strong>. As &lt;a href="https://www.vrt.be/vrtnws/nl/2024/05/03/_schrijfkwaliteit-meten-kan-niet-meer-hoe-ai-onze-universiteite/">Belgian universities allow LLM usage by their students&lt;/a>, we urgently need to reform how we evaluate the quality of a student&amp;rsquo;s work now that we can&amp;rsquo;t rely on traditional assignments. I&amp;rsquo;ve learned a lot by struggling through my university classes&amp;rsquo; programming projects, and I&amp;rsquo;m afraid I would have learned a lot less if I had easy access to software that completes my work for me.&lt;/p>
&lt;p>Next, it is easy to &lt;strong>misuse these models&lt;/strong> (&lt;em>not always out of malice, but also out of misunderstanding how the technology works&lt;/em>) — applying them to use cases that range from &lt;a href="https://mastodon.social/@jbaert/113155479457961565">wasteful but innocent&lt;/a> to downright unethical and harmful. Consequences of this misuse include the &lt;a href="https://www.nytimes.com/2024/06/11/style/ai-search-slop.html">pollution of the internet with &lt;em>slop&lt;/em>&lt;/a> and spreading misinformation (as some users do not realise that tools like ChatGPT are &lt;em>not&lt;/em> a replacement for search engines and should not be trusted to return reliable information). Sadly, because of these consequences, the whole field of AI is getting backlash—even though the field itself is so much more than just generative AI.&lt;/p>
&lt;p>As an example of unethical and harmful misuse, we have &lt;a href="https://zetane.com/ZetaForge-video-demo-voice.mp4">ZetaForge&lt;/a>, a no-code tool to &amp;ldquo;rapidly build and deploy advanced AI pipelines&amp;rdquo;. The video linked above demonstrates how they use their tool to generate a textual description of war scenario images, on which they use a large language model to suggest a course of action to military personnel. In their own words, their demonstration pipeline &amp;ldquo;can be deployed as is, without any modification.&amp;rdquo; I&amp;rsquo;m not looking forward to a future where we rely on statistical models outputting &lt;span class="llm-generated">As a large language model, I recommend opening fire. 🔫🤠&lt;/span> as a military strategy to ensure civilian safety.&lt;/p>
&lt;p>Finally, &lt;strong>the resources necessary to keep these models operational:&lt;/strong> a summary of a scientific paper — which I can perfectly write myself, albeit slower — is not worth the computing time, energy, and the &lt;a href="https://www.washingtonpost.com/technology/2024/09/18/energy-ai-use-electricity-water-data-centers/">bottle of water&lt;/a> necessary to generate that summary. Combined with the fact that these models are trained on data without permission from artists and writers made me question my own use of these models in their current form.&lt;/p>
&lt;h2 id="personal-ethics">Personal ethics&lt;/h2>
&lt;p>&lt;strong>I have decided to stop using LLMs as a writing tool&lt;/strong>, as — personally — their downsides outweigh the benefits. Summarising text was my initial use case for LLMs, but I summarise text to internalise and learn from the material I am working with. In my case, summarising should take time. LLMs promise speed, but speed isn&amp;rsquo;t always desired.&lt;/p>
&lt;p>I also previously used an LLM to help &lt;a href="http://patrick.vanderspie.gl/posts/automating-boggle/">write a blog post&lt;/a> for a small half-day project. I still believe I used the tool responsibly, in this case, as I only used it to rephrase or expand certain lines of text, using the output as an initial draft and not copying generated text verbatim. I also clearly indicated which text was written with assistance, even though most was still my own. However, I plan to never use LLMs for writing blog posts again: I strongly believe that &lt;strong>if I don&amp;rsquo;t take the time to write a text, no one should waste their time reading it either&lt;/strong>.&lt;/p>
&lt;p>I plan to put my money where my mouth is by taking the amount that I usually pay for API costs (which is only pennies, so I&amp;rsquo;m throwing in the price of a subscription to &lt;em>ChatGPT Plus&lt;/em>) and donating it to a charity standing up for nature and biodiversity in my region. I know I am not making a dent by donating a small amount of money to a charity and mindful use of LLMs, but I am doing my best to align my actions to my values.&lt;/p></description></item><item><title>Notes from August</title><link>http://patrick.vanderspie.gl/posts/2024-08/</link><pubDate>Thu, 05 Sep 2024 00:00:00 +0000</pubDate><guid>http://patrick.vanderspie.gl/posts/2024-08/</guid><description>&lt;p>In August, after learning about the &lt;a href="https://indieweb.org/">IndieWeb&lt;/a>, I updated my website and turned it into this blog. It has been years since I did something web-related, and I always enjoyed tinkering on the web, so I am glad I took the time to try and rebuild my website. This month was my first time using Hugo, customising an existing theme to fit my needs. I also:&lt;/p>
&lt;ul>
&lt;li>&lt;a href="http://patrick.vanderspie.gl/notes/2024-08-07/">Automated the creation of daily notes on this blog using GitHub Actions&lt;/a>&lt;/li>
&lt;li>&lt;a href="http://patrick.vanderspie.gl/notes/2024-08-24/">Created category filters for this blog using the &lt;code>:has&lt;/code> selector in CSS&lt;/a>&lt;/li>
&lt;li>Pulled up-to-date statistics, like &lt;a href="http://patrick.vanderspie.gl/notes/2024-08-09/">distance ran in a year&lt;/a> — &lt;em>&lt;span class="tooltip">418.2 km&lt;span class="tooltiptext">Data pulled from intervals.icu&lt;/span>&lt;/span>&lt;/em>, and &lt;a href="http://patrick.vanderspie.gl/notes/2024-08-17/">my current Tetra League ranking&lt;/a> — &lt;em>&lt;a href="https://ch.tetr.io/u/geuze/league">&lt;span>11126.88 TR (&lt;img class="tr_rank" src="https://tetr.io/res/league-ranks/a.png"/>)&lt;/span>&lt;/a>&lt;/em>, using various APIs&lt;/li>
&lt;/ul>
&lt;p>In terms of other small projects,&lt;/p>
&lt;ul>
&lt;li>I &lt;a href="http://patrick.vanderspie.gl/notes/2024-08-10/">found new breakfast spots using Overture Places&lt;/a> and &lt;code>duckdb&lt;/code>,&lt;/li>
&lt;li>Created a &lt;a href="http://patrick.vanderspie.gl/notes/2024-08-14/">Pomodoro timer script&lt;/a> that keeps track of my work in Obsidian,&lt;/li>
&lt;li>Started &lt;a href="http://patrick.vanderspie.gl/notes/2024-08-21/">learning the Rust programming language&lt;/a>,&lt;/li>
&lt;li>Looked into &lt;a href="http://patrick.vanderspie.gl/notes/2024-08-20/">visualising Belgian roads&lt;/a>,&lt;/li>
&lt;li>Learned more about &lt;a href="http://patrick.vanderspie.gl/notes/2024-08-23/">&lt;code>uv&lt;/code> for Python&lt;/a>&lt;/li>
&lt;/ul>
&lt;h3 id="writing-daily-notes">Writing daily notes&lt;/h3>
&lt;p>Initially, &lt;a href="http://patrick.vanderspie.gl/posts/daily-notes/">I aimed to write a daily note on this blog&lt;/a>, sharing small personal projects or interesting things I encountered that day. After twenty consecutive notes, however, I started putting quantity before quality on days I did not feel like writing. If I did not enjoy writing a note, I am sure it isn&amp;rsquo;t pleasant for others to read that note either. I am still looking for how and when to best write and share short notes for &lt;a href="https://www.jvt.me/posts/2017/06/25/blogumentation/">&amp;ldquo;blogumentation&amp;rdquo;&lt;/a>.&lt;/p>
&lt;h3 id="blogs-i-enjoyed-reading-this-august">Blogs I enjoyed reading this August&lt;/h3>
&lt;p>&lt;a href="https://fabiensanglard.net/ilike/index.html">Fabien Sanglard&amp;rsquo;s &lt;code>0X10 RULES&lt;/code>&lt;/a> are great rules to follow when creating (small) websites. On that note, I also appreciated &lt;a href="https://wonger.dev/posts/monospace-dump">Justin Wong&amp;rsquo;s collection of &amp;ldquo;monospace webpages&amp;rdquo;&lt;/a>, &lt;a href="https://brutalist.report/?limit=5">The Brutalist Report&lt;/a>, and &lt;a href="https://computer.rip/">computer.rip&lt;/a>.&lt;/p>
&lt;p>Other blogs that I added to my RSS feed this month include &lt;a href="https://simonwillison.net/">Simon Willison’s Weblog&lt;/a>, &lt;a href="https://sebastiandedeyne.com/">Sebastian De Deyne&lt;/a>, &lt;a href="https://matthodges.com/posts.html">Matt Hodges&lt;/a>, &lt;a href="https://robinrendle.com/">Robin Rendle&lt;/a>, &lt;a href="https://bradbarrish.com/">Brad Barrish&lt;/a>, &lt;a href="https://anhvn.com/blog/">anhvn.com&lt;/a>, and &lt;a href="https://jamesg.blog/">James G.&lt;/a>&lt;/p></description></item><item><title>Sharing daily notes</title><link>http://patrick.vanderspie.gl/posts/daily-notes/</link><pubDate>Wed, 07 Aug 2024 00:00:00 +0000</pubDate><guid>http://patrick.vanderspie.gl/posts/daily-notes/</guid><description>&lt;p>For my work, I picked up the habit of transferring my daily notes and insights from my physical notebooks to an &lt;a href="https://obsidian.md/">Obsidian&lt;/a> vault. Thanks to using Obsidian and &lt;code>git&lt;/code>, I have a locally-stored, version-controlled, and easily searchable overview of my work that I can reference when preparing reports and presentations.&lt;/p>
&lt;p>Equivalently, I miss a digital space where I can transfer personal insights, store interesting links and quotes, and write down notes unrelated to my work. Topics of these notes could include everything from programming, research, and literature, to physical exercise and ceramics.&lt;/p>
&lt;h3 id="why-make-these-notes-public">Why make these notes public?&lt;/h3>
&lt;p>I could start a new Obsidian vault for these notes, but I think they should be someplace public as I want to &lt;a href="https://notes.andymatuschak.org/Work_with_the_garage_door_up">“work with my garage door up.”&lt;/a> I also want to keep up a daily writing streak: if I would just create another private Obsidian vault, it&amp;rsquo;ll be too easy to convince myself to skip writing to do other things instead. Public challenges like &lt;a href="https://100daystooffload.com/">&lt;code>#100DaysToOffload&lt;/code>&lt;/a> seem appealing to participate in, as well.&lt;/p>
&lt;h3 id="why-share-these-on-a-personal-website">Why share these on a personal website?&lt;/h3>
&lt;p>We can write whatever we want on personal websites, without being vulnerable to tech companies and their social media algorithms + keeping ownership of content. I am currently hosting this website through &lt;a href="https://pages.github.com/">GitHub Pages&lt;/a>, but I can pack up my content and move it elsewhere whenever I want. Read more about this in this &lt;a href="https://matthiasott.com/articles/into-the-personal-website-verse">great article by Matthias Ott&lt;/a>:&lt;/p>
&lt;div class="quote-block">
 &lt;blockquote class="quote-content">If you decide to publish your work on a platform like Medium, you&amp;rsquo;re giving away control over it. What if Medium suddenly decided to extend the already existing paywall to all articles? There&amp;rsquo;s not much you could do about it. Simply because you don&amp;rsquo;t own your content anymore.&lt;/blockquote>
&lt;/div>&lt;div class="quote-source-link">
 &lt;a href="https://matthiasott.com/articles/into-the-personal-website-verse" target="_blank" rel="noopener">Matthias Ott&lt;/a>
&lt;/div>
&lt;div class="quote-block">
 &lt;blockquote class="quote-content">But maybe the most compelling reason why a personal website (⋯) is incredibly valuable is this: community. Since the days of &amp;ldquo;guest books&amp;rdquo;, personal websites have been a place to receive feedback and discuss ideas and concepts with others. (⋯) Now imagine, for a moment, an environment where a decentralized fabric of connected personal sites allows everyone to publish their own content but also enables each individual to engage in an open discussion – answering, challenging, and acknowledging the ideas of others through this universe of personal sites.&lt;/blockquote>
&lt;/div>&lt;div class="quote-source-link">
 &lt;a href="https://matthiasott.com/articles/into-the-personal-website-verse" target="_blank" rel="noopener">Matthias Ott&lt;/a>
&lt;/div>
&lt;p>To allow for this engagement, I&amp;rsquo;m planning to add &lt;a href="https://indieweb.org/Webmention">Webmentions&lt;/a> to this website soon.&lt;/p>
&lt;h3 id="why-share-unpolished-work">Why share unpolished work?&lt;/h3>
&lt;p>By trying to write and publish a quick note every day, I&amp;rsquo;ll hopefully improving my writing. Not all of these notes will be polished, which is why I plan to keep them separate from &lt;a href="http://patrick.vanderspie.gl/posts">my regular posts&lt;/a>. It is okay to create unpolished things — a quote from &lt;a href="https://robinrendle.com/notes/the-story-is-a-codebase/">Robin Rendle&lt;/a>:&lt;/p>
&lt;div class="quote-block">
 &lt;blockquote class="quote-content">I feel like I&amp;rsquo;m free now to write absolute junk. And this is vital for any creative thing! You have to feel like your mistakes won&amp;rsquo;t be judged, that there&amp;rsquo;s no one watching you write shameful, embarrassing things. Because if you have all that pressure of trying to write the next great American novel then I reckon you&amp;rsquo;re destined to fail.&lt;/blockquote>
&lt;/div>&lt;div class="quote-source-link">
 &lt;a href="https://robinrendle.com/notes/the-story-is-a-codebase/" target="_blank" rel="noopener">Robin Rendle&lt;/a>
&lt;/div>
&lt;p>Hopefully, these notes give me inspiration to write some longer posts, where I can combine ideas and flesh out concepts of multiple related notes.&lt;/p>
&lt;hr>
&lt;h3 id="writing-streak">Writing streak&lt;/h3>
&lt;p>I currently have a writing streak of &lt;span class="tooltip">



0 days&lt;span class="tooltiptext">Updated on May 10, 2026&lt;/span>&lt;/span>, and my largest streak to date is &lt;span class="tooltip">




20 days&lt;span class="tooltiptext">Achieved on Aug 27, 2024&lt;/span>&lt;/span>. You can find these daily notes &lt;a href="http://patrick.vanderspie.gl/notes">here&lt;/a>. To find the note for a corresponding date, go to &lt;code>/notes/YYYY-MM-DD&lt;/code>. Each date since &lt;code>2024-08-07&lt;/code> should have an existing entry, even if I did not write anything that day.&lt;/p></description></item><item><title>Obsidian.md, Espanso, and llm</title><link>http://patrick.vanderspie.gl/posts/obsidian-espanso-llm/</link><pubDate>Tue, 23 Jul 2024 00:00:00 +0000</pubDate><guid>http://patrick.vanderspie.gl/posts/obsidian-espanso-llm/</guid><description>&lt;p>I switched from Notion to &lt;a href="https://obsidian.md/">Obsidian.md&lt;/a> to keep track of meeting notes, to store highlights from scientific papers (using a &lt;a href="https://github.com/mgmeyers/obsidian-zotero-integration">Zotero plugin&lt;/a>), and to maintain a knowledge base for my PhD research.&lt;/p>
&lt;h2 id="obsidian-and-llm">Obsidian and &lt;code>llm&lt;/code>&lt;/h2>
&lt;p>To summarise or rephrase parts of notes and paper highlights, I often use OpenAI models through &lt;a href="https://github.com/simonw/llm">&lt;code>llm&lt;/code>&lt;/a>, a CLI utility that provides an easy way to interact with the OpenAI API. Traditionally, my workflow consisted of piping Markdown files from my Obsidian vault to &lt;code>llm&lt;/code>, combining them with a system prompt that described the requested task.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>cat papers/paper.md | llm -m &lt;span style="color:#666;font-style:italic">&amp;#39;gpt-4o&amp;#39;&lt;/span> 
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>-s &lt;span style="color:#666;font-style:italic">&amp;#34;Summarise the methodology described in the provided paper notes&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>I then copied the model output from the terminal and pasted it in a custom callout that I specifically defined for LLM-generated content (as to keep it separate from my own writing), giving me a first rough draft that I can edit later.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-css" data-lang="css">&lt;span style="display:flex;">&lt;span>.&lt;span style="color:#666;font-weight:bold;font-style:italic">callout&lt;/span>[data-callout=&lt;span style="color:#666;font-style:italic">&amp;#34;llm&amp;#34;&lt;/span>] {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#666;font-weight:bold;font-style:italic">--callout-color&lt;/span>: 0, 100, 100;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#666;font-weight:bold;font-style:italic">--callout-icon&lt;/span>: bot;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>A main disadvantage, however, is that I kept switching between Obsidian and the terminal to copy and paste model output. Another disadvantage was that I had no control over which lines from the relevant file to send to OpenAI when piping the full contents of a file to &lt;code>llm&lt;/code>&lt;label for="sidenote-0" class="sidenote-number">&lt;/label>&lt;input type="checkbox" id="sidenote-0" class="sidenote-toggle">&lt;span class="sidenote">I am sure that there are plenty of ways to &lt;code>cat&lt;/code> specific lines only given a file, but I assume for this you need to provide specific line numbers, making the process a bit more complex.&lt;/span>.&lt;/p>
&lt;h2 id="obsidian-and-espanso">Obsidian and Espanso&lt;/h2>
&lt;p>Outside of tagging LLM output, I also use &lt;a href="https://help.obsidian.md/Editing+and+formatting/Callouts#Customize+callouts">custom Obsidian callouts&lt;/a> to highlight definitions, examples, and quotes from papers and textbooks. For this, I started using &lt;a href="https://espanso.org/">Espanso&lt;/a> — a text expander that automatically replaces specific keywords with predefined text. This helps me avoid repeatedly typing callout boilerplate:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-yaml" data-lang="yaml">&lt;span style="display:flex;">&lt;span>- trigger: &lt;span style="color:#666;font-style:italic">&amp;#34;:def&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> replace: &lt;span style="color:#666;font-style:italic">&amp;#34;&amp;gt; [!Definition] ⋯\n&amp;gt; ⋯&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>- trigger: &lt;span style="color:#666;font-style:italic">&amp;#34;:ex&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> replace: &lt;span style="color:#666;font-style:italic">&amp;#34;&amp;gt; [!Example]- ⋯ \n&amp;gt; ⋯&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Every time I type &lt;code>:def&lt;/code> — inside or outside of Obsidian — it replaces this keyword with the boilerplate that is needed to define a &lt;code>Definition&lt;/code> callout in Obsidian. Other custom triggers include arrows (←, →), dots (⋯, ⋮), and dashes (—) that I use in notes, and abbreviations for commonly used words (all preceded by the &lt;code>:&lt;/code> character, which I have reserved for Espanso keywords only).&lt;/p>
&lt;h3 id="regex-triggers-arguments-and-shell-commands">Regex Triggers, Arguments, and Shell Commands&lt;/h3>
&lt;p>&lt;a href="https://espanso.org/docs/matches/regex-triggers/">Regex triggers&lt;/a> are an advanced alternative to regular triggers in Espanso. Instead of the static triggers shown above, these dynamic triggers provide flexibility through regex syntax and the use of named groups to pass arguments to expansions. The following two examples are taken from the Espanso documentation:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-yaml" data-lang="yaml">&lt;span style="display:flex;">&lt;span> - regex: &lt;span style="color:#666;font-style:italic">&amp;#34;:greet\\((?P&amp;lt;person&amp;gt;.*)\\)&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> replace: &lt;span style="color:#666;font-style:italic">&amp;#34;Hi {{person}}!&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>We can also execute scripts and shell commands through Espanso:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-yaml" data-lang="yaml">&lt;span style="display:flex;">&lt;span> - regex: &lt;span style="color:#666;font-style:italic">&amp;#34;=sum\\((?P&amp;lt;num1&amp;gt;.*?),(?P&amp;lt;num2&amp;gt;.*?)\\)&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> replace: &lt;span style="color:#666;font-style:italic">&amp;#34;{{result}}&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> vars:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> - name: result
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> type: shell
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> params:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> cmd: &lt;span style="color:#666;font-style:italic">&amp;#34;expr $ESPANSO_NUM1 + $ESPANSO_NUM2&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;hr>
&lt;h2 id="expanding-llm-output">Expanding LLM Output&lt;/h2>
&lt;p>My initial goal was to create a new trigger — &lt;code>:llm(⋯)&lt;/code> — that allows for the following workflow: copy the text that we want the LLM to work with to the system&amp;rsquo;s clipboard (so that we don&amp;rsquo;t have to pipe a full file), type the trigger with the system prompt in the parenthesis, and expand this trigger to the custom LLM callout in Obsidian (with the system prompt as title, and the output of the LLM as the body of the callout). The trigger &lt;code>:llm(&amp;quot;Summarise the provided text.&amp;quot;)&lt;/code> would expand to:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-text" data-lang="text">&lt;span style="display:flex;">&lt;span>&amp;gt; [!llm]- &amp;#34;Summarise the provided text.&amp;#34; 💬
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>⋯ (→ Summary of text currently in clipboard)
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="limitation-of-regex-triggers">Limitation of Regex Triggers&lt;/h3>
&lt;p>Regex triggers have a limitation where the maximum length of a regex match, including captured named groups, are restricted to 30 characters&lt;label for="sidenote-1" class="sidenote-number">&lt;/label>&lt;input type="checkbox" id="sidenote-1" class="sidenote-toggle">&lt;span class="sidenote">&lt;a href="https://espanso.org/docs/matches/regex-triggers/#limitations">Regex triggers - Espanso Documentation&lt;/a>&lt;/span>. This constraint is intended to enhance performance, but makes it harder to write an expressive system prompt.&lt;/p>
&lt;p>To circumvent this issue, we can use an LLM to first write us a full-sized prompt based on one or two provided keywords (as to keep our trigger under 30 characters) that we can then feed to our main LLM.&lt;/p>
&lt;p>We could do this in one call — asking to expand the prompt and then use this prompt to perform the task at hand — but this would make it harder to extract the expanded prompt for the title for our custom callout. Another approach would be to provide the callout boilerplate to &lt;code>llm&lt;/code>, asking it to return the filled-in callout; but as API calls are cheap, we&amp;rsquo;ll go with the most straightforward approach:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-yaml" data-lang="yaml">&lt;span style="display:flex;">&lt;span>- regex: &lt;span style="color:#666;font-style:italic">&amp;#34;:llm\\((?P&amp;lt;userprompt&amp;gt;.*?)\\)&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> replace: &lt;span style="color:#666;font-style:italic">&amp;#34;&amp;gt; [!llm]- \&amp;#34;{{prompt}}\&amp;#34; 💬\n⋯\n\n&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> vars:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> - name: prompt
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> type: shell
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> params:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> cmd: &amp;#34;echo $ESPANSO_USERPROMPT | llm -s
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> \ &amp;#39;Expand provided keywords to a short but effective, 
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> \ imperative one-sentence prompt in UK English.
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> \ The final output when using this prompt should be 
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> \ short and to the point.&amp;#39;
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>For now, if we type &lt;code>:llm(explain to 5yo)&lt;/code>, we get the following in return:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-text" data-lang="text">&lt;span style="display:flex;">&lt;span>&amp;gt; [!llm]- &amp;#34;Explain the provided text in simple words 
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>that a 5-year-old can easily understand.&amp;#34; 💬
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>⋯
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="passing-full-sized-prompt">Passing Full-Sized Prompt&lt;/h3>
&lt;p>Now that we can circumvent our character limit and have access to our full-sized prompt, we just have to provide it to another call of &lt;code>llm&lt;/code>. We can access the full-sized prompt by using the variable name in all caps, preceded by &lt;code>$ESPANSO_&lt;/code>. I pass the output through &lt;code>sed&lt;/code> — replacing newlines with the &lt;code>&amp;gt; &lt;/code> character — as to make sure that everything sticks together in the final callout.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-yaml" data-lang="yaml">&lt;span style="display:flex;">&lt;span>- regex: &lt;span style="color:#666;font-style:italic">&amp;#34;:llm\\((?P&amp;lt;userprompt&amp;gt;.*?)\\)&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> replace: &lt;span style="color:#666;font-style:italic">&amp;#34;&amp;gt; [!llm]- \&amp;#34;{{prompt}}\&amp;#34; 💬\n{{output}}\n\n&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> vars:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> - name: prompt
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> type: shell
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> params:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> cmd: &lt;span style="color:#666;font-style:italic">&amp;#34;echo $ESPANSO_USERPROMPT | llm -m &amp;#39;gpt-4o&amp;#39; -s
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#666;font-style:italic"> \ &amp;#39;Expand provided keywords to a short but effective, 
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#666;font-style:italic"> \ imperative one-sentence prompt in UK English.
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#666;font-style:italic"> \ The final output when using this prompt should be 
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#666;font-style:italic"> \ short and to the point.&amp;#39;
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#666;font-style:italic"> - name: output
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#666;font-style:italic"> type: shell
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#666;font-style:italic"> params:
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#666;font-style:italic"> cmd: &amp;#34;&lt;/span>llm -m &amp;#39;gpt-4o&amp;#39; -s
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> \ \&amp;#34;$ESPANSO_PROMPT.\&amp;#34; | sed -z \&amp;#34;s/\\n/\\n&amp;gt; /g\&amp;#34;&amp;#34;
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="final-espanso-trigger">Final Espanso Trigger&lt;/h3>
&lt;p>The last thing we have to do is accessing our clipboard. We do this by creating a new variable inside our trigger,&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-yaml" data-lang="yaml">&lt;span style="display:flex;">&lt;span>- name: clipboard
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> type: clipboard
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>and piping it to our final call to &lt;code>llm&lt;/code>. The final Espanso trigger looks as follows:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-yaml" data-lang="yaml">&lt;span style="display:flex;">&lt;span>- regex: &lt;span style="color:#666;font-style:italic">&amp;#34;:llm\\((?P&amp;lt;userprompt&amp;gt;.*?)\\)&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> replace: &lt;span style="color:#666;font-style:italic">&amp;#34;&amp;gt; [!llm]- \&amp;#34;{{prompt}}\&amp;#34; 💬\n{{output}}\n\n&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> vars:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> - name: clipboard
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> type: clipboard
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> - name: prompt
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> type: shell
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> params:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> cmd: &lt;span style="color:#666;font-style:italic">&amp;#34;echo $ESPANSO_USERPROMPT | llm -m &amp;#39;gpt-4o&amp;#39; -s
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#666;font-style:italic"> \ &amp;#39;Expand provided keywords to a short but effective, 
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#666;font-style:italic"> \ imperative one-sentence prompt in UK English.
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#666;font-style:italic"> \ The final output when using this prompt should be 
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#666;font-style:italic"> \ short and to the point.&amp;#39;
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#666;font-style:italic"> - name: output
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#666;font-style:italic"> type: shell
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#666;font-style:italic"> params:
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#666;font-style:italic"> cmd: &amp;#34;&lt;/span>echo $ESPANSO_CLIPBOARD | llm -m &amp;#39;gpt-4o&amp;#39; -s
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> \ \&amp;#34;$ESPANSO_PROMPT.\&amp;#34; | sed -z \&amp;#34;s/\\n/\\n&amp;gt; /g\&amp;#34;&amp;#34;
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;hr>
&lt;h2 id="demonstration">Demonstration&lt;/h2>
&lt;p>










&lt;figure class="">

 &lt;div>
 &lt;img loading="lazy" alt="Demonstration of LLM expansion" src=" /posts/llm-demo.gif">
 &lt;/div>

 
 
 &lt;div class="caption-container">
 &lt;figcaption> Demonstration of LLM expansion &lt;/figcaption>
 &lt;/div>
 
 
&lt;/figure>&lt;/p></description></item><item><title>Automating Boggle</title><link>http://patrick.vanderspie.gl/posts/automating-boggle/</link><pubDate>Sat, 06 Apr 2024 00:00:00 +0000</pubDate><guid>http://patrick.vanderspie.gl/posts/automating-boggle/</guid><description>&lt;p>Last week, I had a conversation with a good friend in the spirit of the &lt;a href="https://thomasdeneuville.com/cult-of-done-manifesto/">&amp;ldquo;Cult of Done&amp;rdquo;&lt;/a> (&lt;span class="llm-generated">valuing action over perfection, boldly experimenting and learning through doing&lt;/span>) and &lt;a href="https://austinkleon.com/show-your-work/">sharing our work&lt;/a>. Based on this conversation, I decided to get my hands dirty and attempt to create a quick and fun project, constraining myself to a couple hours of work on a Saturday morning. Key here is not worrying about perfection, but getting something out there quickly&lt;label for="sidenote-1" class="sidenote-number">&lt;/label>&lt;input type="checkbox" id="sidenote-1" class="sidenote-toggle">&lt;span class="sidenote">As the goal of this experiment was to move fast, this writing is assisted using GPT-4 through the OpenAI API. Everything with a highlighted background was first written with the help of an LLM using my instructions and provided information, and rewritten and tweaked afterwards to fit my writing style.&lt;/span>.&lt;/p>
&lt;hr>
&lt;h2 id="thrifted-boggle">Thrifted Boggle&lt;/h2>
&lt;p>My girlfriend and I enjoy playing board and card games. Recently, we&amp;rsquo;ve picked up a copy of &lt;a href="https://en.wikipedia.org/wiki/Boggle">Boggle&lt;/a> in a thrift shop. &lt;span class="llm-generated">Boggle is a word game where you try to find as many words as you can from 16 dice, each with a different letter, that are shaken in a covered tray and then set in a $4\times4$ grid. Players get three minutes to find words that are at least three letters long, and each letter in the word has to touch the letter before it in any direction. Players are not allowed to use the same letter dice more than once in a word. Once time&amp;rsquo;s up, players compare their lists and any word that appears on more than one list gets cancelled out, with players scoring points based on the length of their unique words.&lt;/span>&lt;/p>
&lt;p>










&lt;figure class="">

 &lt;div>
 &lt;img loading="lazy" alt="Boggle board" src=" /posts/boggle-board.jpg">
 &lt;/div>

 
 
 &lt;div class="caption-container">
 &lt;figcaption> Boggle board &lt;/figcaption>
 &lt;/div>
 
 
&lt;/figure>&lt;/p>
&lt;p>Our second-hand copy of Boggle has a couple of problems, however (excluding the fact that one of the dice was replaced by a Scrabble block by the previous owner). Our main problem is that the hourglass provided with our copy is broken, so we have to set up a smartphone timer. Other problems include:&lt;/p>
&lt;ul>
&lt;li>Counting the points afterwards takes up a lot of time, especially if we want to confirm that our words are actually on the board&lt;/li>
&lt;li>Confirming if a found word is an actual word takes time as well (as I love making up some plausible sounding Dutch words, looking them up in the dictionary, and claiming that I was certain that it exists if it&amp;rsquo;s not there.)&lt;/li>
&lt;/ul>
&lt;h2 id="project-goals">Project Goals&lt;/h2>
&lt;p>As I want to create something that both helps solve the problems above, but I also want to get something done quickly, the goal for this project is threefold:&lt;/p>
&lt;ol>
&lt;li>First, create a &lt;a href="https://telegram.org/blog/bot-revolution">Telegram bot&lt;/a> that can:
&lt;ul>
&lt;li>Receive a picture of the shaken tray of dice&lt;/li>
&lt;li>Start a built-in timer to know when the round starts and ends&lt;/li>
&lt;li>Count our points by confirming that our words are on the board and that the words themselves exist based on a (Dutch) dictionary&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>Next, I want to share my work using this blog post (in the spirit of the Cult of Done)&lt;/li>
&lt;li>Do all this in a couple of hours on a Saturday morning (instead of enjoying the nice Spring weather like a normal person)&lt;/li>
&lt;/ol>
&lt;h2 id="implementation">Implementation&lt;/h2>
&lt;p>I started with a planning, trying to come up with the necessary sub-tasks:&lt;/p>
&lt;ol>
&lt;li>Recognising labelled dice from a picture, which we need to reconstruct the grid&lt;/li>
&lt;li>Create a data structure that contains the allowed list of words&lt;/li>
&lt;li>Algorithm that traverses the grid, finding all valid words&lt;/li>
&lt;li>Create a Telegram bot that uses the above to act as a &amp;ldquo;referee&amp;rdquo;&lt;/li>
&lt;li>Write the text (that you&amp;rsquo;re currently reading) whilst doing all of the above&lt;/li>
&lt;/ol>
&lt;h3 id="recognising-labelled-dice">Recognising labelled dice&lt;/h3>
&lt;p>As I am not that familiar with computer vision techniques, and I wanted to move quickly, my first attempt was to use &lt;a href="https://platform.openai.com/docs/guides/vision">GPT-4V&lt;/a>, or &lt;code>gpt-4-vision-preview&lt;/code>, to take a picture of the Boggle grid as input and retrieve the letters from that picture as a continuous string. If this would work out, I could quickly move to the other steps of the project. I used the following prompt:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-text" data-lang="text">&lt;span style="display:flex;">&lt;span>Given the image of a 4x4 grid of Boggle dice, 
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>convert the letters visible on the dice into a 
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>continuous 16-character string by reading from left to right,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>top to bottom. 
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>For example, if the top row contains the letters &amp;#39;A&amp;#39;, &amp;#39;B&amp;#39;, &amp;#39;C&amp;#39;, &amp;#39;D&amp;#39;, 
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>and the second row contains letters &amp;#39;E&amp;#39;, &amp;#39;F&amp;#39;, &amp;#39;G&amp;#39;, and &amp;#39;H&amp;#39;, etc., 
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>your output should be &amp;#39;ABCDEFGH...etc&amp;#39;, up to the 16th letter.
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>Keep in mind that letters could be rotated!
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>This failed miserably, however. The received output based on the image above (cropped to only display the grid of dice) was:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-text" data-lang="text">&lt;span style="display:flex;">&lt;span>The continuous 16-character string from the Boggle dice 
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>is &amp;#34;INRAWMNBUDOVSOLO&amp;#34;.
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Afterwards, I lost an hour trying out different things with &lt;a href="https://opencv.org/">OpenCV&lt;/a> and &lt;a href="https://github.com/tesseract-ocr/tesseract">tesseract&lt;/a>, but this did not seem to work out. As I still wanted to move quickly, I skipped this step and will rely on user input for the grid.&lt;/p>
&lt;h3 id="recognising-valid-words">Recognising valid words&lt;/h3>
&lt;p>The &lt;a href="https://en.wikipedia.org/wiki/Trie">trie&lt;/a> is an efficient data structure for storing large word lists. The data structure itself can be seen as a finite state machine that only accepts words used for its creation. Each node of the trie represents a prefix, with the root as the empty string. Transitions between nodes adds characters to the prefixes. &lt;span class="llm-generated">This offers quick search capabilities, therefore making it suitable for our project.&lt;/span>&lt;/p>
&lt;p>As I&amp;rsquo;ve recently had to create an implementation of tries for other work, I simply reused this for this project. The idea behind the construction algorithm is very simple: &lt;span class="llm-generated">starting from the initial state, we check every symbol in each word. We follow transitions to states if they already exist, and create new states and transitions if they don&amp;rsquo;t. At the end of each word, we mark the last state as the final state.&lt;/span>&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>&lt;span style="font-weight:bold;text-decoration:underline">def&lt;/span> &lt;span style="color:#666;font-weight:bold;font-style:italic">trie&lt;/span>(sequences): 
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#666;font-style:italic">&amp;#34;&amp;#34;&amp;#34;Constructs a trie from given collection of sequences.&amp;#34;&amp;#34;&amp;#34;&lt;/span> 
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> fsm = Automaton() 
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="font-weight:bold;text-decoration:underline">for&lt;/span> sequence &lt;span style="font-weight:bold">in&lt;/span> sequences: 
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> current = fsm.initial 
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="font-weight:bold;text-decoration:underline">for&lt;/span> symbol &lt;span style="font-weight:bold">in&lt;/span> sequence: 
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="font-weight:bold;text-decoration:underline">if&lt;/span> symbol &lt;span style="font-weight:bold">not&lt;/span> &lt;span style="font-weight:bold">in&lt;/span> fsm.alphabet: 
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> fsm.add_symbol(symbol) 
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="font-weight:bold;text-decoration:underline">if&lt;/span> (next_state := fsm.follow(current, symbol)) &lt;span style="font-weight:bold">is&lt;/span> &lt;span style="font-weight:bold;text-decoration:underline">None&lt;/span>: 
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> next_state = fsm.add_state() 
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> fsm.set_transition(current, next_state, symbol) 
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> current = next_state 
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> fsm.accepting.add(current) 
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="font-weight:bold;text-decoration:underline">return&lt;/span> fsm
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-python" data-lang="python">&lt;span style="display:flex;">&lt;span>words = {&lt;span style="color:#666;font-style:italic">&amp;#34;tree&amp;#34;&lt;/span>, &lt;span style="color:#666;font-style:italic">&amp;#34;three&amp;#34;&lt;/span>, &lt;span style="color:#666;font-style:italic">&amp;#34;trie&amp;#34;&lt;/span>, &lt;span style="color:#666;font-style:italic">&amp;#34;tried&amp;#34;&lt;/span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>automaton = trie(words)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>automaton.accept(&lt;span style="color:#666;font-style:italic">&amp;#34;tree&amp;#34;&lt;/span>) &lt;span style="color:#888;font-style:italic"># True&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>automaton.accept(&lt;span style="color:#666;font-style:italic">&amp;#34;tried&amp;#34;&lt;/span>) &lt;span style="color:#888;font-style:italic"># True&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>automton.accept(&lt;span style="color:#666;font-style:italic">&amp;#34;thr&amp;#34;&lt;/span>) &lt;span style="color:#888;font-style:italic"># False&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>show(automaton)
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>
&lt;figure>
 &lt;img src="http://patrick.vanderspie.gl/posts/trie-example.svg" alt="Example of the trie for set (tree, trie, three, tried)" class="invert-dark" loading="lazy" />
 &lt;figcaption>Example of the trie for set (tree, trie, three, tried)&lt;/figcaption>
&lt;/figure>


&lt;p>For the dictionary of valid words, I relied on the &lt;a href="https://raw.githubusercontent.com/OpenTaal/opentaal-wordlist/master/wordlist.txt">OpenTaal wordlist&lt;/a>, containing over 400k Dutch words. We can shorten this list significantly by filtering out the ones that are impossible to find in Boggle:&lt;/p>
&lt;ul>
&lt;li>Words that contain something other than the letters from &lt;code>a&lt;/code> to &lt;code>z&lt;/code>&lt;/li>
&lt;li>Words shorter than 3 letters or longer than 10 letters (as we&amp;rsquo;re usually never finding any words longer than 6, 10 is very generous)&lt;/li>
&lt;/ul>
&lt;p>In the end, we end up with a large trie that accepts all words from our Dutch wordlist.&lt;/p>
&lt;h3 id="searching-for-words-in-a-grid">Searching for words in a grid&lt;/h3>
&lt;p>If we have a string representing the grid as input (e.g., &lt;code>&amp;quot;NTAWIRIRNBDAOASO&amp;quot;&lt;/code>), we can find all possible words by &lt;span class="llm-generated">converting our grid into a graph, allowing us to utilize graph traversal algorithms to systematically search for potential word formations. This graph representation simplifies the problem into a more navigable structure where each dice is a vertex, denoted by its character. Edges between vertices are formed based on a dice&amp;rsquo;s spatial relation to its neighbours, indicating possible transitions or movements in our word formation process.&lt;/span>&lt;/p>
&lt;p>To find all words in the grid, we can perform a depth-first traversal starting from each dice, &lt;span class="llm-generated">exploring as far as possible along each branch before backtracking. It is an ideal choice for our case here since we are interested in finding all potential paths that form words. Starting from each dice (or vertex), our DFT traces all unique paths devoid of cycles, while continuously inspecting the trie to confirm whether the traced path is leading to a valid word.&lt;/span> We stop traversing further in a given path if the prefix is not present in the trie. This way, we retrieve all acyclic paths in the graph that have a path in the trie, and we don&amp;rsquo;t go deeper than necessary into the current path.&lt;/p>
&lt;h3 id="telegram-bot">Telegram bot&lt;/h3>
&lt;p>Using the above, we can create a &lt;a href="https://core.telegram.org/bots">Telegram bot&lt;/a> that uses the above to confirm our words and to keep track of our scores:&lt;/p>
&lt;p>










&lt;figure class="">

 &lt;div>
 &lt;img loading="lazy" alt="Screenshot of the Telegram chat with bogglebot" src=" /posts/telegram-screenshot.PNG">
 &lt;/div>

 
 
 &lt;div class="caption-container">
 &lt;figcaption> Screenshot of the Telegram chat with &lt;code>bogglebot&lt;/code> &lt;/figcaption>
 &lt;/div>
 
 
&lt;/figure>&lt;/p>
&lt;span class="llm-generated">Users input their Boggle game board as a grid by invoking the &lt;code>/new&lt;/code> command in a chat with the bot, and the bot generates a list of all possible words by simulating all possible paths in the grid. The bot will then confirm the start and the counter of the game. During the game, the users type in the words they find, and the bot checks if the word is valid (i.e., is in the list of possible words). If the word is valid, the user&amp;rsquo;s score is incremented according to the length of the word. The score and game updates are sent back to the user in the chat. The bot is built using the &lt;code>python-telegram-bot&lt;/code> library, which allows easy interaction with the Telegram Bot API.&lt;/span>
&lt;h2 id="possible-extensions">Possible Extensions&lt;/h2>
&lt;span class="llm-generated">Given the flexibility of the implementation, there are various ways we could extend this project in the future:&lt;/span>
&lt;ul>
&lt;li>&lt;span class="llm-generated">&lt;strong>Wordlist Variants:&lt;/strong> Depending on our mood or the challenge we desire, we could spice up our games by introducing different wordlists for specific themes. For example, a game could be based solely on palindromes, or we could introduce a Flemish dialect-based wordlist. The possibilities and variations are endless, making each game a fresh and exciting challenge.&lt;/span>&lt;/li>
&lt;li>&lt;span class="llm-generated">&lt;strong>Custom Time Counter:&lt;/strong> A part of the thrill of Boggle comes from the time pressure. We could experiment with this element by introducing a varying time or perhaps even a countdown whereby the time decreases each round, raising the stakes progressively.&lt;/span>&lt;/li>
&lt;li>&lt;span class="llm-generated">&lt;strong>Automatic Scoring:&lt;/strong> One of the main appeals of this project is that it removes the cumbersome task of manual point counting. This could be further extended to automatically keep track of our scores across multiple games or even different days. Thus, we could concentrate on the game at hand, while the bot takes care of the statistics and progress tracking.&lt;/span>&lt;/li>
&lt;li>&lt;span class="llm-generated">&lt;strong>Speed Chess Inspired Mode:&lt;/strong> Perhaps one of the most exciting possibilities would be to create a &amp;lsquo;speed chess&amp;rsquo; inspired mode. Each player could have an individual clock, and you&amp;rsquo;d need to quickly spot and share a unique word to pause your countdown and switch turns. The game would then continue until someone&amp;rsquo;s time reaches zero. This offers the perfect blend of frantic quick-thinking, strategy, and suspense — a true test of one&amp;rsquo;s Boggle skills!&lt;/span>&lt;/li>
&lt;/ul>
&lt;span class="llm-generated">In the spirit of the &amp;ldquo;Cult of Done&amp;rdquo;, I have focused on quickly getting this project up and running first in a short amount of allotted time, before worrying about enhancements or delivering the perfect project.&lt;/span></description></item><item><title>Inferno CTF — Write-Up</title><link>http://patrick.vanderspie.gl/posts/inferno-ctf-misc/</link><pubDate>Sat, 28 Dec 2019 00:00:00 +0000</pubDate><guid>http://patrick.vanderspie.gl/posts/inferno-ctf-misc/</guid><description>&lt;p>In this short write-up, we will go over two challenges in the &lt;em>Misc&lt;/em> category of Inferno CTF, which was hosted by &lt;a href="https://ctftime.org/team/69272">&lt;code>Dc1ph3R&lt;/code>&lt;/a>. The challenges that we will discuss are “Color Blind” (which can be solved with a simple one-liner) and “Registering X” (which was a bit more complex). I completed these challenges with our CTF team, &lt;a href="https://ctftime.org/team/83549">&lt;code>U+1F966&lt;/code>&lt;/a>.&lt;/p>
&lt;hr>
&lt;h2 id="color-blind">Color Blind&lt;/h2>
&lt;p>In this challenge, we are provided with a &lt;code>PNG&lt;/code> image with a resolution of $64 \times 1$. Running &lt;code>strings&lt;/code>, &lt;code>binwalk&lt;/code>, and &lt;code>foremost&lt;/code> on the given file does not give us any useful information.&lt;/p>
&lt;p>Looking closer at this image, we can see that each pixel has a specific colour. A teammate mentioned that the actual colours of the pixels could be the key to our flag. Each pixel has a colour code that can be expressed in &lt;code>RGB&lt;/code> or in &lt;code>HEX&lt;/code>. Stringing together all the &lt;code>HEX&lt;/code> values is the key to obtaining our flag.&lt;/p>
&lt;p>Opening an image, retrieving all the &lt;code>HEX&lt;/code> values of the pixels, and stringing them together is a trivial task that can be done with a simple one-liner in the &lt;a href="https://julialang.org/">Julia programming language&lt;/a>:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-Julia" data-lang="Julia">&lt;span style="display:flex;">&lt;span>&lt;span style="font-weight:bold;text-decoration:underline">using&lt;/span> FileIO, Colors
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>join(map(hex, load(&lt;span style="color:#666;font-style:italic">&amp;#34;colorblind.png&amp;#34;&lt;/span>)))
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>This works because images in Julia are simple arrays containing the &lt;code>RGB&lt;/code> value of each pixel. We map the &lt;code>hex&lt;/code> function (provided to us by &lt;code>Colors&lt;/code>) over the color values, which returns an array with &lt;code>HEX&lt;/code> values. We then join all the values together, which gives us the following string:&lt;/p>
&lt;pre tabindex="0">&lt;code class="language-hex" data-lang="hex">626C6168626C6168626C61685F68656C6C6F5F686F775F6172655F796F755F746F6461795F695F686F70655F796F755F6172655F6E6F745F646F696E675F746869735F6D616E75616C6C795F696E6665726E6F4354467B6833795F3130306B5F7930755F3472335F6E30375F6833785F626C316E445F3A4F7D5F646F696E675F746869735F6D616E75616C6C795F776F756C645F62655F615F6261645F696465615F796F755F73686F756C646E745F646F5F69745F6D616E75616C6C795F6F6B
&lt;/code>&lt;/pre>&lt;p>This string can probably be translated to readable text using Julia, but it was quicker to copy this output and paste it in an online &lt;em>hex to string&lt;/em> converter:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-text" data-lang="text">&lt;span style="display:flex;">&lt;span>blahblahblah_hello_how_are_you_today_i_hope_you_are_not_doing_this_manually_infernoCTF{h3y_100k_y0u_4r3_n07_h3x_bl1nD_:O}_doing_this_manually_would_be_a_bad_idea_you_shouldnt_do_it_manually_ok
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>The output above contains the flag:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-text" data-lang="text">&lt;span style="display:flex;">&lt;span>infernoCTF{h3y_100k_y0u_4r3_n07_h3x_bl1nd_:O}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;hr>
&lt;h2 id="registering-x">Registering X&lt;/h2>
&lt;p>The goal of this challenge was to construct a string that matches the &lt;code>regex&lt;/code> given to us in the challenge attachment:&lt;/p>
&lt;pre tabindex="0">&lt;code class="language-regex" data-lang="regex">infernoCTF{.(?&amp;lt;=H){21}.[a-z](?&amp;lt;=\+a){1024}[a-z][a-j](?&amp;lt;!([a-u]|[w-z])(j|[a-h])).{2,64}(?&amp;lt;!\S){255}n.{2}(?&amp;lt;=\s)g_fUn\W(?&amp;lt;=[A-z])}(?&amp;lt;=\..{14})(?&amp;lt;=^.{33})
&lt;/code>&lt;/pre>&lt;p>The given &lt;code>regex&lt;/code> is complex at first glance, but the challenge itself is very straightforward. We did not find a way to automate the solution of this challenge, so we solved it by hand. First, we simplified the &lt;code>regex&lt;/code> by removing the quantifiers after the lookbehinds, because they do not serve a purpose. Removing these quantifiers will give us an equivalent, but shorter, &lt;code>regex&lt;/code>:&lt;/p>
&lt;pre tabindex="0">&lt;code class="language-regex" data-lang="regex">infernoCTF{.(?&amp;lt;=H).[a-z](?&amp;lt;=\+a)[a-z][a-j](?&amp;lt;!([a-u]|[w-z])(j|[a-h])).{2,64}(?&amp;lt;!\S)n.{2}(?&amp;lt;=\s)g_fUn\W(?&amp;lt;=[A-z])}(?&amp;lt;=\..{14})(?&amp;lt;=^.{33})
&lt;/code>&lt;/pre>&lt;p>We start by learning about &lt;em>lookbehinds&lt;/em>. Positive lookbehinds (e.g, &lt;code>&amp;quot;(?&amp;lt;=a)b&amp;quot;&lt;/code> ) match the character after the parenthesis, but only if the character is followed by the character in the parenthesis, after the &lt;code>=&lt;/code>. Negative lookbehinds (e.g, &lt;code>&amp;quot;(?&amp;lt;!a)b&amp;quot;&lt;/code> ) match the character after the parenthesis, but only if they are not followed by the character in the parenthesis, after the &lt;code>!&lt;/code>. Knowing this, we can start constructing our flag.&lt;/p>
&lt;p>Our flag starts with &lt;code>infernoCTF{&lt;/code>, because the given &lt;code>regex&lt;/code> states that we have to match these characters literally. &lt;code>.&lt;/code> in our regex matches any character in our flag, so we will put an &lt;code>X&lt;/code> as a placeholder. We now have to match the positive lookbehind &lt;code>(?&amp;lt;=H).&lt;/code>, which means that our string will only match the regex when we have a character following the character &lt;code>H&lt;/code>. We could try adding &lt;code>H+&lt;/code> to our flag (making our flag &lt;code>infernoCTF{XH+)&lt;/code>, but this will not match the given regex. The cause of this mismatch is the fact that lookbehinds only match the character &lt;em>after&lt;/em> the parenthesis. It will &lt;strong>not&lt;/strong> match the character &lt;em>in&lt;/em> the parenthesis.&lt;/p>
&lt;p>The solution for this problem is replacing our previous &lt;code>X&lt;/code> with an &lt;code>H&lt;/code>. We now have &lt;code>infernoCTF{H+&lt;/code>, which matches the first part of our &lt;code>regex&lt;/code>. The rest of the &lt;code>regex&lt;/code> follows the same principle: we have a bunch of lookbehinds that force us to precede a character with another character. Around those lookbehinds we have more common &lt;code>regex&lt;/code> symbols that make it possible to match the character in the parenthesis. Now that we got the gist of the challenge, we will quickly go over the solution:&lt;/p>
&lt;ul>
&lt;li>&lt;code>infernoCTF{&lt;/code> forces us to start our flag with &lt;code>infernoCTF{&lt;/code>&lt;/li>
&lt;li>&lt;code>.(?&amp;lt;=H).[a-z](?&amp;lt;=\+a)&lt;/code> forces us to add &lt;code>H+a&lt;/code>&lt;/li>
&lt;li>&lt;code>[a-z][a-j](?&amp;lt;!([a-u]|[w-z)(j|[a-h])).{2,64}&lt;/code> makes us add &lt;code>vi&lt;/code> to our flag, followed by an unspecified amount (between two and sixty-four, which will be constrained later in the &lt;code>regex&lt;/code>) of placeholder characters&lt;/li>
&lt;li>&lt;code>(?&amp;lt;!\S)n&lt;/code> forces us to add an &lt;code>n&lt;/code> after a whitespace character&lt;/li>
&lt;li>&lt;code>.{2}(?&amp;lt;=\s)g&lt;/code> forces us to add a &lt;code>g&lt;/code> after a whitespace character&lt;/li>
&lt;li>&lt;code>_fUn&lt;/code> forces us to add &lt;code>_fUn&lt;/code> to our flag&lt;/li>
&lt;li>&lt;code>\W(?&amp;lt;=[A-z])}&lt;/code> forces us to add a &lt;code>}&lt;/code> after a non-word character between &lt;code>A&lt;/code> and &lt;code>z&lt;/code> in the ASCII table (e.g., &lt;code>]&lt;/code>)&lt;/li>
&lt;/ul>
&lt;p>&lt;code>(?&amp;lt;=\..{14})&lt;/code> forces us to count fourteen characters back and add a &lt;code>.&lt;/code>. We can safely put our &lt;code>.&lt;/code> in the collection of unspecified characters mentioned in the third step above.&lt;/p>
&lt;p>The last part of our &lt;code>regex&lt;/code> forces us to make the flag exactly thirty-three characters long. This can be achieved by adding or removing a bunch of unspecified characters (which were mentioned in the same third step). One of the possible solutions is &lt;code>infernoCTF{H+avi . ng no g_fUn]}&lt;/code>.&lt;/p>
&lt;p>Because of a lack of knowledge about tools that generate strings based on given &lt;code>regex&lt;/code> (&lt;em>that also support lookbehinds!&lt;/em>), we decided to solve this challenge by hand. Although it is a bit of tedious work, it certainly is possible to construct the flag manually.&lt;/p></description></item></channel></rss>