<?xml version="1.0" encoding="UTF-8"?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en">
    <title>yuri.is</title>
    <subtitle>crafting tools for the information age</subtitle>
    <link rel="self" type="application/atom+xml" href="https://yuri.is/atom.xml"/>
    <link rel="alternate" type="text/html" href="https://yuri.is"/>
    <generator uri="https://www.getzola.org/">Zola</generator>
    <updated>2026-05-23T10:05:41-04:00</updated>
    <id>https://yuri.is/atom.xml</id>
    <entry xml:lang="en">
        <title>LLM Experiments #5: Design by API</title>
        <published>2026-05-22T21:25:56-04:00</published>
        <updated>2026-05-23T10:05:41-04:00</updated>
        
        <author>
          <name>
            Yuri Vishnevsky
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://yuri.is/writing/llm-design-by-api/"/>
        <id>https://yuri.is/writing/llm-design-by-api/</id>
        
        <content type="html" xml:base="https://yuri.is/writing/llm-design-by-api/">&lt;p&gt;&lt;em&gt;Note:&lt;&#x2F;em&gt; Experimenting with quick writeups as I play around with LLMs and my homegrown agentic workflow system. &lt;a href=&quot;&#x2F;writing&#x2F;llm-intermezzo&#x2F;&quot;&gt;Previously.&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;When working on my workflow system I’m starting to find that the most effective use of my attention is to focus on API surfaces of the core subsystems. With a &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;caseymuratori.com&#x2F;blog_0024&quot;&gt;layered API design&lt;&#x2F;a&gt;, the surface will span multiple levels of subsystem, and so give a rough shape to everything underneath. I’ve found &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;hegel.dev&#x2F;&quot;&gt;property testing&lt;&#x2F;a&gt; and &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;go.dev&#x2F;blog&#x2F;synctest&quot;&gt;synctest&lt;&#x2F;a&gt; to be valuable tools for defining and testing the properties of the concurrent execution engine: the contracts of the API serve as useful inspiration for tests. In general, a lot about this project feels like it’s about artfully and verifiably constraining growth.&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>LLM Experiments #4: Intermezzo</title>
        <published>2026-05-18T20:59:34-04:00</published>
        <updated>2026-05-19T08:47:13-04:00</updated>
        
        <author>
          <name>
            Yuri Vishnevsky
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://yuri.is/writing/llm-intermezzo/"/>
        <id>https://yuri.is/writing/llm-intermezzo/</id>
        
        <content type="html" xml:base="https://yuri.is/writing/llm-intermezzo/">&lt;p&gt;&lt;em&gt;Note:&lt;&#x2F;em&gt; Experimenting with quick writeups as I play around with LLMs and my homegrown agentic workflow system. &lt;a href=&quot;&#x2F;writing&#x2F;llm-noodling&#x2F;&quot;&gt;Previously.&lt;&#x2F;a&gt; Scattered notes this time.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;agent-swarms-for-bug-hunting&quot;&gt;Agent swarms for bug hunting&lt;&#x2F;h2&gt;
&lt;p&gt;Inspired by Mythos, I wanted to see what I could find with a small army of sub-agents hunting for bugs in Julia. It helped to have done this &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;yuri.is&#x2F;not-julia&#x2F;&quot;&gt;before&lt;&#x2F;a&gt; since I could direct the agents to known-sus parts of the codebase, and instruct them on tactics.&lt;&#x2F;p&gt;
&lt;p&gt;With the lower-level work automated I was able to put more of my efforts into qualifying the legitimacy&#x2F;severity of the bugs and organizing the results, ending up with a compact table of ~10 bugs each with a one-line summary and one-line reproduction. A few turned out to be documented&#x2F;allowed behavior, others were &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;JuliaStats&#x2F;Statistics.jl&#x2F;issues&#x2F;204&quot;&gt;quickly&lt;&#x2F;a&gt; &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;JuliaSparse&#x2F;SparseArrays.jl&#x2F;issues&#x2F;713&quot;&gt;fixed&lt;&#x2F;a&gt;, and some remain open, awaiting agents from the future.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;example-driven-evaluation-quick-cross-language-prototyping&quot;&gt;Example-driven evaluation &amp;amp; quick cross-language prototyping&lt;&#x2F;h2&gt;
&lt;p&gt;I’ve been working on the design of my agentic workflow&#x2F;podcast analysis system by writing lots of pipelines and seeing how they surface gaps in the underlying system and API. It’s so cool that the example-driven feedback loop approach can now be scaled.&lt;&#x2F;p&gt;
&lt;p&gt;I’ve also been playing around with cross-language rewrites, but am finding that when this is done with too much AI assistance it’s much less valuable since it’s difficult to judge the results. Implementation complexity can vary by orders of magnitude depending on the skill of the implementer, and evaluating the results a deep familiarity with the possibility space. ￼&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>LLM Experiments #3: Noodling</title>
        <published>2026-05-10T10:18:33-04:00</published>
        <updated>2026-05-11T01:16:50-04:00</updated>
        
        <author>
          <name>
            Yuri Vishnevsky
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://yuri.is/writing/llm-noodling/"/>
        <id>https://yuri.is/writing/llm-noodling/</id>
        
        <content type="html" xml:base="https://yuri.is/writing/llm-noodling/">&lt;p&gt;&lt;em&gt;Note:&lt;&#x2F;em&gt; Experimenting with quick writeups as I play around with LLMs and my homegrown agentic workflow system. &lt;a href=&quot;&#x2F;writing&#x2F;eval-planning&#x2F;&quot;&gt;Previously.&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;Nothing much to write about today, but here are some things I’ve explored since my last post.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;gemma-4-has-goblin-tokens&quot;&gt;Gemma 4 has goblin tokens&lt;&#x2F;h2&gt;
&lt;p&gt;Gemma 4 tokens can be seen in tokenizer.json &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;huggingface.co&#x2F;google&#x2F;gemma-4-E4B&#x2F;tree&#x2F;main&quot;&gt;here&lt;&#x2F;a&gt;. I took a look, and it turns out that Gemma has a “ Yuri“ token: &lt;code&gt;&quot;▁Yuri&quot;: 142143&lt;&#x2F;code&gt;, as well as a few goblin-related tokens: &lt;code&gt;&quot;▁goblin&quot;: 218798&lt;&#x2F;code&gt;,&lt;code&gt;&quot;▁Goblin&quot;: 171680&lt;&#x2F;code&gt;, &lt;code&gt;&quot;oblins&quot;: 236144&lt;&#x2F;code&gt;  (the underscore-looking character encodes a space). The tokenizer is BPE (line 311 of tokenizer.json) so I wonder what the presence of these tokens says about the frequency of goblins in the training data. Probably not much, since each of &lt;code&gt;tacos&lt;&#x2F;code&gt;, &lt;code&gt;Tocco&lt;&#x2F;code&gt;, &lt;code&gt;getColumn&lt;&#x2F;code&gt;, &lt;code&gt;StudentVector&lt;&#x2F;code&gt;, &lt;code&gt;flipping&lt;&#x2F;code&gt;, &lt;code&gt;counselling&lt;&#x2F;code&gt;, and &lt;code&gt;combustibles&lt;&#x2F;code&gt; are also tokens, as well as other infrequent words. There are multiple tokens for each of Google and OpenAI, and tokens for &lt;code&gt;anthrop&lt;&#x2F;code&gt; and &lt;code&gt;ic&lt;&#x2F;code&gt;.&lt;&#x2F;p&gt;
&lt;p&gt;The existence of these tokens implies that you can use llama-server’s &lt;code&gt;--logit-bias&lt;&#x2F;code&gt; option to selectively upweight goblins, for example, or steer the model into profanity by increasing the probability of &lt;code&gt;bastard&lt;&#x2F;code&gt;.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;you-need-to-know-the-goal-to-refine-the-task&quot;&gt;You need to know the goal to refine the task&lt;&#x2F;h2&gt;
&lt;p&gt;I relearned this lesson today, this time in the context of optimizing data pipelines whose outputs are subjective.&lt;&#x2F;p&gt;
&lt;p&gt;When you’re just starting out it can be hard to know what you want precisely enough to describe to a small language model, since small models fail loudly in visible ways that force you to specify increasing amounts of procedural detail in your prompt. In contrast, big models are often right enough that you can defer tweaking until you know more about how the results will be applied.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;sampling-in-llama-server&quot;&gt;Sampling in llama-server&lt;&#x2F;h2&gt;
&lt;p&gt;One way to control model output is by changing your sampling strategy. &lt;code&gt;llama-server&lt;&#x2F;code&gt; has many options for this. Many strategies are represented as functions that accept and return a probability distribution, and can thus be composed. Others, like &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;ggml-org&#x2F;llama.cpp&#x2F;pull&#x2F;17927&quot;&gt;adaptive-p&lt;&#x2F;a&gt;, are intended to be used as the last sampler in the chain:&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;adaptive-p selects a token ID rather than just mutating candidates, so it must be last in the sampler chain. It shares this behaviour with some existing samplers like mirostat, dist, and greedy (mirostat being the closest relative).&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;Lots of good descriptions in the &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;ggml-org&#x2F;llama.cpp&#x2F;tree&#x2F;2e97c5f96f9fe2bb26f794a348e05d7a1c74baa1&#x2F;tools&#x2F;completion#generation-flags&quot;&gt;completions tool readme&lt;&#x2F;a&gt;.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;gemma-4-26b-is-pretty-good-at-extracting-emotion-subject-pairs&quot;&gt;Gemma 4 26B is pretty good at extracting emotion-subject pairs&lt;&#x2F;h2&gt;
&lt;p&gt;Gemma 4 does surprisingly well, but has false positives, eg. failing in simple cases like “I want my pilot to lie to me.” which returns &lt;code&gt;{ subject: &quot;pilot&quot;,  emotion: &quot;desire&quot; }&lt;&#x2F;code&gt; where the referent of the emotion is  inaccurate (and I don’t think wanting is an emotion, though it’s apparently debatable). Other edge cases involve tracking the subject through nested conversations, eg. it emitted &lt;code&gt;{ subject: &quot;speaker&quot;, emotion: &quot;ridicule&quot; }&lt;&#x2F;code&gt; for the following quote:&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;And the funny thing is I own Final Cut Pro. I could just &lt;em&gt;click, click, click&lt;&#x2F;em&gt; and make it happen, but I’m like, but I want to figure it out using ffmpeg. And somebody could objectively say, &quot;You idiot, it’s going to take you an hour to do it this way. It would take a minute to do it this way.&quot; I’m like, &quot;But this makes me happier.&quot; And that matters.&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;Whether or not this is acceptable depends on what you’re planning to do with it, but to me, a quote where the speaker imagines a critic ridiculing the speaker feels like it should be represented differently from one in which the the speaker directly ridicules themselves.&lt;&#x2F;p&gt;
&lt;p&gt;I wonder if, rather than trying to anticipate all possible edge cases in the prompt, a generator-verifier pattern might be useful here. I have an unvalidated intuition that decomposing into workflows can be a way to work with the grain of a model, and, more generally, that designing robust pipelines that inherently compensate for expected failure may be a better strategy than to trying to engineer it out.&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>LLM Experiments #2: Eval Planning</title>
        <published>2026-05-08T17:54:55-04:00</published>
        <updated>2026-05-09T06:12:23-04:00</updated>
        
        <author>
          <name>
            Yuri Vishnevsky
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://yuri.is/writing/eval-planning/"/>
        <id>https://yuri.is/writing/eval-planning/</id>
        
        <content type="html" xml:base="https://yuri.is/writing/eval-planning/">&lt;p&gt;&lt;em&gt;Note:&lt;&#x2F;em&gt; Experimenting with quick writeups as I play around with LLMs and my homegrown agentic workflow system. &lt;a href=&quot;&#x2F;writing&#x2F;shorter-sentences&#x2F;&quot;&gt;Previously.&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;LLM costs scale with a request volume, and it turns out that my extractive summarization approach is much too expensive when run at scale on frontier-ish cloud models. So I’m seeing if I can get a small off-the-shelf model, &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;huggingface.co&#x2F;google&#x2F;gemma-4-E2B&quot;&gt;Gemma 4 E2B&lt;&#x2F;a&gt;, to do the task more efficiently.&lt;&#x2F;p&gt;
&lt;p&gt;Since this is a side project I’m been experimenting with letting an agent autonomously drive both the creation and optimization of the pipeline, and view my job as giving the agent a good home with easy access to tools for exploring data, debugging code, and taking performance measurements.&lt;&#x2F;p&gt;
&lt;p&gt;As part of this, the workflow system itself is &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;martinfowler.com&#x2F;eaaDev&#x2F;EventSourcing.html&quot;&gt;event-sourced&lt;&#x2F;a&gt; and includes a provenance tracking system for every data output, allowing us to effortlessly connect outputs back to inputs and intermediate results.&lt;&#x2F;p&gt;
&lt;p&gt;One point of concern is that extractive text compression doesn’t have an obvious scoring function. Fortunately language models are pretty good at language so I’m letting Opus 4.7 judge the quality of the output, with small bits of targeted feedback from me. And for inspiration I did have it read John McPhee’s &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;www.newyorker.com&#x2F;magazine&#x2F;2013&#x2F;04&#x2F;29&#x2F;draft-no-4&quot;&gt;Draft No. 4&lt;&#x2F;a&gt;.&lt;&#x2F;p&gt;
&lt;p&gt;In terms of technology, I’ve installed &lt;code&gt;llama-cli&lt;&#x2F;code&gt; and am running with enough parallelism to almost saturate the GPU at peak load as measured by &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;metaspartan&#x2F;mactop&quot;&gt;mactop&lt;&#x2F;a&gt;. Beyond that, I haven’t done any additional tuning beyond making sure that we’re caching common prefixes across prompts:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code data-lang=&quot;json&quot;&gt;{     
    &amp;quot;model&amp;quot;: &amp;quot;ggml-org&#x2F;gemma-4-E2B-it-GGUF&amp;quot;,
    &amp;quot;messages&amp;quot;: […],                   
    &amp;quot;cache_prompt&amp;quot;: true,                      
    &amp;quot;max_tokens&amp;quot;: 2048,
    &amp;quot;response_format&amp;quot;: { … }
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;To look at the results I had the agent build me a viewer that lets me browse the iterative simplifications and easily leave individualized feedback on sentences or paragraphs whose outputs look incorrect. The feedback goes into a file that the agent looks at to guide its optimization.&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>LLM Experiments #1: Shorter Sentences</title>
        <published>2026-05-07T12:56:02-04:00</published>
        <updated>2026-05-07T23:32:30-04:00</updated>
        
        <author>
          <name>
            Yuri Vishnevsky
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://yuri.is/writing/shorter-sentences/"/>
        <id>https://yuri.is/writing/shorter-sentences/</id>
        
        <content type="html" xml:base="https://yuri.is/writing/shorter-sentences/">&lt;p&gt;&lt;em&gt;Note:&lt;&#x2F;em&gt; Experimenting with quick writeups as I play around with LLMs using my homegrown agentic workflow system.&lt;&#x2F;p&gt;
&lt;p&gt;Today’s idea (and the workflow system itself) both came from a dream of building a “daily pulse” of the podcast news ecosystem. As part of that I’m exploring ways to concisely present podcast transcripts. In particular, I want to create summaries that preserve the voice of the speakers while still condensing the text.&lt;&#x2F;p&gt;
&lt;p&gt;A summary can be &lt;em&gt;abstractive&lt;&#x2F;em&gt; or &lt;em&gt;extractive&lt;&#x2F;em&gt;: &lt;em&gt;abstractive&lt;&#x2F;em&gt; summaries paraphrase the original text; extractive summaries “extract” by pruning — removing sentences, phrases, or words while preserving structure where possible (see &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;glassmanlab.seas.harvard.edu&#x2F;papers&#x2F;gptsm.pdf&quot;&gt;this paper&lt;&#x2F;a&gt; for a discussion).&lt;&#x2F;p&gt;
&lt;p&gt;For my first attempt I tried a simple two-agent approach with a generator and a verifier. Given a sentence, the generator proposes multiple candidate simplifications, and then the verifier accepts the best one, or rejects them all. This proceeds iteratively until there is no simpler acceptable candidate.&lt;&#x2F;p&gt;
&lt;p&gt;Here’s the first try on a sentence from &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;sive.rs&#x2F;2010-09-techzing&quot;&gt;this podcast episode&lt;&#x2F;a&gt;:
&lt;img src=&quot;https:&#x2F;&#x2F;yuri.is&#x2F;writing&#x2F;shorter-sentences&#x2F;Screenshot%202026-05-07%20at%2022.25.33@2x.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;What’s worth keeping depends on context, so I provide the LLM a conversation snippet to help it judge what’s semantically meaningful:&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;yuri.is&#x2F;writing&#x2F;shorter-sentences&#x2F;Screenshot%202026-05-07%20at%2023.03.28@2x.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;In its current form the algorithm is a bit overeager and can sometimes lop off semantically significant phrases, so I’m already re-running with adjustments based on this initial result, tweaking the prompts and experimenting with how to provide guidance to the model so it can  more accurately tell which parts of the sentence are most important.&lt;&#x2F;p&gt;
&lt;p&gt;I like this idea – it’s an antidote to the tendency of generative AI systems to increase the separation between raw materials and the final result. Instead, we can summarize people in their own words.&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Abraham Lincoln &amp; English as She Is Spoke</title>
        <published>2025-12-14T23:41:28-05:00</published>
        <updated>2025-12-16T05:56:56-05:00</updated>
        
        <author>
          <name>
            Yuri Vishnevsky
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://yuri.is/writing/abraham-lincoln-english-as-she-is-spoke/"/>
        <id>https://yuri.is/writing/abraham-lincoln-english-as-she-is-spoke/</id>
        
        <content type="html" xml:base="https://yuri.is/writing/abraham-lincoln-english-as-she-is-spoke/">&lt;p&gt;Sometimes the threads of the universe weave together in unexpected ways: one of my favorite books, &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;en.wikipedia.org&#x2F;wiki&#x2F;English_as_She_Is_Spoke&quot;&gt;English as She Is Spoke&lt;&#x2F;a&gt;, turns out to have also been enjoyed by Abraham Lincoln. The book is a primer on how to speak English written by somebody who does not speak it themselves. Mark Twain said of it that&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;Nobody can add to the absurdity of this book, nobody can imitate it successfully, nobody can hope to produce its fellow; it is perfect.&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;while&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;Stephen Pile mentions this work in &lt;em&gt;&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;en.wikipedia.org&#x2F;wiki&#x2F;The_Book_of_Heroic_Failures&quot;&gt;The Book of Heroic Failures&lt;&#x2F;a&gt;&lt;&#x2F;em&gt; and comments: “Is there anything in conventional English which could equal the vividness of ‘to craunch a marmoset’?” The original has “to craunch the marmoset”, an entry under the book’s “Idiotisms and Proverbs”. This is the author’s attempt to translate the French slang idiomatic expression croquer le marmot, used to indicate “waiting patiently for someone to open a door”&lt;&#x2F;p&gt;
&lt;p&gt;— &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;en.wikipedia.org&#x2F;wiki&#x2F;English_as_She_Is_Spoke#Cultural_appraisals_and_influence&quot;&gt;Wikipedia&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;But most remarkably, this:&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;In January 1864, then US President &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;en.wikipedia.org&#x2F;wiki&#x2F;Abraham_Lincoln&quot;&gt;Abraham Lincoln&lt;&#x2F;a&gt; and Secretary of State &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;en.wikipedia.org&#x2F;wiki&#x2F;William_H._Seward&quot;&gt;William H. Seward&lt;&#x2F;a&gt; laughed as Lincoln’s private secretary John Hay read aloud from the book. The book has been cited as one example of many diversions that Lincoln used to lighten his heart and mind from the weight of the US Civil War and his cabinet’s political infighting.&lt;&#x2F;p&gt;
&lt;p&gt;— &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;en.wikipedia.org&#x2F;wiki&#x2F;English_as_She_Is_Spoke#Contemporary_allusions&quot;&gt;Wikipedia&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;I folllowed the citation and &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;archive.org&#x2F;details&#x2F;williamhsewardau03sewa&#x2F;page&#x2F;208&#x2F;mode&#x2F;1up&quot;&gt;sure enough&lt;&#x2F;a&gt;, the story is there, in a book chapter on Lincoln’s humor:&lt;&#x2F;p&gt;
&lt;!--The book is William H. Seward: an autobiography from 1801 to 1834. With a memoir of his life, and selections from his letters --&gt;
&lt;blockquote&gt;
&lt;p&gt;One evening, after a day of harassing anxiety and work, the President, accompanied by his private secretary, walked into the library of Seward’s house.&lt;&#x2F;p&gt;
&lt;p&gt;“No, more bad news, I hope, Mr. President?”&lt;&#x2F;p&gt;
&lt;p&gt;“No; nothing further; but Hay happened to find a book that amused us; so I told him we would walk over to Seward’s, and read it to him and have a laugh over it.”&lt;&#x2F;p&gt;
&lt;p&gt;The book was the little Portuguese guide to English conversation, since become famous under the title of “English as She is Spoke.”&lt;&#x2F;p&gt;
&lt;p&gt;As John Hay read aloud its queer inverted sentences, Lincoln and Seward laughed heartily, their minds finding a brief but welcome relief from care.&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;yuri.is&#x2F;writing&#x2F;abraham-lincoln-english-as-she-is-spoke&#x2F;image.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>H2 Morris Counter</title>
        <published>2025-10-23T21:14:53-04:00</published>
        <updated>2025-10-25T09:46:56-04:00</updated>
        
        <author>
          <name>
            Yuri Vishnevsky
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://yuri.is/writing/h2-morris-counter/"/>
        <id>https://yuri.is/writing/h2-morris-counter/</id>
        
        <content type="html" xml:base="https://yuri.is/writing/h2-morris-counter/">&lt;p&gt;I was thinking about the &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;en.wikipedia.org&#x2F;wiki&#x2F;Approximate_counting_algorithm&quot;&gt;Morris counter&lt;&#x2F;a&gt; algorithm and wondered if it would be possible to combine it with the &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;iop.systems&#x2F;blog&#x2F;h2-histogram&#x2F;&quot;&gt;H2 histogram&lt;&#x2F;a&gt; encoding to have more control over the standard deviation of the approximation in exchange for spending more bits to represent the same value range.&lt;&#x2F;p&gt;
&lt;p&gt;It turns out the answer is yes:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code data-lang=&quot;js&quot;&gt;&#x2F;&#x2F; Probabilistic increment based on the bin width at the current counter value
function increment(counter, a, b) {
  return counter + (Math.random() &amp;lt; 1&#x2F;h2(counter, a, b).binWidth)
}

&#x2F;&#x2F; Expected value of the H2 counter
function estimatedValue(counter, a, b) {
    return h2(counter, a, b).lower
}

&#x2F;&#x2F; A mini H2 histogram decoder implementation for values &amp;lt;= 2^32-1.
&#x2F;&#x2F; Returns the lowest value in, and bin size of, the `index`-th bin.
function h2(index, a, b) {
  const c = a + b + 1;
  const binsBelowCutoff = u32(1 &amp;lt;&amp;lt; (c - a));
  if (index &amp;lt; binsBelowCutoff) {
    &#x2F;&#x2F; we&amp;#39;re in the linear section of the histogram; each bin is 2^a wide
    return {
      lower: u32(index &amp;lt;&amp;lt; a),
      binWidth: u32(1 &amp;lt;&amp;lt; a)
    }
  } else {
    &#x2F;&#x2F; we&amp;#39;re in the log section of the histogram; 2^b bins per log segment
    const logSegment = c + ((index - binsBelowCutoff) &amp;gt;&amp;gt;&amp;gt; b);
    const binOffset = index &amp;amp; (u32(1 &amp;lt;&amp;lt; b) - 1);
    return {
      lower: u32(1 &amp;lt;&amp;lt; logSegment) + u32(binOffset &amp;lt;&amp;lt; (logSegment - b)),
      binWidth: u32(1 &amp;lt;&amp;lt; (logSegment - b))
    }
  }
}

function u32(x) {
  return x &amp;gt;&amp;gt;&amp;gt; 0;
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;The idea is that every integer represents a random variable whose expected value is the lower edge of the corresponding histogram bin, since this way the index 0 can represent the value 0. The number of integers that share this encoding is the bin width, so the probability of ascending to the next bin is &lt;em&gt;1 &#x2F; bin width&lt;&#x2F;em&gt;.&lt;&#x2F;p&gt;
&lt;p&gt;The parameter &lt;em&gt;a&lt;&#x2F;em&gt; controls the bin width on the low end of the value range (typically set to 0 to represent small integers exactly), while the parameter &lt;em&gt;b&lt;&#x2F;em&gt; controls relative error on the high end by determining the number of linear subdivisions within the upper log segments. Larger values of &lt;em&gt;b&lt;&#x2F;em&gt; increase the quality of the approximation by increasing the number of bins covering the value range.&lt;&#x2F;p&gt;
&lt;p&gt;When used as the basis for a histogram, the H2 encoding upper-bounds the relative error for large values. But in the context of probabilistic counters the H2 encoding does not prevent the value estimate from being wildly far away from the true value, since an unlikely series of coin flips can cause the value estimate to drift very far away. But, compared to the Morris counter, it &lt;em&gt;does&lt;&#x2F;em&gt; allow you to spend more bits to decrease the expected relative error and variance of the approximation.&lt;&#x2F;p&gt;
&lt;p&gt;As a special case, when &lt;em&gt;a&lt;&#x2F;em&gt; and &lt;em&gt;b&lt;&#x2F;em&gt; are both zero, this counter is almost the same as the standard Morris counter, with the only difference being that the positive estimated values are all exact powers of two.&lt;&#x2F;p&gt;
&lt;p&gt;Here’s the code for a Morris counter for comparison:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code data-lang=&quot;js&quot;&gt;&#x2F;&#x2F; Probabilistic increment as per the Morris counter algorithm
function morrisIncrement(counter) {
  return counter + (Math.random() &amp;lt; 1&#x2F;Math.pow(2, counter))
}

&#x2F;&#x2F; Expected value of the Morris counter
function morrisEstimatedValue(counter) {
  return 2 ** counter - 1
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;References:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;The original paper by Robert Morris is &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;graphics.stanford.edu&#x2F;courses&#x2F;cs321&#x2F;Private&#x2F;Readings&#x2F;p840-morris.pdf&quot;&gt;Counting Large Numbers of Events in Small Registers&lt;&#x2F;a&gt;, published in 1977&lt;&#x2F;li&gt;
&lt;li&gt;The mini H2 encoding above was originally written for &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;iopsystems&#x2F;h2-histogram-js?tab=readme-ov-file&quot;&gt;iopsystems&#x2F;h2-histogram-js&lt;&#x2F;a&gt;.&lt;&#x2F;li&gt;
&lt;li&gt;The reference H2 histogram implementation is in Rust at &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;pelikan-io&#x2F;rustcommon&#x2F;tree&#x2F;main&#x2F;histogram&quot;&gt;rustcommon&#x2F;histogram&lt;&#x2F;a&gt;.&lt;&#x2F;li&gt;
&lt;li&gt;There’s a visual explanation of the &lt;em&gt;a&lt;&#x2F;em&gt; and &lt;em&gt;b&lt;&#x2F;em&gt; histogram parameters in &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;observablehq.com&#x2F;d&#x2F;35f0b601ed888da9&quot;&gt;this notebook&lt;&#x2F;a&gt;.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;&lt;em&gt;Thanks to Sean Lynch, Brian Martin, Yao Yue, and Mihir Nanavati for a discussion that lead to these implementations. Sean made a “&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;swlynch99&#x2F;morris-float&quot;&gt;morris float&lt;&#x2F;a&gt;” which applies the same probabilistic approach directly to the exponent and mantissa of an &lt;code&gt;f32&lt;&#x2F;code&gt;&lt;&#x2F;em&gt;.&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Podcast Summarization</title>
        <published>2025-10-20T22:23:33-04:00</published>
        <updated>2025-10-21T01:17:35-04:00</updated>
        
        <author>
          <name>
            Yuri Vishnevsky
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://yuri.is/writing/podcast-summarization/"/>
        <id>https://yuri.is/writing/podcast-summarization/</id>
        
        <content type="html" xml:base="https://yuri.is/writing/podcast-summarization/">&lt;p&gt;Previously: &lt;a href=&quot;https:&#x2F;&#x2F;yuri.is&#x2F;writing&#x2F;podcast-vibes-presentation&#x2F;&quot;&gt;Podcast Vibes Presentation&lt;&#x2F;a&gt;.&lt;&#x2F;p&gt;
&lt;p&gt;Text summarization is a fundamentally a creative act, at its best preserving the essence of the original work. &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;www.newyorker.com&#x2F;magazine&#x2F;2015&#x2F;09&#x2F;14&#x2F;omission&quot;&gt;Writing is selection&lt;&#x2F;a&gt;, and summarization doubly so. This came to mind today while I was exploring designs for a user interface to substitute for the experience of listening to a podcast.&lt;&#x2F;p&gt;
&lt;p&gt;This is a chronicle of the first hour or so of that exploration, playing around with a transcript from this recent &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;www.youtube.com&#x2F;watch?v=WyJV6VwEGA8&amp;amp;feature=youtu.be&quot;&gt;interview&lt;&#x2F;a&gt;.&lt;&#x2F;p&gt;
&lt;p&gt;At first I wanted to see whether I could simply avoid summarization altogether by avoiding line breaks and using a full-width display, but there’s quite a lot of text in a transcript, so this doesn’t work even with a relatively compact font.&lt;&#x2F;p&gt;
&lt;p&gt;So I tried columns instead:&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;yuri.is&#x2F;writing&#x2F;podcast-summarization&#x2F;Screenshot%202025-10-20%20at%2023.49.15@2x.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;Wrapping the text in columns felt a lot better since I could now move my gaze in a smaller region of space to read a complete thought. But there’s still just too much text.&lt;&#x2F;p&gt;
&lt;p&gt;So I experimented with asking an LLM to take individual sentences and omit unnecessary words.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;yuri.is&#x2F;writing&#x2F;podcast-summarization&#x2F;Screenshot%202025-10-20%20at%2013.23.25@2x.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;Here it is for the whole transcript. Not very useful! But interesting…&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;yuri.is&#x2F;writing&#x2F;podcast-summarization&#x2F;Screenshot%202025-10-20%20at%2013.28.35@2x.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;I used an LLM to split the transcript into topics, and then give each topic a title and description.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;yuri.is&#x2F;writing&#x2F;podcast-summarization&#x2F;Screenshot%202025-10-20%20at%2014.36.28@2x.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;I was initially resisting using an LLM for topic identification because it felt like handing over a lot of power to the system, but the result is much more easily scannable. You can look across the top row to get a sense of conversation, then dive in by scanning down.&lt;&#x2F;p&gt;
&lt;p&gt;A fun idea to evaluate how easily auditable an AI-powered user interface is might be to use adversarial prompts that generate misleading summaries with some probability, in order to see whether I would notice.  Can the cognitive cost of spot checks be made low?&lt;&#x2F;p&gt;
&lt;p&gt;Same design with slightly wider columns:&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;yuri.is&#x2F;writing&#x2F;podcast-summarization&#x2F;Screenshot%202025-10-20%20at%2014.38.59@2x.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;It could be interesting to identify conversational dependencies between transcript sections and use that to enable something like &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;en.wikipedia.org&#x2F;wiki&#x2F;Program_slicing&quot;&gt;program slicing&lt;&#x2F;a&gt;, but for meaning. An example of this idea in a different context is &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;willcrichton&#x2F;flowistry&quot;&gt;Flowistry&lt;&#x2F;a&gt; – imagine being able to click on a topic or sentence to cut down the conversation to the subset of “related” discussion (presumably with some kind of soft relatendess cutoff).&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;yuri.is&#x2F;writing&#x2F;podcast-summarization&#x2F;Screenshot%202025-10-20%20at%2023.23.31@2x.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;Future work:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;Use a change of color or font to differentiate LLM-generated from original source text.&lt;&#x2F;li&gt;
&lt;li&gt;Identify speakers and allow toggling the presence of individual conversational participants.&lt;&#x2F;li&gt;
&lt;li&gt;Try view-aware topic-splitting, where if a topic is too long for the current display, adaptively partition it into subtopics with the goal of maximizing the usage of screen space.&lt;&#x2F;li&gt;
&lt;li&gt;Rather than on topic per column, explore a grid with fixed-height scrollable cells or a &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;blog.scottlogic.com&#x2F;2025&#x2F;09&#x2F;17&#x2F;intro-to-masonry.html&quot;&gt;masonry layout&lt;&#x2F;a&gt;.&lt;&#x2F;li&gt;
&lt;li&gt;How could this generalize to visualizing a set of thematically overlapping podcasts with the same guests (eg. 3-10 podcasts of the same author on a podcast media tour)?&lt;&#x2F;li&gt;
&lt;li&gt;Are there principles from story structure that we could use to structure the presentation of the text?&lt;&#x2F;li&gt;
&lt;li&gt;This project is forcing me to confront squarely the question of what exactly it is that one gets from a podcast. It’s clearly not just factual information: there’s also emotion, allusion, and subtext. What matters?&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;Further reading:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;arxiv.org&#x2F;abs&#x2F;2401.10873&quot;&gt;An AI-Resilient Text Rendering Technique for Reading and Skimming Documents&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;arxiv.org&#x2F;abs&#x2F;2405.08447&quot;&gt;AI-Resilient Interfaces&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Widows, Orphans, and Runts</title>
        <published>2025-10-17T17:45:39-04:00</published>
        <updated>2025-10-17T19:59:04-04:00</updated>
        
        <author>
          <name>
            Yuri Vishnevsky
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://yuri.is/writing/widows-orphans-and-runts/"/>
        <id>https://yuri.is/writing/widows-orphans-and-runts/</id>
        
        <content type="html" xml:base="https://yuri.is/writing/widows-orphans-and-runts/">&lt;p&gt;In redesigning this site I wanted to prevent &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;en.wikipedia.org&#x2F;wiki&#x2F;Widows_and_orphans&quot;&gt;widows&lt;&#x2F;a&gt; in headlines, ensuring that individual overflowing words don’t wrap onto a new line. Fortunately modern CSS has a way to indicate that you want the browser to trade a bit more time for improved text layout: &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;developer.mozilla.org&#x2F;en-US&#x2F;docs&#x2F;Web&#x2F;CSS&#x2F;text-wrap#pretty&quot;&gt;&lt;code&gt;text-wrap: pretty&lt;&#x2F;code&gt;&lt;&#x2F;a&gt;. From MDN:&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;text-wrap: pretty&lt;&#x2F;code&gt; results in the same behavior as wrap, except that the user agent will use a slower algorithm that favors better layout over speed. This is intended for body copy where good typography is favored over performance (for example, when the number of orphans should be kept to a minimum).&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;Amusingly the &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;en.wikipedia.org&#x2F;wiki&#x2F;Widows_and_orphans&quot;&gt;Wikipedia page&lt;&#x2F;a&gt; for widows and orphans states that, at least when it comes to typography, all three of these terms are used somewhat interchangeably:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;Widow (sometimes called orphan)&lt;&#x2F;li&gt;
&lt;li&gt;Orphan (sometimes called widow)&lt;&#x2F;li&gt;
&lt;li&gt;Runt (sometimes called widow or orphan)&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;As of late 2026 &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;caniuse.com&#x2F;?search=text-wrap%3A+pretty%3B&quot;&gt;Can I Use&lt;&#x2F;a&gt; reports around 75% of global browser support for &lt;code&gt;text-wrap: pretty&lt;&#x2F;code&gt;, with the bulk of the holdouts being versions of iOS before iOS 26.&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Data Visualizations are Data Structures</title>
        <published>2025-10-17T16:57:48-04:00</published>
        <updated>2025-10-17T22:12:18-04:00</updated>
        
        <author>
          <name>
            Yuri Vishnevsky
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://yuri.is/writing/data-visualizations-are-data-structures/"/>
        <id>https://yuri.is/writing/data-visualizations-are-data-structures/</id>
        
        <content type="html" xml:base="https://yuri.is/writing/data-visualizations-are-data-structures/">&lt;p&gt;Information is independent of its representation, and any particular data representation makes some things easy and other things hard. The art of data structures — and data visualizations — is choosing the right structure for your circumstances, and tuning it well.&lt;&#x2F;p&gt;
&lt;p&gt;Concepts from the analysis and engineering of data structures, like &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;en.wikipedia.org&#x2F;wiki&#x2F;Big_O_notation&quot;&gt;big &lt;em&gt;O&lt;&#x2F;em&gt;&lt;&#x2F;a&gt; notation and locality in space and time, are also useful in understanding and improving visualizations and visualization systems. The “computer” that a data visualization runs on is the human mind, which has its own performance properties based on the biology of the human perceptual system and the desired information access patterns.&lt;&#x2F;p&gt;
&lt;p&gt;There’s more to say on this analogy, but it will have to wait for a future post.&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Great Singers on Great Singing</title>
        <published>2025-10-14T10:56:14-04:00</published>
        <updated>2025-10-14T20:04:09-04:00</updated>
        
        <author>
          <name>
            Yuri Vishnevsky
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://yuri.is/writing/great-singers-on-great-singing/"/>
        <id>https://yuri.is/writing/great-singers-on-great-singing/</id>
        
        <content type="html" xml:base="https://yuri.is/writing/great-singers-on-great-singing/">&lt;p&gt;Years ago I read a &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;www.amazon.com&#x2F;Great-Singers-Singing-Interviews-Technique&#x2F;dp&#x2F;0879100257&quot;&gt;book&lt;&#x2F;a&gt; where the author interviewed 40 famous opera singers, including Pavarotti, on their technique.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;yuri.is&#x2F;writing&#x2F;great-singers-on-great-singing&#x2F;Screenshot%202025-10-14%20at%2010.57.46@2x.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;I remember being struck by the differences in the ways the singers thought about their craft. While the interviewer didn’t exactly ask everyone the same questions, there was enough overlap that the tensions between them were clear.&lt;&#x2F;p&gt;
&lt;p&gt;What I was most curious about – how different singers answered the same questions about their technique – was not easy to see because of the structure of the book: I wanted the data organized by topic, but the book was organized by interview.&lt;&#x2F;p&gt;
&lt;p&gt;So I spent some time yesterday playing around with seeing if &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;claude.ai&#x2F;&quot;&gt;Claude&lt;&#x2F;a&gt; could do a first pass at this kind of digital-humanities-style data analysis by taking the knowledge from the book and restructuring it into a form where previously hidden connections would be revealed.&lt;&#x2F;p&gt;
&lt;p&gt;I initially thought I would need to do a lot of manual preprocessing, and spent some time figuring out how to segment the body text away from the annotations that appear on each page.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;yuri.is&#x2F;writing&#x2F;great-singers-on-great-singing&#x2F;image.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;!-- {&quot;width&quot;:273} --&gt;&lt;&#x2F;p&gt;
&lt;p&gt;Here’s a visualization I made to help me evaluate how a particular value for the decision boundary would look when applied across more pages.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;yuri.is&#x2F;writing&#x2F;great-singers-on-great-singing&#x2F;Screenshot%202025-10-13%20at%2014.02.50@2x.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;After some more unsatisfactory futzing around I realized that if all I was interested in was an &lt;em&gt;approximate&lt;&#x2F;em&gt; answer, the raw messiness is okay and given the forgiving nature of LLMs, this level of attention to detail was probably just a waste of time.&lt;&#x2F;p&gt;
&lt;p&gt;So I decided to just chuck all the text into Claude to see how it would do.&lt;&#x2F;p&gt;
&lt;p&gt;Here’s Claude’s response to a simple prompt asking it to find and catalog the most striking juxtapositions of disagreement areas between two singers on the same subject. I asked it to reorder the topics so that the topics that are more relatable to non-singers come first, and within each topic, to reorder the items so that they maximally contrast with the items to either side of them.&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;strong&gt;Topic&lt;&#x2F;strong&gt;&lt;&#x2F;th&gt;&lt;th&gt;&lt;strong&gt;Singer&lt;&#x2F;strong&gt;&lt;&#x2F;th&gt;&lt;th&gt;&lt;strong&gt;Position&lt;&#x2F;strong&gt;&lt;&#x2F;th&gt;&lt;th&gt;&lt;strong&gt;Brief Supporting Quote&lt;&#x2F;strong&gt;&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;NATURAL VS. TECHNICAL&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;Rise Stevens&lt;&#x2F;td&gt;&lt;td&gt;Less technical thinking is better&lt;&#x2F;td&gt;&lt;td&gt;“When I did not know about the larynx I was singing better… Some people think so much about all of these technical things that they really mess themselves up”&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;&#x2F;td&gt;&lt;td&gt;Luciano Pavarotti&lt;&#x2F;td&gt;&lt;td&gt;Years of intense technical work required&lt;&#x2F;td&gt;&lt;td&gt;“It took me six years of study… never change ideas… the first five or six months it is very depressing”&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;&#x2F;td&gt;&lt;td&gt;John Alexander&lt;&#x2F;td&gt;&lt;td&gt;Singing is natural like speaking&lt;&#x2F;td&gt;&lt;td&gt;“Singing is simply sustained speech… I feel almost all vocal problems can be solved by shaping the singing technique to conform to the speaking technique”&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;MENTAL FOCUS&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;Birgit Nilsson&lt;&#x2F;td&gt;&lt;td&gt;Think low for high notes&lt;&#x2F;td&gt;&lt;td&gt;“The higher the note goes, the lower the support. The support is as low as possible”&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;&#x2F;td&gt;&lt;td&gt;Martina Arroyo&lt;&#x2F;td&gt;&lt;td&gt;Stay mentally high throughout&lt;&#x2F;td&gt;&lt;td&gt;“You should stay mentally high throughout the entire scale”&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;&#x2F;td&gt;&lt;td&gt;Luciano Pavarotti&lt;&#x2F;td&gt;&lt;td&gt;Position never changes, always high&lt;&#x2F;td&gt;&lt;td&gt;“For me the position never changes… it is high, even when I sing a low note”&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;PLACEMENT&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;Cornell MacNeil&lt;&#x2F;td&gt;&lt;td&gt;Sing backward, placement is nonsense&lt;&#x2F;td&gt;&lt;td&gt;“Placement is nonsense… utter, complete crap. You sing it backward”&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;&#x2F;td&gt;&lt;td&gt;Anna Moffo&lt;&#x2F;td&gt;&lt;td&gt;Extreme forward placement&lt;&#x2F;td&gt;&lt;td&gt;“The place is the most forward, focused sound, complete with overtones”&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;&#x2F;td&gt;&lt;td&gt;James McCracken&lt;&#x2F;td&gt;&lt;td&gt;Warns against over-focusing on mask&lt;&#x2F;td&gt;&lt;td&gt;“If somebody decides he’s going to sing in the mask before he knows how to sing… it’s thin. There’s not enough body resonance”&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;&#x2F;td&gt;&lt;td&gt;Placido Domingo&lt;&#x2F;td&gt;&lt;td&gt;Think way out in front&lt;&#x2F;td&gt;&lt;td&gt;“My thinking of singing is over there… Way out in front of me… I’m not thinking of placing the voice in the mask”&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;&#x2F;td&gt;&lt;td&gt;Paul Plishka&lt;&#x2F;td&gt;&lt;td&gt;Everything in the nose&lt;&#x2F;td&gt;&lt;td&gt;“To me, everything happens here in the nose”&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;SUPPORT&#x2F;APPOGGIO&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;Luciano Pavarotti&lt;&#x2F;td&gt;&lt;td&gt;Push down, compress against cords&lt;&#x2F;td&gt;&lt;td&gt;“Appoggiare means… push down the voice… in a way that it doesn’t move anymore…”&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;&#x2F;td&gt;&lt;td&gt;Cornell MacNeil&lt;&#x2F;td&gt;&lt;td&gt;Floating ribs, diaphragm overrated&lt;&#x2F;td&gt;&lt;td&gt;“I think the diaphragm is enormously overrated… I think it comes more from the floating ribs in the back”&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;&#x2F;td&gt;&lt;td&gt;Rosa Ponselle&lt;&#x2F;td&gt;&lt;td&gt;Lean on diaphragm, push out&lt;&#x2F;td&gt;&lt;td&gt;“Appoggiare… lean on it… Always push out with the stomach and abdomen during the phrase, not in”&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;&#x2F;td&gt;&lt;td&gt;Beverly Sills&lt;&#x2F;td&gt;&lt;td&gt;Both diaphragm AND larynx&lt;&#x2F;td&gt;&lt;td&gt;“How does one exist without the other?… I use that ‘sitting on the diaphragm’ to produce the feeling that I want in my throat”&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;THROAT SPACE&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;John Alexander&lt;&#x2F;td&gt;&lt;td&gt;Same space throughout range&lt;&#x2F;td&gt;&lt;td&gt;“I try to keep as much space as possible, but always the same throughout the entire range of my voice”&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;&#x2F;td&gt;&lt;td&gt;Sherrill Milnes&lt;&#x2F;td&gt;&lt;td&gt;More space going higher&lt;&#x2F;td&gt;&lt;td&gt;“More spacing… More lift of the soft palate as you’re going higher, and… more opening in the throat”&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;&#x2F;td&gt;&lt;td&gt;Luciano Pavarotti&lt;&#x2F;td&gt;&lt;td&gt;Less space in passaggio&lt;&#x2F;td&gt;&lt;td&gt;“I think I give less space when I go through the passaggio, and then more space after I’ve left it”&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;&#x2F;td&gt;&lt;td&gt;Placido Domingo&lt;&#x2F;td&gt;&lt;td&gt;Horizontal to vertical shift&lt;&#x2F;td&gt;&lt;td&gt;“You are having a horizontal sound… to think vertical, in a way… up and down at the same time”&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;OPEN THROAT&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;Cornell MacNeil&lt;&#x2F;td&gt;&lt;td&gt;Meaningless concept&lt;&#x2F;td&gt;&lt;td&gt;“It doesn’t mean anything to me… I think it’s nonsense!”&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;&#x2F;td&gt;&lt;td&gt;Paul Plishka&lt;&#x2F;td&gt;&lt;td&gt;No sensation in throat at all&lt;&#x2F;td&gt;&lt;td&gt;“I don’t know. I know the vocal cords are in the throat, but when I’m singing I have no sensation in my throat at all”&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;&#x2F;td&gt;&lt;td&gt;Magda Olivero&lt;&#x2F;td&gt;&lt;td&gt;Don’t force larynx down&lt;&#x2F;td&gt;&lt;td&gt;“If you teach a young student to push down the larynx… goodbye! That he should do automatically”&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;LARYNX POSITION&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;Bonaldo Giaiotti&lt;&#x2F;td&gt;&lt;td&gt;Firmly lower it all the way&lt;&#x2F;td&gt;&lt;td&gt;“He demonstrated… breathing deeply, which caused his Adam’s apple to descend firmly all the way down”&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;&#x2F;td&gt;&lt;td&gt;Dr. Leo Reckford&lt;&#x2F;td&gt;&lt;td&gt;Minimal movement only (millimeters)&lt;&#x2F;td&gt;&lt;td&gt;“The larynx should make minimal excursions up or down… measured in millimeters… It’s definitely wrong!”&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;&#x2F;td&gt;&lt;td&gt;Louis Quilico&lt;&#x2F;td&gt;&lt;td&gt;Must bring it down independently&lt;&#x2F;td&gt;&lt;td&gt;“To bring down the larynx is not a natural thing… Yes!” (demonstrated independent up&#x2F;down jiggling)&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;&#x2F;td&gt;&lt;td&gt;Pablo Elvira&lt;&#x2F;td&gt;&lt;td&gt;Never pull it down&lt;&#x2F;td&gt;&lt;td&gt;“The larynx should never be pulled down… You should never feel a down pull”&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;&#x2F;td&gt;&lt;td&gt;Sherrill Milnes&lt;&#x2F;td&gt;&lt;td&gt;Low larynx equals open throat&lt;&#x2F;td&gt;&lt;td&gt;“Open throat… low larynx… Those are in a sense the same thing. If your throat is open your larynx is low”&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;&#x2F;td&gt;&lt;td&gt;Cornell MacNeil&lt;&#x2F;td&gt;&lt;td&gt;Don’t think about it at all&lt;&#x2F;td&gt;&lt;td&gt;“As soon as you pull something somewhere, you’ve already created a tension”&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;&#x2F;td&gt;&lt;td&gt;James McCracken&lt;&#x2F;td&gt;&lt;td&gt;Larynx slightly down&lt;&#x2F;td&gt;&lt;td&gt;“Do we agree that an open throat has to do with the larynx being slightly down? Yep”&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;&#x2F;td&gt;&lt;td&gt;Régine Crespin&lt;&#x2F;td&gt;&lt;td&gt;Beginning of yawn sensation&lt;&#x2F;td&gt;&lt;td&gt;“The beginning of the yawn… Be suddenly, quickly astonished… Start breathing by the nose, and be astonished”&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;PASSAGGIO&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;Luciano Pavarotti&lt;&#x2F;td&gt;&lt;td&gt;Squeeze intensely for years&lt;&#x2F;td&gt;&lt;td&gt;“It took me six years… you become cyanotic, red in the face… you must really make the voice more squeezed”&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;&#x2F;td&gt;&lt;td&gt;John Alexander&lt;&#x2F;td&gt;&lt;td&gt;Don’t think about it&lt;&#x2F;td&gt;&lt;td&gt;“The less we think about passaggio, the better off we are”&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;&#x2F;td&gt;&lt;td&gt;Marilyn Horne&lt;&#x2F;td&gt;&lt;td&gt;Squeeze&lt;&#x2F;td&gt;&lt;td&gt;“We really tightened up the middle and especially the passaggio. We squeezed on that passaggio”&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;I spot-checked some entries and confirmed that the quotes I looked at were accurately sourced from the underlying interviews. (I’ve also fixed up some entries manually, and see other problems that I’m not going to bother fixing right now since this is just a quick post.)&lt;&#x2F;p&gt;
&lt;p&gt;Here are some specific ways in which the analysis above, and others like it, can be incomplete:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Non-exhaustive:&lt;&#x2F;strong&gt; There may be correlated omissions due to bias in the combination of LLM and prompt.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Incorrect data:&lt;&#x2F;strong&gt; LLMs generate data that can be hard to verify and the effort required to verify a claim often exceeds the effort required to generate the claim. As Jonathan Swift said, “Falsehood flies, and the Truth comes limping after it.” This can be a &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;news.ycombinator.com&#x2F;item?id=45581029&quot;&gt;big deal&lt;&#x2F;a&gt;.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;“Wrong” topics:&lt;&#x2F;strong&gt; Topics are proxies for the underlying data and ideally they “carve nature at its joints”. But the appropriateness of topics depends on the task at hand – for example, if you’re interested in the underlying factors that drive agreement or disagreement, the LLM-chosen topics might make it harder or easier to see those factors.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;Nonetheless, the level of synthesis here is pretty impressive to me. While the result isn’t of particularly high quality, it nonetheless captures some real patterns from the underlying text that are worth investigating further. A real analysis would take orders of magnitude more time and this exploration at least shows that a table like the above is an interesting way to look at the data.&lt;&#x2F;p&gt;
&lt;p&gt;I think there’s a huge amount of potential in this kind of ad-hoc, task-specific dimensionality reduction, particularly for books and other rich source material, where the same knowledge can be projected into many different structures from which it is possible to much more easily make surprising connections.&lt;&#x2F;p&gt;
&lt;hr &#x2F;&gt;
&lt;h3 id=&quot;appendix-amusing-quotes&quot;&gt;Appendix: Amusing quotes&lt;&#x2F;h3&gt;
&lt;blockquote&gt;
&lt;p&gt;It’s like Sir Thomas Beecham, who said, ‘Most sopranos sound like they’ve been living on seaweed.’&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;Jerome Hines: “Take away my extraordinary size and physique, take away my handsome face and my glorious voice, and what have you got?”&lt;&#x2F;p&gt;
&lt;p&gt;Jan Peerce: “Yeah, what have you got?”&lt;&#x2F;p&gt;
&lt;p&gt;Hines: “Jan Peerce.”&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;Louis sang to calm 149 cows and 18 chickens on a cattle boat crossing the Atlantic. When he finished his recital, “there was a breathless hush… not the sound of a single handclap. Cows don’t clap.”&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>LLMs for Nominative Determinism</title>
        <published>2025-10-12T20:10:26-04:00</published>
        <updated>2025-11-25T12:22:17-05:00</updated>
        
        <author>
          <name>
            Yuri Vishnevsky
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://yuri.is/writing/llms-for-nominative-determinism/"/>
        <id>https://yuri.is/writing/llms-for-nominative-determinism/</id>
        
        <content type="html" xml:base="https://yuri.is/writing/llms-for-nominative-determinism/">&lt;p&gt;As a long-time fan of &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;observablehq.com&#x2F;@yurivish&#x2F;the-long-tail-of-dog-names&quot;&gt;names&lt;&#x2F;a&gt; and &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;en.wikipedia.org&#x2F;wiki&#x2F;Nominative_determinism&quot;&gt;nominative determinism&lt;&#x2F;a&gt;, I wanted to know how well language models could assess the fit between a person’s name and their chosen profession.&lt;&#x2F;p&gt;
&lt;p&gt;To investigate, I gathered a list of 1,000 scientists and asked &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;gemini.google.com&#x2F;app&quot;&gt;Gemini&lt;&#x2F;a&gt; to score each one on how strongly their name connected with their chosen field of study.&lt;&#x2F;p&gt;
&lt;p&gt;Gemini came up with some obvious names such as &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;en.wikipedia.org&#x2F;wiki&#x2F;Jules_Angst&quot;&gt;Jules Angst&lt;&#x2F;a&gt; the mood researcher, &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;en.wikipedia.org&#x2F;wiki&#x2F;Storm_Field&quot;&gt;Storm Field&lt;&#x2F;a&gt; the television meteorologist, and &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;en.wikipedia.org&#x2F;wiki&#x2F;Russell_Brain,_1st_Baron_Brain&quot;&gt;Russell Brain, 1st Baron Brain&lt;&#x2F;a&gt;, who studied brains. But it also claimed some amusing and more puzzling connections, insisting that &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;en.wikipedia.org&#x2F;wiki&#x2F;Paul_Mockapetris&quot;&gt;Paul Mockapetris&lt;&#x2F;a&gt;’s name was directly connected to his invention of &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;en.wikipedia.org&#x2F;wiki&#x2F;Domain_Name_System&quot;&gt;DNS&lt;&#x2F;a&gt;:&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;The name ‘Mockapetris’ has a phonetic similarity to ‘name’, particularly the ‘mock’ sound, which aligns directly with his invention of the Domain Name System (DNS).&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;His name, ‘Mockapetris’, contains ‘petris’ which sounds similar to ‘petrify’ or ‘petrifies’, evoking a sense of making something permanent or foundational, which aligns with his creation of a core internet system.&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;The name ‘Mockapetris’ has a phonetic resemblance to ‘mocked petris’, which could be interpreted as a playful or abstract reference to ‘mocking’ or manipulating ‘petris’ (perhaps stones or structures, analogous to data structures).&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;Then I realized I’d been querying Google’s smallest model, Gemini Flash Lite, rather than their premier model, Gemini Pro. Oops.&lt;&#x2F;p&gt;
&lt;p&gt;A few more amusing ones:&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;en.wikipedia.org&#x2F;wiki&#x2F;Gian_Francesco_Malfatti&quot;&gt;Gian Francesco Malfatti&lt;&#x2F;a&gt;, mathematician:&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;The surname ‘Malfatti’ translates from Italian to ‘badly made’ or ‘ill-made’. His most famous work is the ‘Malfatti problem,’ in which he posed the problem of packing three circles of maximal area into a triangle.&lt;&#x2F;p&gt;
&lt;p&gt;He conjectured a specific, aesthetically pleasing arrangement of three mutually tangent circles. However, his conjecture was later proven to be incorrect; it is not always the optimal solution.&lt;&#x2F;p&gt;
&lt;p&gt;Therefore, Malfatti’s own proposed solution to his namesake problem was, quite literally, ‘badly made’, creating an exceptionally strong and ironic case of nominative determinism.&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;en.wikipedia.org&#x2F;wiki&#x2F;Brian_E._Dalrymple&quot;&gt;Brian E. Dalrymple&lt;&#x2F;a&gt;, inventor of techniques for fingerprint detection:&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;The surname ‘Dalrymple’ bears a striking phonetic resemblance to the phrase ‘dull rimple’.&lt;&#x2F;p&gt;
&lt;p&gt;‘Rimple’ is an archaic but valid term for a wrinkle or fold, which is an excellent description of the ridges that constitute a fingerprint.&lt;&#x2F;p&gt;
&lt;p&gt;Brian Dalrymple’s primary contribution to forensic science was developing techniques to detect latent fingerprints, which are faint, indistinct, or ‘dull’ by nature.&lt;&#x2F;p&gt;
&lt;p&gt;Therefore, his name, ‘Dalrymple’, can be interpreted as describing a ‘dull rimple’ or a faint fingerprint, the very subject of his groundbreaking work.&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;en.wikipedia.org&#x2F;wiki&#x2F;Benoit_Mandelbrot&quot;&gt;Benoit B. Mandelbrot&lt;&#x2F;a&gt; was given a high score, dubiously justified:&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;The name Mandelbrot is of German&#x2F;Yiddish origin, where ‘Mandel’ means ‘almond’ and ‘brot’ means ‘bread’.&lt;&#x2F;p&gt;
&lt;p&gt;The primary object of study for Benoit Mandelbrot was the Mandelbrot set, the most famous of all fractals. The main body of the Mandelbrot set is a cardioid which is distinctly almond-shaped.&lt;&#x2F;p&gt;
&lt;p&gt;Therefore, Mandelbrot, the father of fractal geometry, is most famous for studying an ‘almond bread’ set that has an ‘almond’ shape.&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;I’d say instead that Benoit Mandelbrot is a great example of &lt;em&gt;reverse&lt;&#x2F;em&gt; nominative determinism: following his discovery of fractals, he gave himself a recursive middle initial:&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;Benoît B. Mandelbrot (he added the middle initial himself, though it does not stand for a middle name) was born on Nov. 20, 1924, to a Lithuanian Jewish family in Warsaw.&lt;&#x2F;p&gt;
&lt;p&gt;— &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;www.nytimes.com&#x2F;2010&#x2F;10&#x2F;17&#x2F;us&#x2F;17mandelbrot.html&quot;&gt;New York Times&lt;&#x2F;a&gt;, October 16, 2010&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Tech Notes on Podcast Vibes</title>
        <published>2025-03-15T19:24:39-04:00</published>
        <updated>2025-10-12T13:59:42-04:00</updated>
        
        <author>
          <name>
            Yuri Vishnevsky
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://yuri.is/writing/tech-notes-on-podcast-vibes/"/>
        <id>https://yuri.is/writing/tech-notes-on-podcast-vibes/</id>
        
        <content type="html" xml:base="https://yuri.is/writing/tech-notes-on-podcast-vibes/">&lt;p&gt;I’m winding down my podcast project and wanted to write a note about some of the things it taught me. In short, all of the major tech choices in this project worked out really well, and I’ll probably use this toolkit to prototype future LLM-based processing pipelines.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;go&quot;&gt;Go&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;go.dev&#x2F;&quot;&gt;Go&lt;&#x2F;a&gt; is a great choice for rapid prototyping. I’ve found it particularly useful for side projects since its simplicity makes it easy to context-switch from the other languages I use regularly. Go was a particularly good fit for this project since processing audio with APIs involved making lots of network requests and interfacing the database without doing much compute.&lt;&#x2F;p&gt;
&lt;p&gt;Why not Rust? I’ve done side projects in Rust before and while it is a very powerful tool, it has a lot of mental overhead compared Go, and the type system inhibits certain forms of exploratory programming that I find very valuable.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;postgres-pgx-and-postico&quot;&gt;Postgres, PGX, and Postico&lt;&#x2F;h2&gt;
&lt;p&gt;I used &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;www.postgresql.org&#x2F;&quot;&gt;Postgres&lt;&#x2F;a&gt; for the database with &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;jackc&#x2F;pgx&quot;&gt;PGX&lt;&#x2F;a&gt; as the driver. &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;www.youtube.com&#x2F;watch?v=sXMSWhcHCf8&quot;&gt;PGX Top to Bottom&lt;&#x2F;a&gt; is a talk by the creator of PGX which explains the layered structure of the library, and taught me about a few useful API functions like &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;pkg.go.dev&#x2F;github.com&#x2F;jackc&#x2F;pgx&#x2F;v5#CollectRows&quot;&gt;&lt;code&gt;CollectRows&lt;&#x2F;code&gt;&lt;&#x2F;a&gt;, &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;pkg.go.dev&#x2F;github.com&#x2F;jackc&#x2F;pgx&#x2F;v5#BeginFunc&quot;&gt;&lt;code&gt;BeginFunc&lt;&#x2F;code&gt;&lt;&#x2F;a&gt;, and &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;pkg.go.dev&#x2F;github.com&#x2F;jackc&#x2F;pgx&#x2F;v5#Conn.CopyFrom&quot;&gt;&lt;code&gt;CopyFrom&lt;&#x2F;code&gt;&lt;&#x2F;a&gt;.&lt;&#x2F;p&gt;
&lt;p&gt;This was also my first time trying out &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;eggerapps.at&#x2F;postico2&#x2F;&quot;&gt;Postico&lt;&#x2F;a&gt;, a user interface to Postgres, which I found to be very well-designed. It made it easy to visually inspect the data in my tables and interactively edit schema definitions.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;river&quot;&gt;River&lt;&#x2F;h2&gt;
&lt;p&gt;One lesson from my years at startups is to never build your own job execution engine if you can avoid it.&lt;&#x2F;p&gt;
&lt;p&gt;After looking at various options I decided to try &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;riverqueue.com&#x2F;&quot;&gt;River&lt;&#x2F;a&gt;, an open-source job queueing library, and am very happy I did. It is made by experts with good taste and comes with nice documentation and a well-designed API that exposes convenient abstractions for defining and scheduling jobs. River also comes with a web-based user interface that let me easily monitor execution and inspect job-specific error logs, which made tracking down processing bugs a lot easier since the UI presents the error in the context of other job information, such as its input arguments, total runtime, and number of retries.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;deepgram&quot;&gt;DeepGram&lt;&#x2F;h2&gt;
&lt;p&gt;I used &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;deepgram.com&#x2F;&quot;&gt;Deepgram&lt;&#x2F;a&gt; to transcribe podcasts. They give you $200 of free credits and the transcriptions are surprisingly good. There are cheaper options, but Deepgram is very convenient.&lt;&#x2F;p&gt;
&lt;p&gt;Rather than uploading the audio to Deepgram myself I send them a link to the podcast audio to avoid the extra round-trip. The API endpint I used for this was &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;developers.deepgram.com&#x2F;reference&#x2F;speech-to-text-api&#x2F;listen&quot;&gt;&#x2F;listen&lt;&#x2F;a&gt;:&lt;&#x2F;p&gt;
&lt;p&gt;https:&#x2F;&#x2F;api.deepgram.com&#x2F;v1&#x2F;listen?punctuate=true&amp;amp;paragraphs=true&amp;amp;utterances=true&amp;amp;diarize=true&amp;amp;language=en&amp;amp;summarize=v2&amp;amp;topics=true&amp;amp;model=nova-2&lt;&#x2F;p&gt;
&lt;p&gt;By default, DeepGram will train on any audio you upload in return for a pricing discount. This was not a problem for me since the podcasts I was transcribing are already public, but might be a poor tradeoff for other use cases like personal voice notes. If you want, you can &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;developers.deepgram.com&#x2F;docs&#x2F;the-deepgram-model-improvement-partnership-program#want-to-opt-out&quot;&gt;opt out&lt;&#x2F;a&gt; by adding a specific query parameter to your request.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;claude&quot;&gt;Claude&lt;&#x2F;h2&gt;
&lt;p&gt;Claude did an excellent job at extracting emotionally-laden subjects from the podcast transcripts. The fact that this kind of AI processing is now possible and affordable opens up a lot of opportunities for fun analytics projects.&lt;&#x2F;p&gt;
&lt;p&gt;A few caveats:&lt;&#x2F;p&gt;
&lt;p&gt;While Claude was able to accurately identify subject-emotion pairs, one thing I don’t have as good a sense of is how many subjects were &lt;em&gt;omitted&lt;&#x2F;em&gt; from the extraction since I only did a limited number of spot checks and never created a manually-constructed dataset for comparison, relying instead on my intuitive sense of whether the extracted data was reasonable.&lt;&#x2F;p&gt;
&lt;p&gt;I noticed was that Claude would produce a similar number of results regardless of the length of the provided transcript snippet. I worked around this by feeding it the transcript in small overlapping chunks and de-duplicated the results using a vector similarity heuristic.&lt;&#x2F;p&gt;
&lt;p&gt;Claude’s &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;www.anthropic.com&#x2F;news&#x2F;introducing-citations-api&quot;&gt;citation feature&lt;&#x2F;a&gt; launched while I was working on this project and turned out to be a great fit for connecting the emotional inferences back to the supporting text.&lt;&#x2F;p&gt;
&lt;p&gt;Previous posts in this series:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https:&#x2F;&#x2F;yuri.is&#x2F;writing&#x2F;note-podcast-vibes&#x2F;&quot;&gt;Note: Podcast Vibes&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a href=&quot;https:&#x2F;&#x2F;yuri.is&#x2F;writing&#x2F;podcast-vibes-prototyping&#x2F;&quot;&gt;Podcast Vibes Prototyping&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a href=&quot;https:&#x2F;&#x2F;yuri.is&#x2F;writing&#x2F;podcast-vibes-presentation&#x2F;&quot;&gt;Podcast Vibes Presentation&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Podcast Vibes Presentation</title>
        <published>2025-02-27T21:21:18-05:00</published>
        <updated>2025-12-19T09:34:23-05:00</updated>
        
        <author>
          <name>
            Yuri Vishnevsky
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://yuri.is/writing/podcast-vibes-presentation/"/>
        <id>https://yuri.is/writing/podcast-vibes-presentation/</id>
        
        <content type="html" xml:base="https://yuri.is/writing/podcast-vibes-presentation/">&lt;p&gt;I gave a talk today about my podcast analysis project (previously: &lt;a href=&quot;https:&#x2F;&#x2F;yuri.is&#x2F;writing&#x2F;note-podcast-vibes&#x2F;&quot;&gt;Note: Podcast Vibes&lt;&#x2F;a&gt;, &lt;a href=&quot;https:&#x2F;&#x2F;yuri.is&#x2F;writing&#x2F;podcast-vibes-prototyping&#x2F;&quot;&gt;Podcast Vibes Prototyping&lt;&#x2F;a&gt;), connecting it to some visualization design work from over a decade ago which explored ways to visualize and organize text corpora with vector-based embedding models.&lt;&#x2F;p&gt;
&lt;p&gt;I’ve put annotated slides up &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;archive.yuri.is&#x2F;podcast-vibes.pdf&quot;&gt;here&lt;&#x2F;a&gt;.&lt;&#x2F;p&gt;
&lt;p&gt;This was in 2013 when I was working at an small machine learning startup spun out of the MIT Media Lab, and AI research was just beginning to be taken over by deep learning. I remember being amused when a team member referred to this phenomenon as &lt;em&gt;deep lemming&lt;&#x2F;em&gt;.&lt;&#x2F;p&gt;
&lt;p&gt;Our main visualization was called a &lt;em&gt;concept cloud&lt;&#x2F;em&gt; and it used semantic vectors to make a more meaningful word cloud visualization, in which not only the size but also position &amp;amp; color of the words was used to convey the structure of conceptual relationships in the underlying text.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;yuri.is&#x2F;writing&#x2F;podcast-vibes-presentation&#x2F;concept-cloud.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;This work was done in collaboration with many people on the team, including &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;rspeer&quot;&gt;Elia Robyn Lake&lt;&#x2F;a&gt;, Jason Alonso, &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;kenarnold.org&#x2F;&quot;&gt;Ken Arnold&lt;&#x2F;a&gt;, Avril Kenney, Christina Laverentz, Alice Kaanta, and Andrew Lin.&lt;&#x2F;p&gt;
&lt;p&gt;Writing about this brought back memories of the infamous &lt;em&gt;Ass Headache Problem&lt;&#x2F;em&gt;,  and the time when our stemming pipeline thought that “Emily” was an adverb and “Coca Cola” was the plural of a singular  “Coca Colon”…&lt;&#x2F;p&gt;
&lt;p&gt;Language modeling has come a long way.&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>XOR Doubly-Linked List</title>
        <published>2025-02-06T23:35:13-05:00</published>
        <updated>2025-10-16T00:04:03-04:00</updated>
        
        <author>
          <name>
            Yuri Vishnevsky
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://yuri.is/writing/xor-doubly-linked-list/"/>
        <id>https://yuri.is/writing/xor-doubly-linked-list/</id>
        
        <content type="html" xml:base="https://yuri.is/writing/xor-doubly-linked-list/">&lt;p&gt;A neat idea I learned today:&lt;&#x2F;p&gt;
&lt;p&gt;You can make a doubly-linked list that only needs to store a single pointer – the &lt;code&gt;xor&lt;&#x2F;code&gt; of the &lt;code&gt;prev&lt;&#x2F;code&gt; and &lt;code&gt;next&lt;&#x2F;code&gt; pointers – to allow traversal of the list from either direction.&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;When moving forward we &lt;code&gt;xor&lt;&#x2F;code&gt; current ptr with &lt;code&gt;prev&lt;&#x2F;code&gt;. Moving backward &lt;code&gt;xor&lt;&#x2F;code&gt; with &lt;code&gt;next&lt;&#x2F;code&gt; to get &lt;code&gt;prev&lt;&#x2F;code&gt;. That it. How do we get &lt;code&gt;prev&lt;&#x2F;code&gt;&#x2F;&lt;code&gt;next&lt;&#x2F;code&gt; for &lt;code&gt;xor&lt;&#x2F;code&gt;? The only way to get in the middle of the list is using iterator. Which can hold one extra pointer. For the first&#x2F;last nodes extra pointer is &lt;code&gt;NULL&lt;&#x2F;code&gt;.&lt;&#x2F;p&gt;
&lt;p&gt;— numba888 on &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;news.ycombinator.com&#x2F;item?id=42968557&quot;&gt;Hacker News&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;This concept has a &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;en.m.wikipedia.org&#x2F;wiki&#x2F;XOR_linked_list&quot;&gt;Wikipedia page&lt;&#x2F;a&gt;.&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Podcast Vibes Prototyping</title>
        <published>2025-02-02T21:39:36-05:00</published>
        <updated>2025-03-16T00:18:04-04:00</updated>
        
        <author>
          <name>
            Yuri Vishnevsky
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://yuri.is/writing/podcast-vibes-prototyping/"/>
        <id>https://yuri.is/writing/podcast-vibes-prototyping/</id>
        
        <content type="html" xml:base="https://yuri.is/writing/podcast-vibes-prototyping/">&lt;p&gt;I’m considering turning my little research note on &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;observablehq.com&#x2F;@yurivish&#x2F;podcast-vibes&quot;&gt;podcast vibes&lt;&#x2F;a&gt; into a bit more of a product. The idea would be to track the daily pulse of the podcast ecosystem in terms of the subjects it discusses and the emotions it puts out.&lt;&#x2F;p&gt;
&lt;p&gt;This weekend I played around with a few visualization ideas for looking at the emotional subjects for a single episode at a time.&lt;&#x2F;p&gt;
&lt;p&gt;The picture below shows all of the subjects discussed on a recent podcast interview with a &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;ediblecommunities.com&#x2F;podcasts&#x2F;beans-with-rancho-gordos-steve-sando&#x2F;&quot;&gt;bean enthusiast&lt;&#x2F;a&gt;, with subjects discussed positively colored green and those discussed negatively colored red.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;yuri.is&#x2F;writing&#x2F;podcast-vibes-prototyping&#x2F;image.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;!-- {&quot;width&quot;:644} --&gt;&lt;&#x2F;p&gt;
&lt;p&gt;The annotations are picked at regular samples across the emotional spectrum so that the labels don’t overlap.  I think it would be better to instead draw attention to the most salient discussion points.&lt;&#x2F;p&gt;
&lt;p&gt;Here’s how the same kind of picture looks for a longer podcast episode:&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;yuri.is&#x2F;writing&#x2F;podcast-vibes-prototyping&#x2F;Screenshot%202025-02-02%20at%2021.53.27@2x.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;!-- {&quot;width&quot;:644} --&gt;&lt;&#x2F;p&gt;
&lt;p&gt;And here’s one for a podcast with a much more negative slant. Amusingly, the most positive segments of this one are the ads. While looking at the transcript DeepSeek R1 complimented the host on his ability to smoothly and undetectably transition from the interview into an advertising segment.&lt;&#x2F;p&gt;
&lt;h1 id=&quot;&quot;&gt;&lt;img src=&quot;https:&#x2F;&#x2F;yuri.is&#x2F;writing&#x2F;podcast-vibes-prototyping&#x2F;Screenshot%202025-02-02%20at%2022.09.00@2x.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;!-- {&quot;width&quot;:644} --&gt;&lt;&#x2F;h1&gt;
&lt;p&gt;Here’s how it might look to lay out the dots on a horizontal timeline, with negatives segmented from positives.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;yuri.is&#x2F;writing&#x2F;podcast-vibes-prototyping&#x2F;Screenshot%202025-02-02%20at%2022.11.08@2x.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;Anyway, just some early explorations. Still getting used to publishing this stuff in such an early state, but here we go.&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Note: Podcast Vibes</title>
        <published>2025-01-26T23:02:05-05:00</published>
        <updated>2025-01-27T10:12:40-05:00</updated>
        
        <author>
          <name>
            Yuri Vishnevsky
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://yuri.is/writing/note-podcast-vibes/"/>
        <id>https://yuri.is/writing/note-podcast-vibes/</id>
        
        <content type="html" xml:base="https://yuri.is/writing/note-podcast-vibes/">&lt;p&gt;Here’s a &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;observablehq.com&#x2F;@yurivish&#x2F;podcast-vibes&quot;&gt;research note&lt;&#x2F;a&gt; on my recent explorations into using LLMs to analyze the emotional content of podcasts.&lt;&#x2F;p&gt;
&lt;p&gt;I’m amazed that we have technology that can do this so well.&lt;&#x2F;p&gt;
&lt;p&gt;This project supports my view that one of the most useful features of language models is that they can function as transducers from messy reality to a structured format that can then be dealt with using standard tools that are more efficient, controllable, and well-understood.&lt;&#x2F;p&gt;
&lt;p&gt;It’s also a reminder of just how open the design space is for projects like this, how valuable good tooling for data pipelines is, and the importance of taste.&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>The Structure of the Wavelet Tree</title>
        <published>2025-01-21T15:43:37-05:00</published>
        <updated>2025-01-21T16:52:08-05:00</updated>
        
        <author>
          <name>
            Yuri Vishnevsky
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://yuri.is/writing/the-structure-of-the-wavelet-tree/"/>
        <id>https://yuri.is/writing/the-structure-of-the-wavelet-tree/</id>
        
        <content type="html" xml:base="https://yuri.is/writing/the-structure-of-the-wavelet-tree/">&lt;p&gt;This is a follow-up to &lt;a href=&quot;https:&#x2F;&#x2F;yuri.is&#x2F;writing&#x2F;wavelet-matrix-construction&#x2F;&quot;&gt;Wavelet Matrix Construction&lt;&#x2F;a&gt;.&lt;&#x2F;p&gt;
&lt;p&gt;(I’m working on a larger post about the wavelet matrix and writing and publishing extremely rough drafts as I go.)&lt;&#x2F;p&gt;
&lt;p&gt;To understand the wavelet matrix it can be useful to understand the wavelet tree first, so here’s a quick primer.&lt;&#x2F;p&gt;
&lt;p&gt;As I’ve mentioned before, I think of the wavelet tree (and matrix) as representing &lt;em&gt;the process of sorting&lt;&#x2F;em&gt; a sequence of integer symbols.&lt;&#x2F;p&gt;
&lt;p&gt;Let’s take an example. Imagine that our integer symbols are picked from a universe of size 2³, consisting of the numbers from 0 to 7.&lt;&#x2F;p&gt;
&lt;p&gt;The wavelet tree represents a &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;en.wikipedia.org&#x2F;wiki&#x2F;Quicksort&quot;&gt;quicksort&lt;&#x2F;a&gt; whose pivots are all powers of two. At each level of recursion, the value space is partitioned into two halves based on the universe size, ie. into a lower half containing the numbers 0 to 3, and an upper half containing the numbers 4 to 7. After this first step, each half is recursively sorted using the same approach, terminating when the halves contain a single integer each.&lt;&#x2F;p&gt;
&lt;p&gt;You can see this in the diagram below, where each node is labeled with the range of the as-yet-unsorted integers inside it.&lt;&#x2F;p&gt;
&lt;p&gt;Each level of the diagram corresponds to the next stage of the sorting process. The full sequence of numbers starts in its original order on the top level, made up of symbols from 0 to 7. On the next level those numbers are partitioned into two sublists, with the one on the left containing all of the numbers from 0 to 3 in their original order, and the one on the right containing all numbers from 4 to 7, in their original order.&lt;&#x2F;p&gt;
&lt;p&gt;Then the same process happens again, with the left (0 to 3) node further partitioned into two child nodes, one from 0 to 1 and another from 2 to 3. Then we do this again and arrive at the fully-sorted sequence on the bottom level.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;yuri.is&#x2F;writing&#x2F;the-structure-of-the-wavelet-tree&#x2F;image.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;The diagram above shows the process abstractly without any particular sequence of numbers, but let’s see how this would actually work in practice.  Here’s a visualization that shows the incremental sorting process for the sequence &lt;code&gt;5 2 1 3 7 2 6 1 5 2&lt;&#x2F;code&gt;:&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;yuri.is&#x2F;writing&#x2F;the-structure-of-the-wavelet-tree&#x2F;image%202.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;We start off with the full sequence in its original order on the top layer, and end up with those same numbers sorted in ascending order on the bottom. In between, you see the process of recursive partitioning, where the numbers “slide” down either the left or the right arrow, depending on whether they belong to the bottom or the top half of the universe.&lt;&#x2F;p&gt;
&lt;p&gt;Now, because the entire sequence is represented in full on each level of the sort, you might think this is a very expensive way to represent the data. But in fact, this is the beautiful trick: the wavelet matrix stores not the boxes, but the arrows!&lt;&#x2F;p&gt;
&lt;p&gt;Here’s the same diagram again, this time with the numbers in parentheses notating the data that actually gets written into the wavelet matrix as individual 0- and 1-bits.&lt;&#x2F;p&gt;
&lt;p&gt;The &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;excalidraw.com&#x2F;&quot;&gt;drawing tool&lt;&#x2F;a&gt; I’m using does not allow me to color the individual zeros and ones red or blue but you should think of the the zeros as colored blue, indicating that the number goes down and to the left along the blue arrow, and ones as red, indicating that the number goes down and to the right following the red arrow.
&lt;img src=&quot;https:&#x2F;&#x2F;yuri.is&#x2F;writing&#x2F;the-structure-of-the-wavelet-tree&#x2F;image%203.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;The data in the wavelet matrix consists of a bit vector per level containing the bits in parentheses, With the one small tweak that, rather than writing the bits down from left to right as they’re shown here, we first write down the bits for all the left nodes, followed by the bits for all the right nodes.&lt;&#x2F;p&gt;
&lt;p&gt;If those bits are all you’ve got it might look like we’ve scrambled the numbers beyond all possibility of repair. But we will soon see that it’s easy to reconstruct the original data from this new representation.&lt;&#x2F;p&gt;
&lt;p&gt;The reason for this is that at each level we’ve actually written out bits from the numbers themselves. For example, the number 5 is represented in binary as &lt;code&gt;101&lt;&#x2F;code&gt;, and those are the exact bits written out for each 5 in the sequence, in that order – a 1-bit on the top layer, a 0-bit on the middle layer, and another 1-bit on the bottom layer.&lt;&#x2F;p&gt;
&lt;p&gt;(I’d like to make an animated version of this diagram later, but for now I’m happy since this is already quite an improvement from my last post.)&lt;&#x2F;p&gt;
&lt;p&gt;More to come.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;appendix-scraps&quot;&gt;Appendix: Scraps&lt;&#x2F;h3&gt;
&lt;p&gt;Here are some scraps of visualizations I’m working on that are quick sketches to see whether an idea works. This first one has some promise:
&lt;img src=&quot;https:&#x2F;&#x2F;yuri.is&#x2F;writing&#x2F;the-structure-of-the-wavelet-tree&#x2F;Screenshot%202025-01-21%20at%2016.25.52@2x.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;!-- {&quot;width&quot;:832} --&gt;&lt;&#x2F;p&gt;
&lt;p&gt;This tree view is an interesting idea, too, and inspired the boxy diagrams below:&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;yuri.is&#x2F;writing&#x2F;the-structure-of-the-wavelet-tree&#x2F;Screenshot%202025-01-21%20at%2016.31.32@2x.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;!-- {&quot;width&quot;:455} --&gt;&lt;&#x2F;p&gt;
&lt;p&gt;The next few are a bit misleading because it is not the case that the bottom half of each node sorts to the left:&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;yuri.is&#x2F;writing&#x2F;the-structure-of-the-wavelet-tree&#x2F;Screenshot%202025-01-21%20at%2016.26.47@2x.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;!-- {&quot;width&quot;:584} --&gt;
&lt;img src=&quot;https:&#x2F;&#x2F;yuri.is&#x2F;writing&#x2F;the-structure-of-the-wavelet-tree&#x2F;Screenshot%202025-01-21%20at%2016.28.18@2x.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;!-- {&quot;width&quot;:584} --&gt;&lt;&#x2F;p&gt;
&lt;p&gt;I experimented with showing the flow from the full parent node, but that seems even less enlightening, and I think the arrows in the main post are better  illustrations of the idea.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;yuri.is&#x2F;writing&#x2F;the-structure-of-the-wavelet-tree&#x2F;Screenshot%202025-01-21%20at%2016.29.42@2x.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;!-- {&quot;width&quot;:785} --&gt;&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Wavelet Matrix Construction</title>
        <published>2025-01-20T18:32:05-05:00</published>
        <updated>2025-01-20T21:13:00-05:00</updated>
        
        <author>
          <name>
            Yuri Vishnevsky
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://yuri.is/writing/wavelet-matrix-construction/"/>
        <id>https://yuri.is/writing/wavelet-matrix-construction/</id>
        
        <content type="html" xml:base="https://yuri.is/writing/wavelet-matrix-construction/">&lt;!-- https:&#x2F;&#x2F;observablehq.com&#x2F;d&#x2F;a544ac0ee9c15962 --&gt;
&lt;p&gt;This is a follow-up post to &lt;a href=&quot;https:&#x2F;&#x2F;yuri.is&#x2F;writing&#x2F;enter-the-wavelet-matrix&#x2F;&quot;&gt;Enter the Wavelet Matrix&lt;&#x2F;a&gt;. Compared to my previous entries this one is much more of a stream-of-consciousness that’s not really designed for anyone else to read (though hopefully on the way to a much more coherent and comprehensive explanation!)&lt;&#x2F;p&gt;
&lt;p&gt;&lt;em&gt;Sidenote:&lt;&#x2F;em&gt; This is very much an early draft, and not really understandable on its own, but I’m practicing the art of forging ahead with partial work.&lt;&#x2F;p&gt;
&lt;p&gt;The wavelet matrix can be constructed using an extremely simple algorithm, though explaining the meaning of the resulting structure will take a little bit more work.&lt;&#x2F;p&gt;
&lt;p&gt;The wavelet matrix is a way to represent a sequence of integers, which we will call &lt;em&gt;symbols&lt;&#x2F;em&gt;, by representing a sorting process – the symbols are incrementally sorted by their successive bits, from high to low, using a stable bucket sort, and the incremental results are written down into the wavelet matrix structure, and later used for reconstituting the elements or performing other algorithms such as ranged quantile queries.&lt;&#x2F;p&gt;
&lt;p&gt;Here’s an image illustrating the construction of a wavelet matrix for the sequence &lt;code&gt;1 4 2 3 7&lt;&#x2F;code&gt;. The sequence starts out in its original order on the top layer and moves down incrementally through a series of bitwise bucket sorts that sort the elements into ‘left’ and ’right buckets based on whether the element has a 0-bit or 1-bit on that level. The&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;yuri.is&#x2F;writing&#x2F;wavelet-matrix-construction&#x2F;Screenshot%202025-01-20%20at%2019.46.34@2x.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;The big bold numbers are the symbols&lt;&#x2F;li&gt;
&lt;li&gt;The gray numbers underneath are their bit encodings&lt;&#x2F;li&gt;
&lt;li&gt;This diagram represents three successive applications of a bucket sort, from high bits to low bits. The unsorted elements on the first row rearrange themselves until they’re in their final positions on the bottom row.&lt;&#x2F;li&gt;
&lt;li&gt;This visualization will need to be improved in many ways, but I’m trying to get better at publishing drafts, so here we are.
&lt;ul&gt;
&lt;li&gt;Don’t re-use colors between the numbers and their bits, and the links which represent the data encoded by the matrix&lt;&#x2F;li&gt;
&lt;li&gt;Consider introducing a version of this that sorts according to the wavelet tree rule&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;The data stored in the wavelet matrix is represented by the “pipes” in between the numbers rather than the numbers themselves.&lt;&#x2F;p&gt;
&lt;p&gt;This is what I mean when I say the wavelet matrix represents the process of sorting. It records whether each element “went left” or “went right” in the successive applications of the bucket sort.&lt;&#x2F;p&gt;
&lt;p&gt;The idea behind this picture is that all elements that go left appear before the elements that go right. So for example, the numbers 1, 2, and 3 have a zero for their top bit, and therefore appear on the left of the second row.&lt;&#x2F;p&gt;
&lt;p&gt;The numbers 4 and 7 have a 1 in that top bit, and therefore go right and appear at the end. The numbers that went left are indicated by gray pipes, and the ones right are indicated by black pipes.&lt;&#x2F;p&gt;
&lt;p&gt;The next sorting is based purely on the second bit. The numbers 2 and 3 have a 1 in that position, so they go right.&lt;&#x2F;p&gt;
&lt;p&gt;The same process occurs again on the third layer. Note how 7 was the maximal element on each row indicated and therefore went right on each level.&lt;&#x2F;p&gt;
&lt;p&gt;This is my sixth attempt to try to explain this process, and clearly it’s not working very well, but I will forge ahead, because I think this section does capture something about the practical algorithm I use for large-alphabet wavelet matrices.&lt;&#x2F;p&gt;
&lt;p&gt;I’m beginning to think it’s probably best to start with an introduction to the wavelet tree where the effects of the sort are much more salient, since the bottom row ends up with the symbols sorted by value. In the wavelet matrix, the symbols end up sorted by the bit-reversal of their value.&lt;&#x2F;p&gt;
&lt;hr &#x2F;&gt;
&lt;p&gt;Actually…&lt;&#x2F;p&gt;
&lt;p&gt;Here’s a visualization of the wavelet tree representation of the sequence&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;7 6 5 4 3 2 1
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;yuri.is&#x2F;writing&#x2F;wavelet-matrix-construction&#x2F;Screenshot%202025-01-20%20at%2020.01.54@2x.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;The wavelet tree has a nice hierarchical structure where all of the children stay underneath their parent node, though this is not obvious from these matrix-style visualizations that lose the node borders.&lt;&#x2F;p&gt;
&lt;p&gt;And here’s the wavelet matrix version:&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;yuri.is&#x2F;writing&#x2F;wavelet-matrix-construction&#x2F;Screenshot%202025-01-20%20at%2020.02.49@2x.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;Here we can see that each layer is sorted independently of the previous – ie. other than the stability of the sort, whether the elements on each layer go either left or right purely based on the bit at that level, rather than taking into account the previous levels too (which is what gives the preceding picture its tree structure).&lt;&#x2F;p&gt;
&lt;p&gt;As a side note, the reason the wavelet matrix is an improvement over the wavelet tree is because the wavelet tree requires you to store the node offsets at each level, which scales linearly with the size of the maximum symbol. Without this information, it becomes impossible to navigate from one level of the wavelet tree to the next.&lt;&#x2F;p&gt;
&lt;p&gt;The wavelet matrix is based on this ingenious idea of ordering all of the left children before all of the right children on each level, rather than keeping the child nodes “underneath” the parent. This helps because when you’re on a particular level you can compute the number of left children that precede you and the number of right children that precede you with a simple rank query on the bit vector holding that level. Therefore, navigating to a left child involves looking at the number of zeros to your left, and jumping to that location on the next level. Navigating to the right child involves looking at the number of ones to your left and jumping to that location on the next level (plus the total number of zeros, ie. left children, since all the left children precede the right children).&lt;&#x2F;p&gt;
&lt;p&gt;It’s very clever but hard to explain. Hopefully I can find a nice way to present all of this.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;references&quot;&gt;References&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;See &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;arxiv.org&#x2F;abs&#x2F;2002.08061&quot;&gt;Translating Between Wavelet Tree and Wavelet Matrix Construction&lt;&#x2F;a&gt; for more details on the relationship between the wavelet tree and wavelet matrix, including a discussion of role bit reversal plays in the translation.&lt;&#x2F;li&gt;
&lt;li&gt;See &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;dl.acm.org&#x2F;doi&#x2F;pdf&#x2F;10.1145&#x2F;3457197&quot;&gt;Practical wavelet tree construction&lt;&#x2F;a&gt; (2021) for description of the bucket sort algorithm visualized above (page 31).&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Enter the Wavelet Matrix</title>
        <published>2025-01-15T20:32:08-05:00</published>
        <updated>2025-01-21T21:04:40-05:00</updated>
        
        <author>
          <name>
            Yuri Vishnevsky
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://yuri.is/writing/enter-the-wavelet-matrix/"/>
        <id>https://yuri.is/writing/enter-the-wavelet-matrix/</id>
        
        <content type="html" xml:base="https://yuri.is/writing/enter-the-wavelet-matrix/">&lt;p&gt;Imagine you have an array of integers and want to preprocess it so that you can answer quantile queries, such as computing the median or the 99th percentile, in constant time.&lt;&#x2F;p&gt;
&lt;p&gt;One possible solution is to sort the array and then answer the quantile queries by looking up the relevant entries in the array.&lt;&#x2F;p&gt;
&lt;p&gt;But what if, instead, I asked you to preprocess the array for &lt;em&gt;range&lt;&#x2F;em&gt; quantile queries, where rather than computing the quantile over the entire array, you want to compute quantiles for any sub-slice &lt;code&gt;arr[i..j]&lt;&#x2F;code&gt;  in constant time?&lt;&#x2F;p&gt;
&lt;p&gt;Computing range quantiles involves combining information from both the original order and the sorted order, so sorting the entire sequence in advance no longer suffices. And the &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;en.wikipedia.org&#x2F;wiki&#x2F;Quickselect&quot;&gt;quickselect&lt;&#x2F;a&gt; algorithm, which is often used for computing specific quantiles directly from an unsorted array, does not help us either, since rather than being constant-time its runtime depends on the length of the slice of the array.&lt;&#x2F;p&gt;
&lt;p&gt;Enter the wavelet matrix, a data structure that represents the &lt;em&gt;process&lt;&#x2F;em&gt; of sorting.&lt;&#x2F;p&gt;
&lt;p&gt;The wavelet matrix reorganizes the data in the array in a way that allows answering range quantile queries, and a lot more, in constant time:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;You can access any element of the original sequence in its original order in constant time.&lt;&#x2F;li&gt;
&lt;li&gt;You can access any element of any slice of the array in &lt;em&gt;sorted&lt;&#x2F;em&gt; order in constant time.&lt;&#x2F;li&gt;
&lt;li&gt;You can count the number of times any element occurs in any slice of the array in constant time, which is known as a rank query.&lt;&#x2F;li&gt;
&lt;li&gt;You can count the number of times all elements occur in any slice of the array in constant time with respect to the length of the slice, with the runtime scaling with the number of unique elements that are counted.&lt;&#x2F;li&gt;
&lt;li&gt;You can find the location of any occurrence of any element of the array in close-to-constant time, which is known as a select query.&lt;&#x2F;li&gt;
&lt;li&gt;You can find all of the elements that occur more than &lt;em&gt;X&lt;&#x2F;em&gt;% of the time, in any slice of the array, in an amount of time that depends only on &lt;em&gt;X&lt;&#x2F;em&gt; and not the length of the slice. This is known as a k-majority query.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;This all sounds very attractive, but there are some trade-offs.&lt;&#x2F;p&gt;
&lt;p&gt;The main downsides are that the data representation has some space overhead, on the order of 10-20% of the space required to store the bits of the integers themselves,  and a high constant time factor due to the distributed nature of the data representation. &lt;!-- the time constraint is the more important one and should be more in focus --&gt;&lt;&#x2F;p&gt;
&lt;p&gt;Internally, the wavelet matrix is composed of an array of bit vectors, each representing the bit planes of the original integers, which means that the bits of any specific element are scattered throughout memory and need to be reassembled as queries are executed, which hurts performance due to a lack of data locality and somewhat unpredictable access patterns, which can be somewhat alleviated by batching.&lt;&#x2F;p&gt;
&lt;p&gt;I’d like to explain more about the mechanics of how this thing works in the future, but for now there’s a great paper written by Gonzalo Navarro, one of the co-discoverers of the wavelet matrix, which serves as a great introduction of the basic idea along with a whirlwind of the literature around it. The article is called &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;www.sciencedirect.com&#x2F;science&#x2F;article&#x2F;pii&#x2F;S1570866713000610&quot;&gt;Wavelet Trees for All&lt;&#x2F;a&gt; because the wavelet matrix is actually an optimized data representation for a conceptual tree structure called the wavelet tree. The word &lt;em&gt;wavelet&lt;&#x2F;em&gt; hints at the fact that the integers are decomposed into something like their frequency components, but no actual wavelets are used in the process of making a wavelet tree.&lt;&#x2F;p&gt;
&lt;p&gt;I’ll leave you with a picture I made a while back of an algorithm that constructs a wavelet tree through a series of incremental stable sorts.
&lt;img src=&quot;https:&#x2F;&#x2F;yuri.is&#x2F;writing&#x2F;enter-the-wavelet-matrix&#x2F;image.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;further-reading&quot;&gt;Further reading&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;www.alexbowe.com&#x2F;wavelet-trees&#x2F;&quot;&gt;A nice description of wavelet trees&lt;&#x2F;a&gt; by Alex Bowe&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;users.dcc.uchile.cl&#x2F;~gnavarro&#x2F;ps&#x2F;spire12.4.pdf&quot;&gt;Original wavelet matrix paper&lt;&#x2F;a&gt; by Francisco Claude and Gonzalo Navarro&lt;&#x2F;li&gt;
&lt;li&gt;Paper: &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;dl.acm.org&#x2F;doi&#x2F;fullHtml&#x2F;10.1145&#x2F;3457197&#x2F;&quot;&gt;Practical Wavelet Tree Construction&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;Paper: &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;www.sciencedirect.com&#x2F;science&#x2F;article&#x2F;pii&#x2F;S0304397511009625&#x2F;pdf?md5=32fe86d035e8a0859fd3a4b045e8b36b&amp;amp;pid=1-s2.0-S0304397511009625-main.pdf&quot;&gt;New algorithms on wavelet trees and applications to information retrieval&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;Paper: &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;arxiv.org&#x2F;abs&#x2F;0903.4726&quot;&gt;Range Quantile Queries: Another Virtue of Wavelet Trees&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Gilbert Curve</title>
        <published>2025-01-10T17:03:28-05:00</published>
        <updated>2025-03-23T17:49:07-04:00</updated>
        
        <author>
          <name>
            Yuri Vishnevsky
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://yuri.is/writing/gilbert-curve/"/>
        <id>https://yuri.is/writing/gilbert-curve/</id>
        
        <content type="html" xml:base="https://yuri.is/writing/gilbert-curve/">&lt;p&gt;The &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;mathworld.wolfram.com&#x2F;HilbertCurve.html&quot;&gt;Hilbert Curve&lt;&#x2F;a&gt; is space-filling fractal that, in the limit, will fill any power-of-two-sized square with an increasingly dense squiggle that fills the plane.&lt;&#x2F;p&gt;
&lt;p&gt;The &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;jakubcerveny&#x2F;gilbert&quot;&gt;Gilbert Curve&lt;&#x2F;a&gt; is a generalization of the Hilbert curve to rectangular spaces. The main code is in Python but there are also implementations in &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;jakubcerveny&#x2F;gilbert&#x2F;blob&#x2F;cbc837465e25a95b68a515daba8e4e71d9269bb9&#x2F;ports&#x2F;gilbert.js&quot;&gt;JavaScript&lt;&#x2F;a&gt; and &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;jakubcerveny&#x2F;gilbert&#x2F;tree&#x2F;cbc837465e25a95b68a515daba8e4e71d9269bb9&#x2F;ports&quot;&gt;C&lt;&#x2F;a&gt;.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;yuri.is&#x2F;writing&#x2F;gilbert-curve&#x2F;Screenshot%202025-01-10%20at%2019.38.43@2x.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;!-- {&quot;width&quot;:318} --&gt;&lt;&#x2F;p&gt;
&lt;p&gt;Related: A very fast &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;rawrunprotected&#x2F;hilbert_curves&quot;&gt;C++ Hilbert curve implementation&lt;&#x2F;a&gt; in 2D and 3D (details &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;threadlocalmutex.com&#x2F;?p=126&quot;&gt;here&lt;&#x2F;a&gt; and &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;threadlocalmutex.com&#x2F;?p=149&quot;&gt;here&lt;&#x2F;a&gt;).&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>LLMs Make Context More Valuable</title>
        <published>2024-12-31T16:49:48-05:00</published>
        <updated>2024-12-31T18:58:22-05:00</updated>
        
        <author>
          <name>
            Yuri Vishnevsky
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://yuri.is/writing/llms-make-context-more-valuable/"/>
        <id>https://yuri.is/writing/llms-make-context-more-valuable/</id>
        
        <content type="html" xml:base="https://yuri.is/writing/llms-make-context-more-valuable/">&lt;p&gt;Reader-side LLMs enable information filtering and synthesis to be done on behalf of the reader.&lt;&#x2F;p&gt;
&lt;p&gt;This increases the value of curated contextual information, which can now be sifted through efficiently, and automatically integrated with the main text.&lt;&#x2F;p&gt;
&lt;p&gt;LLMs have issues with provenance and reliability, though, which is why I’m interested in author-provided context, which not only serves as a trust signal but also tames hallucinations, making catastrophic failure less likely.&lt;&#x2F;p&gt;
&lt;p&gt;Existing media formats have lightweight interaction mechanisms such as footnotes and annotations which augment a text with additional information. However, these interactions are usually limited to a small set of pre-determined annotations and footnotes, and the author likely had ideas  they decided were not worth incorporating into the final work.&lt;&#x2F;p&gt;
&lt;p&gt;Similarly, commented code only contains a fragment of its relevant context and rarely explains every design decision. Some design decisions can be inferred from the surrounding context of the code itself, but other information cannot be inferred, and has to come from knowledge of the broader system and its environment.&lt;&#x2F;p&gt;
&lt;p&gt;I wonder what might be a good testing ground for these ideas. One thought is that large programming projects often have public design discussions about the broader tradeoffs involved in particular architectural or feature decisions. And as far as other kinds of writing, the Bible is probably one of the most analyzed and &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;www.amazon.com&#x2F;3-16-Bible-Texts-Illuminated&#x2F;dp&#x2F;0895792524&quot;&gt;annotated&lt;&#x2F;a&gt; pieces of text in human history, though that one’s a bit out of my area of expertise…&lt;&#x2F;p&gt;
&lt;p&gt;See also: &lt;a href=&quot;https:&#x2F;&#x2F;yuri.is&#x2F;writing&#x2F;advanced-essay&#x2F;&quot;&gt;Advanced Essay&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Splash Color Format</title>
        <published>2024-12-25T17:00:51-05:00</published>
        <updated>2025-03-23T17:49:11-04:00</updated>
        
        <author>
          <name>
            Yuri Vishnevsky
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://yuri.is/writing/splash-color-format/"/>
        <id>https://yuri.is/writing/splash-color-format/</id>
        
        <content type="html" xml:base="https://yuri.is/writing/splash-color-format/">&lt;p&gt;&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;www.todepond.com&#x2F;lab&#x2F;splash&#x2F;&quot;&gt;Splash&lt;&#x2F;a&gt; is a color format that reduces the space of choices in a playful way and lends itself to interesting UI design possibilities.&lt;&#x2F;p&gt;
&lt;p&gt;Each color is specified as a three-digit number with one digit per color channel, for a total of one thousand possible colors. For example, &lt;code&gt;407&lt;&#x2F;code&gt; stands for &lt;span class=frac&gt;4&#x2F;9&lt;&#x2F;span&gt; units of red, &lt;span class=frac&gt;0&#x2F;9&lt;&#x2F;span&gt; units of green, and &lt;span class=frac&gt;7&#x2F;9&lt;&#x2F;span&gt; units of blue.&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Voice Note Browser</title>
        <published>2024-12-24T19:37:05-05:00</published>
        <updated>2024-12-31T10:44:22-05:00</updated>
        
        <author>
          <name>
            Yuri Vishnevsky
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://yuri.is/writing/voice-note-browser/"/>
        <id>https://yuri.is/writing/voice-note-browser/</id>
        
        <content type="html" xml:base="https://yuri.is/writing/voice-note-browser/">&lt;p&gt;I like thinking through ideas on a walk and sometimes record voice notes to myself as I go. The result is a collection of related notes, and I wanted a way to see their transcripts together on one page. But to my surprise, couldn’t find any apps that show more than a single transcript at a time.&lt;&#x2F;p&gt;
&lt;p&gt;So I built a little web app to do this using the newly-added &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;support.apple.com&#x2F;guide&#x2F;iphone&#x2F;view-a-transcription-iph00953a982&#x2F;ios&quot;&gt;transcripts&lt;&#x2F;a&gt; in Apple’s Voice Memos, showing the transcripts from the couple of days’ worth of notes in an simple list.&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;I used an Apple Shortcut to copy out the most recent week of voice memos.
&lt;ul&gt;
&lt;li&gt;I originally wanted to use &lt;code&gt;rsync&lt;&#x2F;code&gt;, but ran into permission issues and did not feel sufficiently bold to give my terminal app “Full Disk Access”.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;I used SvelteKit to make the app.
&lt;ul&gt;
&lt;li&gt;The transcript is stored as JSON alongside the audio in a metadata section. Here’s how I parsed it, treating the audio file contents as a utf-8 string:&lt;pre&gt;&lt;code data-lang=&quot;js&quot;&gt;const jsonStart = text.indexOf(`{&amp;quot;attributedString&amp;quot;`);
if (jsonStart &amp;gt; -1) {
  const jsonEnd = text.indexOf(&amp;quot;\x00&amp;quot;, jsonStart);
  const slice = text.slice(jsonStart, jsonEnd);
  const transcript = JSON.parse(slice);
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;I learned that SvelteKit can &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;svelte.dev&#x2F;docs&#x2F;kit&#x2F;load#Streaming-with-promises&quot;&gt;stream promises&lt;&#x2F;a&gt;, making it easy to load individual voice notes asynchronously and incrementally. The index page returns an array of &lt;code&gt;Promise&lt;&#x2F;code&gt;s representing individual notes, and the frontend shows data as it arrives. This was very cool.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;This came together quickly and I’m really happy with it – it’s nice when something so simple can be so useful.&lt;&#x2F;p&gt;
&lt;p&gt;A few ideas for future work:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;Provenance: Make it easy to listen to the underlying audio for the cases when the transcription doesn’t get things right.
&lt;ul&gt;
&lt;li&gt;For example, making a highlight on the page should pop up a little tooltip that lets you listen to the audio corresponding to that part of the transcript.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;Structured extraction: Use LLMs to extract to-dos so that the ideas are easier for me to act upon.
&lt;ul&gt;
&lt;li&gt;It would be nice if there was a sidebar with a bunch of to-do items next to each note.&lt;&#x2F;li&gt;
&lt;li&gt;There might be other kinds of structured extraction that would also be useful. I could imagine a set of local language models making parallel passes over each note with each focused on extracting a particular kind of structure.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Hyparquet</title>
        <published>2024-12-18T21:22:19-05:00</published>
        <updated>2025-03-23T17:48:57-04:00</updated>
        
        <author>
          <name>
            Yuri Vishnevsky
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://yuri.is/writing/hyparquet/"/>
        <id>https://yuri.is/writing/hyparquet/</id>
        
        <content type="html" xml:base="https://yuri.is/writing/hyparquet/">&lt;p&gt;I came across a nice dependency-free library today, &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;hyparam&#x2F;hyparquet&#x2F;&quot;&gt;hyparquet&lt;&#x2F;a&gt;, for parsing Parquet files in JavaScript.&lt;&#x2F;p&gt;
&lt;p&gt;It supports many &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;hyparam&#x2F;hyparquet-compressors&quot;&gt;compression formats&lt;&#x2F;a&gt; including Snappy and Zstd, and reads Parquet files in chunks using &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;developer.mozilla.org&#x2F;en-US&#x2F;docs&#x2F;Web&#x2F;HTTP&#x2F;Range_requests&quot;&gt;range requests&lt;&#x2F;a&gt; rather than loading the entire file. You can read just the metadata, or particular rows and columns.&lt;&#x2F;p&gt;
&lt;p&gt;It’s a good companion to Jeff Heer’s &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;uwdata&#x2F;flechette&quot;&gt;flechette&lt;&#x2F;a&gt;, a lightweight library for reading and writing Arrow files. (&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;idl.uw.edu&#x2F;flechette&#x2F;api&#x2F;&quot;&gt;API reference&lt;&#x2F;a&gt;)&lt;&#x2F;p&gt;
&lt;p&gt;See also: &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;101arrowz&#x2F;fzstd&quot;&gt;fzstd&lt;&#x2F;a&gt;, a small library for decompressing zstd files.&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Advanced Essay</title>
        <published>2024-12-15T22:07:32-05:00</published>
        <updated>2025-03-25T20:25:35-04:00</updated>
        
        <author>
          <name>
            Yuri Vishnevsky
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://yuri.is/writing/advanced-essay/"/>
        <id>https://yuri.is/writing/advanced-essay/</id>
        
        <summary type="html">&lt;p&gt;In the 1970s, decades before computers began to compete at the highest levels of chess, a new variant of the game was invented, called &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;en.wikipedia.org&#x2F;wiki&#x2F;Advanced_chess&quot;&gt;Advanced Chess&lt;&#x2F;a&gt;. The idea was that instead of a human player competing against a computer, the human and machine would play together on the same side.&lt;&#x2F;p&gt;</summary>
        
    </entry>
    <entry xml:lang="en">
        <title>Rust-analyzer can expand macros</title>
        <published>2024-12-13T13:10:12-05:00</published>
        <updated>2025-03-23T17:49:02-04:00</updated>
        
        <author>
          <name>
            Yuri Vishnevsky
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://yuri.is/writing/rust-analyzer-can-expand-macros/"/>
        <id>https://yuri.is/writing/rust-analyzer-can-expand-macros/</id>
        
        <content type="html" xml:base="https://yuri.is/writing/rust-analyzer-can-expand-macros/">&lt;p&gt;If you use an editor with &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;rust-lang&#x2F;rust-analyzer&quot;&gt;Rust’s LSP integration&lt;&#x2F;a&gt;, you can put your cursor on a particular &lt;code&gt;derive&lt;&#x2F;code&gt; macro, such as &lt;code&gt;Clone&lt;&#x2F;code&gt; in &lt;code&gt;#[derive(Clone)]&lt;&#x2F;code&gt;, or an the name of a macro application, such as &lt;code&gt;matches!&lt;&#x2F;code&gt; in &lt;code&gt;matches(foo, bar)&lt;&#x2F;code&gt;, and select “Expand Macros Recursively”, which will open up a side buffer showing the expanded code from that macro.&lt;&#x2F;p&gt;
&lt;p&gt;This is extraordinarily useful when trying to understand what code is generated from a derive, or when debugging an issue with your own macros.&lt;&#x2F;p&gt;
&lt;p&gt;I discovered this through a comment made by &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;davidbarsky&quot;&gt;David Barsky&lt;&#x2F;a&gt; in the &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;rust-lang.zulipchat.com&#x2F;&quot;&gt;Rust Zulip&lt;&#x2F;a&gt;.&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Formatting Code with a Git Hook</title>
        <published>2024-12-11T18:21:38-05:00</published>
        <updated>2025-03-23T17:48:48-04:00</updated>
        
        <author>
          <name>
            Yuri Vishnevsky
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://yuri.is/writing/formatting-code-with-a-git-hook/"/>
        <id>https://yuri.is/writing/formatting-code-with-a-git-hook/</id>
        
        <content type="html" xml:base="https://yuri.is/writing/formatting-code-with-a-git-hook/">&lt;p&gt;Here’s a &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;git-scm.com&#x2F;book&#x2F;ms&#x2F;v2&#x2F;Customizing-Git-Git-Hooks&quot;&gt;git pre-commit hook&lt;&#x2F;a&gt; to auto-format code on commit. It’s useful if you’re working with code whose formatting guidelines differ from the ones you’ve configured in your code editor.&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code data-lang=&quot;sh&quot;&gt;#!&#x2F;bin&#x2F;bash
# .git&#x2F;hooks&#x2F;pre-commit

# Store list of staged files that match your target pattern.
# The `git diff` command returns a list of staged files.
files=$(git diff --cached --name-only | grep -E &amp;#39;\.(js|ts|svelte)$&amp;#39;)

if [ -n &amp;quot;$files&amp;quot; ]; then
    pnpm format
    
    # Add all of the previously staged files back to the staging area
    # to check in the formatted files.
    git add $files
fi

# Exit with an error when there are no longer any staged changes,
# since otherwise git will create an empty commit.
if git diff --cached --quiet; then
    echo &amp;quot;Error: After formatting, there are no longer any files are staged for commit&amp;quot; &amp;gt;&amp;amp;2
    exit 1
fi
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Quicker Netlify Deploys</title>
        <published>2024-12-08T11:16:20-05:00</published>
        <updated>2025-03-23T17:49:38-04:00</updated>
        
        <author>
          <name>
            Yuri Vishnevsky
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://yuri.is/writing/quicker-netlify-deploys/"/>
        <id>https://yuri.is/writing/quicker-netlify-deploys/</id>
        
        <content type="html" xml:base="https://yuri.is/writing/quicker-netlify-deploys/">&lt;p&gt;I use &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;www.netlify.com&#x2F;&quot;&gt;Netlify&lt;&#x2F;a&gt; to host small static sites like this one. It does its job well and is very convenient to use.&lt;&#x2F;p&gt;
&lt;p&gt;But I noticed that since the standard way of setting things up is to link Netlify directly with your Git repo, deploys could take a while. When you push your code, Netlify builds the site on their own infrastructure before deploying it.&lt;&#x2F;p&gt;
&lt;p&gt;I recently &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;www.getzola.org&#x2F;documentation&#x2F;deployment&#x2F;netlify&#x2F;#manual-deploys&quot;&gt;came across&lt;&#x2F;a&gt; a very neat alternative where you can build your site locally and deploy it to Netlify directly with a single HTTP request. This is really convenient when you don’t need the overhead that comes with more careful deploy management.&lt;&#x2F;p&gt;
&lt;p&gt;The link above does a good job of describing the process, but the upshot is that the following command is all you need to have a deploy running live just a few seconds later.&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code data-lang=&quot;bash&quot;&gt;curl -H &amp;quot;Content-Type: application&#x2F;zip&amp;quot; \
     -H &amp;quot;Authorization: Bearer PERSONAL_ACCESS_TOKEN_FROM_STEP_1&amp;quot; \
     --data-binary &amp;quot;@FILE_NAME.zip&amp;quot; \
     https:&#x2F;&#x2F;api.netlify.com&#x2F;api&#x2F;v1&#x2F;sites&#x2F;SITE_NAME.netlify.com&#x2F;deploys
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Right now I use it like this (these build commands go in a &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;casey&#x2F;just&quot;&gt;&lt;code&gt;justfile&lt;&#x2F;code&gt;&lt;&#x2F;a&gt;) to ensure that there’s a corresponding commit for every deployed version:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;pub: no-uncommitted-changes build
  @rm -f dist.zip
  @zip -q -r dist.zip dist;
  @curl -H &amp;quot;Content-Type: application&#x2F;zip&amp;quot; \
       -H &amp;quot;Authorization: Bearer $(cat .netlify-token)&amp;quot; \
        --data-binary &amp;quot;@dist.zip&amp;quot; \
       https:&#x2F;&#x2F;api.netlify.com&#x2F;api&#x2F;v1&#x2F;sites&#x2F;what.yuri.is&#x2F;deploys | jq
  @rm -f dist.zip
  @echo &amp;quot;&amp;quot;
  @echo &amp;quot;Deployed!&amp;quot;

# Halts with an error if the repository contains uncommitted changes
no-uncommitted-changes:
  @ git diff --exit-code &amp;gt; &#x2F;dev&#x2F;null || (echo &amp;quot;Please commit changes to the following files before proceeding:&amp;quot; &amp;amp;&amp;amp; git status --short)

build:
  zola build
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Pareto Frontier</title>
        <published>2024-10-25T22:21:38-04:00</published>
        <updated>2024-12-30T21:48:27-05:00</updated>
        
        <author>
          <name>
            Yuri Vishnevsky
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://yuri.is/writing/pareto-frontier/"/>
        <id>https://yuri.is/writing/pareto-frontier/</id>
        
        <content type="html" xml:base="https://yuri.is/writing/pareto-frontier/">&lt;p&gt;&lt;em&gt;Status: This was written in a hurry. I should find some way to label these with how much effort went into them &#x2F; how much attention should be put on them…&lt;&#x2F;em&gt;&lt;&#x2F;p&gt;
&lt;!-- idea: something like maggie appleton&#x27;s seedling&#x2F;budding&#x2F;evergreen growth stages? --&gt;
&lt;p&gt;The Pareto frontier can help you find a subset of your data points that are the ones that represent trade-offs between maximizing the various dimensions.&lt;&#x2F;p&gt;
&lt;p&gt;Imagine you’re looking at data about thousands of cars, and for each car you know its cost, mileage, and top speed. Maybe you’re trying to get a general sense of which cars perform the best, but this can be difficult when there are multiple attributes along which they can be compared.&lt;&#x2F;p&gt;
&lt;p&gt;All else being equal, though, you want a lower cost, higher mileage, and higher top speed, and it could be the case that some cars are strictly better than others on all three of these aspects. That set of those points is called the &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;en.wikipedia.org&#x2F;wiki&#x2F;Pareto_front&quot;&gt;Pareto frontier&lt;&#x2F;a&gt;.&lt;&#x2F;p&gt;
&lt;p&gt;The Pareto frontier will reveal the points that represent interesting trade-offs: no point on the frontier is strictly better than any other. Of course, there may be important real-world dimensions not captured in your data – perhaps you also care about the size of the car, for example – and in that case, looking at only the frontier of cost, mileage, and top speed can be a dangerous thing since it would cause you to ignore potentially relevant points along those other dimensions or skew your data (since size is correlated with all three dimensions). But if you can quantify these “hidden” aspects and add them to your data, the new higher-dimensional frontier might prove to be a source of insight.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;implementation&quot;&gt;Implementation&lt;&#x2F;h2&gt;
&lt;p&gt;Here’s a simple JavaScript implementation of a Pareto frontier algorithm from the paper &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;www.researchgate.net&#x2F;publication&#x2F;220779603_Fast_Linear_Expected-Time_Algorithms_for_Computing_Maxima_and_Convex_Hulls&quot;&gt;Fast Linear Expected-Time Algorithms for Computing Maxima and Convex Hulls.&lt;&#x2F;a&gt;, coauthored by &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;en.wikipedia.org&#x2F;wiki&#x2F;Jon_Bentley_%28computer_scientist%29&quot;&gt;Jon Bentley&lt;&#x2F;a&gt; of Programming Pearls fame.&lt;&#x2F;p&gt;
&lt;p&gt;It takes an array of points, each represented by an array, and returns the indices of the points that are on the “max” Pareto frontier (intuitively, these points are generally “strictly larger” than the others when considering all dimensions).&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code data-lang=&quot;js&quot;&gt;&#x2F;&#x2F; Return the indices of points on the max Pareto frontier (maxima of a point set) using algorithm
&#x2F;&#x2F; M3 from &amp;quot;Fast Linear Expected-Time Algorithms for Computing Maxima and Convex Hulls&amp;quot;.
&#x2F;&#x2F; Note that if multiple equal points are potentially on the frontier, only one will be returned.
function paretoFrontier(points) {
  const n = points.length;
  if (n === 0) return [];
  let topMax = 0;
  const max = [0];
  for (let i = 1; i &amp;lt; n; i++) {
    let j = 0;
    while (j &amp;lt;= topMax) {
      if (dominates(points[max[j]], points[i])) {
        &#x2F;&#x2F; move max[j] to front of max[1..j]
        const [move] = max.splice(j, 1);
        max.unshift(move);
        j = topMax + 2;
      } else if (dominates(points[i], points[max[j]])) {
        &#x2F;&#x2F; move max[j] to max[topMax]
        const [move] = max.splice(j, 1);
        max.splice(topMax, 0, move);
        topMax -= 1;
      } else if (equals(points[i], points[max[j]])) {
        j = topMax + 2;
      } else {
        &#x2F;&#x2F; pts i, max[k] incomparable
        j += 1;
      }
    }
    if (j === topMax + 1) {
      topMax += 1;
      max[topMax] = i;
    }
  }
  return max.slice(0, topMax + 1);
}

function equals(p1, p2) {
  const k = p1.length;
  for (let i = 0; i &amp;lt; k; i++) {
    if (p1[i] !== p2[i]) return false;
  }
  return true;
}

&#x2F;&#x2F; p1 dominates p2 if it is greater than it in some components and equal
&#x2F;&#x2F; in the remainder (incomparable in no components). Allows for dimensions
&#x2F;&#x2F; s. t. neither p1 nor p2 is less than the other, eg. if the values
&#x2F;&#x2F; are distinct Objects.
function dominates(p1, p2) {
  const k = p1.length;
  let i = 0;
  while (i &amp;lt; k &amp;amp;&amp;amp; p1[i] === p2[i]) i++;
  if (i === k) return false; &#x2F;&#x2F; equal in all dimensions

  for (; i &amp;lt; k; i++) {
    &#x2F;&#x2F; if p2 is greater than p1 in this dimension, then p1 does not dominate p2.
    if (p2[i] &amp;gt; p1[i]) return false;
    &#x2F;&#x2F; if p2 is incomparable to p1 in this dimension, the p1 does not dominate p2.
    if (!(p2[i] &amp;lt; p1[i] || p2[i] === p1[i])) return false;
  }
  return true; &#x2F;&#x2F; p1 dominates p2
}
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Cheerio for Web Scraping</title>
        <published>2024-10-13T19:22:53-04:00</published>
        <updated>2024-12-09T10:09:18-05:00</updated>
        
        <author>
          <name>
            Yuri Vishnevsky
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://yuri.is/writing/cheerio-for-web-scraping/"/>
        <id>https://yuri.is/writing/cheerio-for-web-scraping/</id>
        
        <content type="html" xml:base="https://yuri.is/writing/cheerio-for-web-scraping/">&lt;p&gt;A great JavaScript library has recently reached 1.0: &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;cheerio.js.org&#x2F;&quot;&gt;Cheerio&lt;&#x2F;a&gt;. It gives you a jQuery-like syntax for quickly parsing out content from an HTML string.&lt;&#x2F;p&gt;
&lt;p&gt;I recently used it to parse out some data from a structured list using a nice high-level DSL that’s used by Cheerio’s &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;cheerio.js.org&#x2F;docs&#x2F;advanced&#x2F;extract&quot;&gt;extract&lt;&#x2F;a&gt; function.&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code data-lang=&quot;js&quot;&gt;const $ = cheerio.load(htmlContent);

&#x2F;&#x2F; The page has many of these:
&#x2F;&#x2F; &amp;lt;div class=&amp;quot;ts-segment&amp;quot;&amp;gt;
&#x2F;&#x2F;     &amp;lt;span class=&amp;quot;ts-name&amp;quot;&amp;gt;Jimmy Wales&amp;lt;&#x2F;span&amp;gt; 
&#x2F;&#x2F;     &amp;lt;span class=&amp;quot;ts-timestamp&amp;quot;&amp;gt;&amp;lt;a href=&amp;quot;https:&#x2F;&#x2F;youtube.com&#x2F;watch?v=XXXX&amp;amp;t=5112&amp;quot;&amp;gt;(01:23:45)&amp;lt;&#x2F;a&amp;gt; &amp;lt;&#x2F;span&amp;gt;
&#x2F;&#x2F;     &amp;lt;span class=&amp;quot;ts-text&amp;quot;&amp;gt;Hmm?&amp;lt;&#x2F;span&amp;gt;
&#x2F;&#x2F; &amp;lt;&#x2F;div&amp;gt;

const result = $.extract({
  segments: [{
    selector: &amp;#39;.ts-segment&amp;#39;,
    value: {
      name: &amp;#39;.ts-name&amp;#39;,
      timestamp: {
        selector: &amp;#39;.ts-timestamp&amp;#39;,
      },
      href: {
        selector: &amp;#39;.ts-timestamp a&amp;#39;,
        value: &amp;#39;href&amp;#39;
      },
      text: &amp;#39;.ts-text&amp;#39;,
    },
  }]
});
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;The input HTML content contains many snippets like the one in the comment above. One nice thing about Cheerio’s extract function is that if parts of a composite value are missing, then the entries in each segment will remain undefined while the existing values are extracted. For example, if the &lt;code&gt;name&lt;&#x2F;code&gt; is missing from a particular segment and its selector fails to match, then its &lt;code&gt;timestamp&lt;&#x2F;code&gt;, &lt;code&gt;href&lt;&#x2F;code&gt;, and &lt;code&gt;text&lt;&#x2F;code&gt; will still be extracted.&lt;&#x2F;p&gt;
&lt;p&gt;Running the extraction will return a JavaScript object with the key &lt;code&gt;segments&lt;&#x2F;code&gt;, whose value is an array of objects each with a &lt;code&gt;name&lt;&#x2F;code&gt;, &lt;code&gt;timestamp&lt;&#x2F;code&gt;, &lt;code&gt;href&lt;&#x2F;code&gt;, and &lt;code&gt;text&lt;&#x2F;code&gt;, mirroring the shape of the input.&lt;&#x2F;p&gt;
&lt;p&gt;One nice thing about this interface is its flexibility, which allows you to extract arbitrary properties at multiple levels of a nested tree-like query. The &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;cheerio.js.org&#x2F;docs&#x2F;api&#x2F;interfaces&#x2F;CheerioAPI#extract&quot;&gt;docs&lt;&#x2F;a&gt; for the function aren’t very informative as to what can be extracted but &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;cheerio.js.org&#x2F;docs&#x2F;advanced&#x2F;extract&quot;&gt;this tutorial&lt;&#x2F;a&gt; explains many of the possibilities, including using a function as the extractor.&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Visualizing Sentences with Finite State Automata</title>
        <published>2024-10-07T13:10:38-04:00</published>
        <updated>2025-03-16T00:52:50-04:00</updated>
        
        <author>
          <name>
            Yuri Vishnevsky
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://yuri.is/writing/visualizing-sentences-with-finite-state-automata/"/>
        <id>https://yuri.is/writing/visualizing-sentences-with-finite-state-automata/</id>
        
        <content type="html" xml:base="https://yuri.is/writing/visualizing-sentences-with-finite-state-automata/">&lt;p&gt;The &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;www.cpsc.gov&#x2F;Research--Statistics&#x2F;NEISS-Injury-Data&quot;&gt;National Electronic Injury Surveillance System&lt;&#x2F;a&gt; in the US collects data on product-related injuries. It has a dataset of emergency room visits, with granular information on over 8.5 million visits across 100 hospitals going back over 20 years.&lt;&#x2F;p&gt;
&lt;p&gt;An interesting feature of the data is that each ER visit comes with a narrative story. For example:&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;A 50YOF DROPPED A CRYSTAL BALL ON FOOT, SPRAINED FOOT&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;PATIENT ROLLED OFF BED, HIT EAR ON DRESSER OR ON WOODEN KNOB SHAPED LIKE AN ELEPHANT HEAD AT HOME; EAR LAC.&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;2 NIGHTS AGO A FRIEND MICROWAVED A MARSHMALLOW AND PUT IT ON PT’S NECKPT HAS BURN TO NECK WITH BLISTERING&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;One way to get a qualitative sense of what’s in this dataset is to do some thematic keyword searches. For example, for bicycles:&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;PT FELL OFF BICYCLE AND HIT HEAD&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;PT RIDING BICYCLE AND FELL AND LAC ARM&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;13YOBF FELL OFF BICYCLE;RADIAL HEAD FX&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;Avocados:&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;PT CUTTING AN AVOCADO AND LAC HAND ON KNIFE&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;LAC FINGER ON KNIFE CUTTING AVOCADO&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;CUT WITH KNIFE TO FINGER WHILE CUTTING AVOCADOSDX: LAC FINGER PT&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;Cowboy boots:&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;PT DANCING IN COWBOY BOOTS STRAINED RIGHT ANKLE&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;PT. HAS FOOT DISFIGUREMENT FROM CHRONIC COWBOY BOOT WEARING. DX:FOOT DISFIGUREMENT&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;PATIENT WAS ACCIDENTALLY KICKED IN RIBS PLAYING WITH FRIENDS WHOWORE COWBOY BOOTS. DX. RIGHT RIB CAGE STERNAL SOFT TISSUE.&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;Banana peels:&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;WALKING DOWN HANDICAP RAMP CARRYING BOXES AND STEPPED ON A BANANA PEELAND FELL STRAINING KNEE&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;32 YO FEMALE SLIPPED ON BANANA PEEL AND FELL TO FLOORKNEE STRAIN&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;PT SLIPPED ON BANANA PEEL AND FELL ON FLOOR IN STORE&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;I wanted to visually summarize the variation within sets of similar stories like these, and after tinkering a bit realized that this problem was very similar to a &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;archive.yuri.is&#x2F;pdfing&#x2F;triangulating_the_news.pdf&quot;&gt;collaboration&lt;&#x2F;a&gt; I was a part of a while ago that focused on visualizing variations across a set of lede sentences in newspaper articles. One of the goals there was to reveal authorial&#x2F;media bias, which led us to stay close to the surface texts since phrase ordering and word choice are part of how bias is expressed. In the ER visit stories, though, the texts are written in medicalese and the event semantics matter more than surface syntax, so I think it would be fine to dramatically restructure the text if doing so reveals interesting higher-level patterns. It’s also possible that visualizing something like the temporal sequence of events and their consequences directly will wind up being a good idea here.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;the-story-so-far&quot;&gt;The story so far&lt;&#x2F;h2&gt;
&lt;p&gt;After getting the dataset into shape I spent some time learning about minimal finite state acceptors, which I thought could be a mechanism for merging similar sentence fragments together. FSAs compactly represent a set of strings by reusing suffixes wherever possible:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;Input:
Yuri accidentally went to the store
Ruby deliberately went to the store

Output:
Yuri accidentally ⤵
                   went to the store
Ruby deliberately ⤴
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;blog.burntsushi.net&#x2F;transducers&#x2F;&quot;&gt;This post&lt;&#x2F;a&gt; by Andrew Gallant has a great overview of FSA construction and usage. In the section on &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;blog.burntsushi.net&#x2F;transducers&#x2F;#fsa-construction&quot;&gt;FSA construction&lt;&#x2F;a&gt; Andrew visualizes the algorithm for constructing a minimum finite-state acceptor for a given set of strings. I implemented the algorithm described in the post, which comes from a paper called &lt;a rel=&quot;external&quot; href=&quot;http:&#x2F;&#x2F;www.mitpressjournals.org&#x2F;doi&#x2F;pdfplus&#x2F;10.1162&#x2F;089120100561601&quot;&gt;Incremental Construction of Minimal Acyclic Finite State Automata&lt;&#x2F;a&gt; , and tried it out on some of the stories from the emergency room data.&lt;&#x2F;p&gt;
&lt;p&gt;My plan was to see whether FSAs could be used to capture the variation in a curated set of sentences based on a search term, such AVOCADO or BOOTS.&lt;&#x2F;p&gt;
&lt;p&gt;An issue that came up is that, since each node in an FSA represents a fixed set of strings, finite state automata will only compress strings if they share a suffix. For example, the following two sentences cannot be compressed using an FSA because they differ in their last word:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;Yuri accidentally went to the bar
Ruby deliberately went to the store
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;One way around this is to focus the compression on a local neighborhood around the word of interest (eg. WENT above) and to build two automata: one for the prefix leading up to the word, and another for the reversed suffix of the word. The reversal of the suffix makes it so that the “end” of the FSA is closest to the word of interest, which often leads to a more compact automaton.&lt;&#x2F;p&gt;
&lt;p&gt;For example, here’s a pair of FSAs for BOOTS, visualizing local neighborhoods across 10 sentences:
&lt;img src=&quot;https:&#x2F;&#x2F;yuri.is&#x2F;writing&#x2F;visualizing-sentences-with-finite-state-automata&#x2F;Screenshot%202024-10-07%20at%2013.00.18@2x.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;Here’s BOOTS with 20 sentences:
&lt;img src=&quot;https:&#x2F;&#x2F;yuri.is&#x2F;writing&#x2F;visualizing-sentences-with-finite-state-automata&#x2F;Screenshot%202024-10-07%20at%2013.46.52@2x.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;And here’s BICYCLE with 25 sentences:
&lt;img src=&quot;https:&#x2F;&#x2F;yuri.is&#x2F;writing&#x2F;visualizing-sentences-with-finite-state-automata&#x2F;Screenshot%202024-10-07%20at%2013.02.39@2x.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;It feels like there’s a glimmer of promise here, but I need to do some more thinking about how to proceed.&lt;&#x2F;p&gt;
&lt;p&gt;One idea is to use LLMs to conform the sentences to a more standard structure, then build automata for each “semantic fragment” of the sentence, and visualize the result using something like a parallel coordinates plot with ordinal Y axes.&lt;&#x2F;p&gt;
&lt;p&gt;Another thing I want to try is to identify collocations (multi-word phrases like “christmas tree”) and use those as the elements over which the FSA is built, rather than always simple-mindedly splitting on word boundaries.&lt;&#x2F;p&gt;
&lt;p&gt;There’s also a lot of work to do on visual presentation. The current diagrams are just the raw output from &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;mermaid.js.org&#x2F;&quot;&gt;Mermaid&lt;&#x2F;a&gt; and are hard to read. Perhaps there’s a layout algorithm that is a better match for this family of graphs. My suspicion is that, in the end, the visualization should look significantly less graph-like, with the FSA graph serving as more of an inner structure to guide layout decisions. &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;cs.stackexchange.com&#x2F;questions&#x2F;114676&#x2F;planar-regular-languages&quot;&gt;Related&lt;&#x2F;a&gt;:&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;First there is old work by Book and Chandra, with the following abstract.&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;Summary. It is shown that for every finite-state automaton there exists an equivalent nondeterministic automaton with a planar state graph. However there exist finite-state automata with no equivalent deterministic automaton with a planar state graph.&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;I wonder if it would be reasonable to “planarize” nonplanar FSA graphs by duplicating nodes and edges. I’d like to make it easier to read these from left to right without so much eye movement, and being able to lay sentences out without intersection seems like it would help.&lt;&#x2F;p&gt;
&lt;p&gt;A few things this project reminds me of:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;The data is similar to that underlying some visualizations I worked on for large collections of distributed traces. For that effort we aggregated individual traces into a prefix tree, with edges representing service dependencies. (FSAs are compressed representations of prefix trees).
&lt;ul&gt;
&lt;li&gt;One of the powerful ideas in that project was that we built service-centric views that merged together all of the prefix tree nodes for the target service in postprocessing, which is similar to what I did above in building the prefix trees for the prefix&#x2F;suffix of a local window around the target word.&lt;&#x2F;li&gt;
&lt;li&gt;Both forms of merging result in ambiguous graphs. I think this is fine so long as there is a way to disambiguate interactively – it could let us scale to much larger sentence sets.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;This post on &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;neugierig.org&#x2F;software&#x2F;blog&#x2F;2023&#x2F;07&#x2F;dominator.html&quot;&gt;dominator trees&lt;&#x2F;a&gt;.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;pudding.cool&#x2F;2019&#x2F;02&#x2F;gyllenhaal&#x2F;&quot;&gt;The Gyllenhaal Experiment&lt;&#x2F;a&gt;, which visualizes people’s attempts to spell a famous person’s name:&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;yuri.is&#x2F;writing&#x2F;visualizing-sentences-with-finite-state-automata&#x2F;Screenshot%202024-10-07%20at%2014.47.10@2x.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;!-- {&quot;width&quot;:501} --&gt;&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Creating a Visualization Library in Mathematica</title>
        <published>2024-10-03T21:24:26-04:00</published>
        <updated>2024-12-09T10:09:27-05:00</updated>
        
        <author>
          <name>
            Yuri Vishnevsky
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://yuri.is/writing/creating-a-visualization-library-in-mathematica/"/>
        <id>https://yuri.is/writing/creating-a-visualization-library-in-mathematica/</id>
        
        <content type="html" xml:base="https://yuri.is/writing/creating-a-visualization-library-in-mathematica/">&lt;p&gt;&lt;em&gt;Status: First draft; written in a hurry. Publishing since I want to get better at publishing incremental work and nobody reads these things anyways.&lt;&#x2F;em&gt;&lt;&#x2F;p&gt;
&lt;p&gt;I recently came back to using &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;www.wolfram.com&#x2F;mathematica&#x2F;&quot;&gt;Mathematica&lt;&#x2F;a&gt; as my tool of choice for early-stage prototyping and data exploration. Unlike many other language ecosystems, Mathematica comes with a lot built in, including many data visualization functions. However, plots are scattered throughout the landscape without a high-level system unifying them: every new chart lives in its own world, and there’s nothing resembling &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;link.springer.com&#x2F;book&#x2F;10.1007&#x2F;0-387-28695-0&quot;&gt;The Grammar of Graphics&lt;&#x2F;a&gt; unifying them.&lt;&#x2F;p&gt;
&lt;p&gt;So I decided to see how much work it would take to create a more uniform and flexible plotting system like &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;observablehq.com&#x2F;plot&#x2F;&quot;&gt;Plot&lt;&#x2F;a&gt;, my current tool of choice for browser-based visualizations, in Mathematica. It’s been a few days and I’ve made some progress and have the following picture to show for it:&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;yuri.is&#x2F;writing&#x2F;creating-a-visualization-library-in-mathematica&#x2F;Screenshot%202024-10-03%20at%2021.31.37@2x.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;!-- {&quot;width&quot;:316} --&gt;&lt;&#x2F;p&gt;
&lt;p&gt;Which was made by this code:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code data-lang=&quot;wolfram&quot;&gt;plot[
 dot[
  &amp;quot;x&amp;quot; -&amp;gt; data[[All, 1]],
  &amp;quot;y&amp;quot; -&amp;gt; data[[All, 2]],
  &amp;quot;stroke&amp;quot; -&amp;gt; data[[All, 5]]]]
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;The library works along very similar lines to Observable Plot, which is the existing library I’m most familiar with, and directly inspired this effort.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;technical-notes&quot;&gt;Technical notes&lt;&#x2F;h2&gt;
&lt;p&gt;The following was written in something of a hurry, and I plan to refine it later.&lt;&#x2F;p&gt;
&lt;p&gt;The basic idea is that a plot is a collection of marks overlaid atop each other. (This plot has one mark, &lt;code&gt;dot&lt;&#x2F;code&gt;.)&lt;&#x2F;p&gt;
&lt;p&gt;A plot encodes meaning through visual &lt;em&gt;channels&lt;&#x2F;em&gt;, such as &lt;em&gt;x&lt;&#x2F;em&gt; or &lt;em&gt;y&lt;&#x2F;em&gt; or &lt;em&gt;color&lt;&#x2F;em&gt;. Each mark provides data to some number of channels. Based on this data, together with preferences provided by the user, the plot function comes up with an &lt;em&gt;encoding&lt;&#x2F;em&gt; for each channel that maps from the data domain to the visual domain (eg. taking a category label such as “a” and turning it into a color, or taking a numeric data value and turning it into a position along the x-axis.&lt;&#x2F;p&gt;
&lt;p&gt;The usability of the library depends strongly on the amount of deduction the library can make for you, and its ability to add explanatory marks to the plot, such as legends and axes, so that the resulting picture can be made sense of.&lt;&#x2F;p&gt;
&lt;p&gt;An interesting point that I had not sufficiently appreciated is the importance of representing scales in a way that allows you to make these explanatory marks. For example, the library needs to be able to tell when a log scale is used so that it can generate logarithmic axis ticks. Another example is that in Plot, the “shorthand” field specification syntax is what allows the library to know how to label your axes. A possibility in Mathematica is to use controlled evaluation for the same purpose.&lt;&#x2F;p&gt;
&lt;p&gt;This also raises some questions about evaluation order. I was hoping to use regular marks to build the axis legends, but had to figure out a solution to the time-ordering problem: Normal marks determine the set of inputs for each channel, from which scales for each channel are inferred, and ticks generated. But axis marks need to know the tick values. My current solution is to allow a mark to be a function which will be invoked with scale information. Any points plotted by such marks will not be used to determine the domain of the plot.&lt;&#x2F;p&gt;
&lt;p&gt;Another Plot design point that I hadn’t fully appreciated is that one reason it uses a &lt;em&gt;z&lt;&#x2F;em&gt; channel to allow eg. the line mark to make multiple lines is because sometimes one data column wants to be plotted as multiple curves, eg. with categorical time series, where you don’t want the user to need to reshape their data to calling your mark function.&lt;&#x2F;p&gt;
&lt;p&gt;My implementation so far is about 250 lines of code, and doesn’t yet include the ability to make bar charts, does not include faceting, or support for something like Plot’s &lt;em&gt;z&lt;&#x2F;em&gt; channel. I’m also not yet sure about the best way to support interactive selection, highlighting, and animated transitions. But you can use continuous, ordinal, and categorical encodings to plot text, dots, and lines, with scale and tick inference and a composable mark system.&lt;&#x2F;p&gt;
&lt;p&gt;We’ll see if my current design survives the addition of any of these new features. Part of the pleasure of using such a high-level programming language is that rewrites are cheap: when you know what you want to say, and if performance isn’t too much of a concern, you can express yourself very concisely.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;references&quot;&gt;References&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;observablehq.com&#x2F;plot&#x2F;&quot;&gt;Plot&lt;&#x2F;a&gt; – My favorite plotting library, from the folks behind &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;d3js.org&#x2F;&quot;&gt;d3&lt;&#x2F;a&gt;.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;www.wolfram.com&#x2F;broadcast&#x2F;video.php?c=104&amp;amp;p=10&amp;amp;v=3238&quot;&gt;Wolfram Videos: ggplot: A Grammar of Graphics for the Wolfram Language&lt;&#x2F;a&gt; – I came across this while writing this note. Haven’t watched it yet.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;appendix-code&quot;&gt;Appendix: Code&lt;&#x2F;h2&gt;
&lt;pre&gt;&lt;code data-lang=&quot;wolfram&quot;&gt;(* Names of all channels *)
channelNames = {&amp;quot;x&amp;quot;, &amp;quot;y&amp;quot;, &amp;quot;r&amp;quot;, &amp;quot;color&amp;quot;, &amp;quot;opacity&amp;quot;, &amp;quot;fx&amp;quot;, &amp;quot;fy&amp;quot;};
(* Mapping from option names to channel names *)
optionChannels = &amp;lt;|&amp;quot;x&amp;quot; -&amp;gt; &amp;quot;x&amp;quot;, &amp;quot;x1&amp;quot; -&amp;gt; &amp;quot;x&amp;quot;, &amp;quot;x2&amp;quot; -&amp;gt; &amp;quot;x&amp;quot;, &amp;quot;y&amp;quot; -&amp;gt; &amp;quot;y&amp;quot;, 
   &amp;quot;y1&amp;quot; -&amp;gt; &amp;quot;y&amp;quot;, &amp;quot;y2&amp;quot; -&amp;gt; &amp;quot;y&amp;quot;, &amp;quot;r&amp;quot; -&amp;gt; &amp;quot;r&amp;quot;, &amp;quot;stroke&amp;quot; -&amp;gt; &amp;quot;color&amp;quot;, 
   &amp;quot;fill&amp;quot; -&amp;gt; &amp;quot;color&amp;quot;, &amp;quot;opacity&amp;quot; -&amp;gt; &amp;quot;opacity&amp;quot;, 
   &amp;quot;strokeOpacity&amp;quot; -&amp;gt; &amp;quot;opacity&amp;quot;, &amp;quot;fillOpacity&amp;quot; -&amp;gt; &amp;quot;opacity&amp;quot;, 
   &amp;quot;fx&amp;quot; -&amp;gt; &amp;quot;fx&amp;quot;, &amp;quot;fy&amp;quot; -&amp;gt; &amp;quot;fy&amp;quot;|&amp;gt;;

(* Converts its argument to a list, if it isn&amp;#39;t one already *)
toList[x_] := If[ListQ[x], x, {x}]

(* &amp;quot;Broadcasts&amp;quot; scalar values in an association to lists, so the \
result is tabular in structure. *)
toTable[assoc_] := 
 AssociationThread[
  Keys[assoc] -&amp;gt; Map[toList, Transpose[Thread[Values[assoc]]]]]

(* Normalization functions to rescale a domain to [0, 1] in the case \
of continuous and ordinal, or to [0, n] in the case of indexed *)
continuous[domain_] := Rescale[#, MinMax[domain]] &amp;amp;
indexed[domain_] := 
 AssociationThread[domain -&amp;gt; Range[Length[domain]]]
ordinal[domain_] := 
 indexed[domain]&#x2F;Length[domain] - 1&#x2F;(2 Length[domain])

(* Map an association to another association: &amp;lt;| k-&amp;gt;v |&amp;gt; -&amp;gt; &amp;lt;| \
k-&amp;gt;f[k,v ]|&amp;gt; *)
(* This is a bit confusing, but have a look at its usage in mark \
render functions. *)
kvMap[f_, assoc___] := 
 AssociationMap[First[#] -&amp;gt; f[First[#], Last[#]] &amp;amp;, assoc]

(* Applies the appropriate scale function to each option in assocs *)
scaleMap[scale_, assocs__] := kvMap[
  Map[scale[optionChannels[#]][&amp;quot;apply&amp;quot;], #2] &amp;amp;,
  toTable[Association[assocs]]]

(* Returns a list of k-&amp;gt;v where k is the channel name and v is a list \
of values *)
markChannelValues[mark_] := 
 mark &#x2F;&#x2F; List &#x2F;&#x2F; Prepend[optionChannels] &#x2F;&#x2F; KeyIntersection &#x2F;&#x2F; 
     Values &#x2F;&#x2F; Transpose &#x2F;&#x2F; MapApply[#1 -&amp;gt; toList[#2] &amp;amp;] &#x2F;&#x2F; 
  KeyDrop[Keys[mark[&amp;quot;scales&amp;quot;]]]

(* Dot *)
render[dot, opts_, scale_, ctx_] := Module[{
      m = 
    scaleMap[scale, &amp;lt;|&amp;quot;x&amp;quot; -&amp;gt; 0, &amp;quot;y&amp;quot; -&amp;gt; 0|&amp;gt;, 
     KeyTake[opts, {&amp;quot;x&amp;quot;, &amp;quot;y&amp;quot;, &amp;quot;r&amp;quot;, &amp;quot;stroke&amp;quot;}]]},
    MapThread[
   Style[Circle[#1, #2], #3] &amp;amp;, {Transpose[
     ctx[&amp;quot;dims&amp;quot;]*{m[&amp;quot;x&amp;quot;], m[&amp;quot;y&amp;quot;]}], m[&amp;quot;r&amp;quot;], m[&amp;quot;stroke&amp;quot;]}]]
dot[opts___] := Association[Options[dot], opts]
Options[dot] = {&amp;quot;type&amp;quot; -&amp;gt; dot, &amp;quot;fillOpacity&amp;quot; -&amp;gt; 0.1, 
   &amp;quot;strokeOpacity&amp;quot; -&amp;gt; 0.5, &amp;quot;r&amp;quot; -&amp;gt; 1, &amp;quot;stroke&amp;quot; -&amp;gt; Automatic, 
   &amp;quot;scales&amp;quot; -&amp;gt; &amp;lt;||&amp;gt;};

(* Line *)
render[line, opts_, scale_, ctx_] := Module[{
   m = scaleMap[scale, KeyTake[opts, {&amp;quot;x&amp;quot;, &amp;quot;y&amp;quot;, &amp;quot;stroke&amp;quot;}]]},
    (* For now we draw a single curve with a single color. *)
    Style[Line[Transpose[ctx[&amp;quot;dims&amp;quot;]*{m[&amp;quot;x&amp;quot;], m[&amp;quot;y&amp;quot;]}]], 
   First[m[&amp;quot;stroke&amp;quot;]]]]
line[opts___] := Association[Options[line], opts]
Options[line] = {&amp;quot;type&amp;quot; -&amp;gt; line, &amp;quot;x&amp;quot; -&amp;gt; 0, &amp;quot;y&amp;quot; -&amp;gt; 0, 
   &amp;quot;fillOpacity&amp;quot; -&amp;gt; 0.3, &amp;quot;stroke&amp;quot; -&amp;gt; Automatic, &amp;quot;scales&amp;quot; -&amp;gt; &amp;lt;||&amp;gt;};

(* Text *)
render[text, opts_, scale_, ctx_] := Module[{
   m = toTable[Association[
      scaleMap[scale, KeyTake[opts, {&amp;quot;x&amp;quot;, &amp;quot;y&amp;quot;, &amp;quot;stroke&amp;quot;}]],
      KeyTake[opts, {&amp;quot;dx&amp;quot;, &amp;quot;dy&amp;quot;}],
      &amp;lt;|&amp;quot;align&amp;quot; -&amp;gt; opts[&amp;quot;align&amp;quot;]|&amp;gt;]]},
    MapThread[
   Style[Text[#1, #2, #3], #4] &amp;amp;,
   {opts[&amp;quot;text&amp;quot;], 
    Transpose[ctx[&amp;quot;dims&amp;quot;]*{m[&amp;quot;x&amp;quot;], m[&amp;quot;y&amp;quot;]} + {m[&amp;quot;dx&amp;quot;], m[&amp;quot;dy&amp;quot;]}], 
    m[&amp;quot;align&amp;quot;], m[&amp;quot;stroke&amp;quot;]}]]
text[opts___] := Association[Options[text], opts]
Options[text] = {&amp;quot;type&amp;quot; -&amp;gt; text, &amp;quot;x&amp;quot; -&amp;gt; 0, &amp;quot;y&amp;quot; -&amp;gt; 0, &amp;quot;dx&amp;quot; -&amp;gt; 0, 
   &amp;quot;dy&amp;quot; -&amp;gt; 0, &amp;quot;align&amp;quot; -&amp;gt; Center, &amp;quot;stroke&amp;quot; -&amp;gt; Automatic, &amp;quot;scales&amp;quot; -&amp;gt; &amp;lt;||&amp;gt;};

(* Link *)
render[links, opts_, scale_, ctx_] := Module[{
   m = Association[
     scaleMap[scale, 
      KeyTake[opts, {&amp;quot;x1&amp;quot;, &amp;quot;y1&amp;quot;, &amp;quot;x2&amp;quot;, &amp;quot;y2&amp;quot;, &amp;quot;stroke&amp;quot;}]],
     KeyTake[opts, {&amp;quot;dx1&amp;quot;, &amp;quot;dx2&amp;quot;, &amp;quot;dy1&amp;quot;, &amp;quot;dy2&amp;quot;}]],
    p1, p2, xy
   },
    p1 = 
   Transpose[ctx[&amp;quot;dims&amp;quot;]*{m[&amp;quot;x1&amp;quot;], m[&amp;quot;y1&amp;quot;]} + {m[&amp;quot;dx1&amp;quot;], m[&amp;quot;dy1&amp;quot;]}];
    p2 = 
   Transpose[ctx[&amp;quot;dims&amp;quot;]*{m[&amp;quot;x2&amp;quot;], m[&amp;quot;y2&amp;quot;]} + {m[&amp;quot;dx2&amp;quot;], m[&amp;quot;dy2&amp;quot;]}];
    Thread[Style[Map[Line, Thread[List[p1, p2]]]], m[&amp;quot;stroke&amp;quot;]]]
Options[links] = {&amp;quot;type&amp;quot; -&amp;gt; links, &amp;quot;x1&amp;quot; -&amp;gt; 0, &amp;quot;y1&amp;quot; -&amp;gt; 0, &amp;quot;x2&amp;quot; -&amp;gt; 0, 
   &amp;quot;y2&amp;quot; -&amp;gt; 0, &amp;quot;dx1&amp;quot; -&amp;gt; 0, &amp;quot;dx2&amp;quot; -&amp;gt; 0, &amp;quot;dy1&amp;quot; -&amp;gt; 0, &amp;quot;dy2&amp;quot; -&amp;gt; 0, 
   &amp;quot;stroke&amp;quot; -&amp;gt; Automatic, &amp;quot;scales&amp;quot; -&amp;gt; &amp;lt;||&amp;gt;};
links[opts___] := Association[Options[links], opts]

(* Axes are given as function marks, which accept the scales as an \
argument, and whose values do not inform the scale domains. *)
(* This solves the order-of-operations problem where the domain is \
inferred from marks, and wants to be &amp;quot;visualized&amp;quot; by axis marks. *)
(* We still need a way to signal to Plot to not add its own axes if \
the user provided some; maybe with some magic options. *)
xAxis[] :=
 If[KeyExistsQ[#, &amp;quot;x&amp;quot;] &amp;amp;&amp;amp; Length[#[&amp;quot;x&amp;quot;][&amp;quot;domain&amp;quot;]] &amp;gt; 0,
   {text[&amp;quot;x&amp;quot; -&amp;gt; #[&amp;quot;x&amp;quot;][&amp;quot;ticks&amp;quot;], &amp;quot;y&amp;quot; -&amp;gt; 0, 
     &amp;quot;text&amp;quot; -&amp;gt; Map[NumberForm[#, {Infinity, 1}] &amp;amp;, #[&amp;quot;x&amp;quot;][&amp;quot;ticks&amp;quot;]], 
     &amp;quot;dy&amp;quot; -&amp;gt; -18, &amp;quot;stroke&amp;quot; -&amp;gt; Black, &amp;quot;align&amp;quot; -&amp;gt; Top, 
     &amp;quot;scales&amp;quot; -&amp;gt; &amp;lt;|&amp;quot;y&amp;quot; -&amp;gt; &amp;lt;|&amp;quot;apply&amp;quot; -&amp;gt; Identity|&amp;gt;, 
       &amp;quot;color&amp;quot; -&amp;gt; &amp;lt;|&amp;quot;apply&amp;quot; -&amp;gt; Identity|&amp;gt;|&amp;gt;],
    links[&amp;quot;x1&amp;quot; -&amp;gt; #[&amp;quot;x&amp;quot;][&amp;quot;ticks&amp;quot;], &amp;quot;x2&amp;quot; -&amp;gt; #[&amp;quot;x&amp;quot;][&amp;quot;ticks&amp;quot;], &amp;quot;y1&amp;quot; -&amp;gt; 0,
      &amp;quot;y2&amp;quot; -&amp;gt; 0, &amp;quot;dy1&amp;quot; -&amp;gt; -12, &amp;quot;dy2&amp;quot; -&amp;gt; -16, 
     &amp;quot;scales&amp;quot; -&amp;gt; &amp;lt;|&amp;quot;y&amp;quot; -&amp;gt; &amp;lt;|&amp;quot;apply&amp;quot; -&amp;gt; Identity|&amp;gt;, 
       &amp;quot;color&amp;quot; -&amp;gt; &amp;lt;|&amp;quot;apply&amp;quot; -&amp;gt; (Black &amp;amp;)|&amp;gt;|&amp;gt;]}, {}] &amp;amp;

yAxis[] := 
 If[KeyExistsQ[#, &amp;quot;y&amp;quot;] &amp;amp;&amp;amp; 
    Length[#[&amp;quot;y&amp;quot;][&amp;quot;domain&amp;quot;]] &amp;gt; 0, {text[&amp;quot;x&amp;quot; -&amp;gt; 0, 
     &amp;quot;y&amp;quot; -&amp;gt; #[&amp;quot;y&amp;quot;][&amp;quot;ticks&amp;quot;], 
     &amp;quot;text&amp;quot; -&amp;gt; Map[NumberForm[#, {Infinity, 1}] &amp;amp;, #[&amp;quot;y&amp;quot;][&amp;quot;ticks&amp;quot;]], 
     &amp;quot;dx&amp;quot; -&amp;gt; -20, &amp;quot;stroke&amp;quot; -&amp;gt; Black, &amp;quot;align&amp;quot; -&amp;gt; Right, 
     &amp;quot;scales&amp;quot; -&amp;gt; &amp;lt;|&amp;quot;x&amp;quot; -&amp;gt; &amp;lt;|&amp;quot;apply&amp;quot; -&amp;gt; Identity|&amp;gt;, 
       &amp;quot;color&amp;quot; -&amp;gt; &amp;lt;|&amp;quot;apply&amp;quot; -&amp;gt; Identity|&amp;gt;|&amp;gt;],
    links[&amp;quot;x1&amp;quot; -&amp;gt; 0, &amp;quot;x2&amp;quot; -&amp;gt; 0, &amp;quot;y1&amp;quot; -&amp;gt; #[&amp;quot;y&amp;quot;][&amp;quot;ticks&amp;quot;], 
     &amp;quot;y2&amp;quot; -&amp;gt; #[&amp;quot;y&amp;quot;][&amp;quot;ticks&amp;quot;], &amp;quot;dx1&amp;quot; -&amp;gt; -12, &amp;quot;dx2&amp;quot; -&amp;gt; -16, 
     &amp;quot;scales&amp;quot; -&amp;gt; &amp;lt;|&amp;quot;x&amp;quot; -&amp;gt; &amp;lt;|&amp;quot;apply&amp;quot; -&amp;gt; Identity|&amp;gt;, 
       &amp;quot;color&amp;quot; -&amp;gt; &amp;lt;|&amp;quot;apply&amp;quot; -&amp;gt; (Black &amp;amp;)|&amp;gt;|&amp;gt;]}, {}] &amp;amp;

(* Default color schemes *)
indexedColorScheme := 
 Module[{colors = 
    Map[Interpreter[&amp;quot;StructuredColor&amp;quot;], {&amp;quot;#1f77b4&amp;quot;, &amp;quot;#ff7f0e&amp;quot;, 
      &amp;quot;#2ca02c&amp;quot;, &amp;quot;#d62728&amp;quot;, &amp;quot;#9467bd&amp;quot;, &amp;quot;#8c564b&amp;quot;, &amp;quot;#e377c2&amp;quot;, 
      &amp;quot;#7f7f7f&amp;quot;, &amp;quot;#bcbd22&amp;quot;, &amp;quot;#17becf&amp;quot;}]}, colors[[#]] &amp;amp;]
ordinalColorScheme := 
 Blend[Map[
    Interpreter[&amp;quot;StructuredColor&amp;quot;], {&amp;quot;#f7f4f9&amp;quot;, &amp;quot;#e7e1ef&amp;quot;, &amp;quot;#d4b9da&amp;quot;, 
     &amp;quot;#c994c7&amp;quot;, &amp;quot;#df65b0&amp;quot;, &amp;quot;#e7298a&amp;quot;, &amp;quot;#ce1256&amp;quot;, &amp;quot;#980043&amp;quot;, 
     &amp;quot;#67001f&amp;quot;}], #] &amp;amp;

(* Scale inference fills in the subcomponents of a channel scale \
based on the channel, whether the values are numeric, and \
already-specified subcomponents. *)

(* Base case *)
inferScale[channelName_, isNumeric_, {dom_, norm_, out_}] := {dom, 
  norm, out}
(* Default &amp;quot;out&amp;quot; for the radius channel *)
inferScale[&amp;quot;r&amp;quot;, isNumeric_, {dom_, norm_, Automatic}] := {dom, 
  norm, (2 + 5 #) &amp;amp;}
(* Default domain for the &amp;quot;opacity&amp;quot; channel *)
inferScale[&amp;quot;opacity&amp;quot;, 
  isNumeric_, {Automatic, norm_, out_}] := {{0, 1} &amp;amp;, norm, out}
(* Default domain and normalization for channels with all numeric \
values *)
inferScale[channelName_, 
  True, {Automatic, Automatic, out_}] := {MinMax, continuous, out}
(* Default domain and normalization for channels with nonnumeric \
values *)
inferScale[channelName_, 
  False, {Automatic, Automatic, out_}] := {Union, ordinal, out}
inferScale[&amp;quot;color&amp;quot;, False, {Automatic, Automatic, out_}] := {Union, 
  indexed, out}
(* Default continuous|ordinal color scheme *)
inferScale[&amp;quot;color&amp;quot;, 
  isNumeric_, {dom_, norm : continuous | ordinal, Automatic}] := {dom,
   norm, ordinalColorScheme}
(* Default indexed color scheme *)
inferScale[&amp;quot;color&amp;quot;, 
  isNumeric_, {dom_, norm : indexed, Automatic}] := {dom, norm, 
  indexedColorScheme}
(* Default normalization is continuous *)
inferScale[channelName_, isNumeric_, {dom_, Automatic, out_}] := {dom,
   continuous, out}
(* Default out transform is Identity *)
inferScale[channelName_, 
  isNumeric_, {dom_, norm_, Automatic}] := {dom, norm, Identity}

(* Tick inference figures out the ticks for a scale based on the \
properties of its subcomponents. *)
inferTicks[in_, norm : ordinal | indexed, domain_] := domain
inferTicks[in : Log10, norm : continuous, domain_] := 
 Range[Floor[in[First[domain]]], Ceiling[in[Last[domain]]], 1&#x2F;4.]
inferTicks[in_, norm : continuous, {lo_, hi_}] := 
 Range[lo, hi, (hi - lo)&#x2F;4.]

(* Remove Null elements from a list, since it&amp;#39;s a common mistake to \
call plot[mark1, mark2, ] with a trailing comma. *)
removeNulls[xs_] := xs &#x2F;. Null -&amp;gt; Sequence[]

(* The plot function accepts a list of marks, and returns a graphics \
object. *)
Options[plot] = {&amp;quot;dims&amp;quot; -&amp;gt; {100, 100}, 
   &amp;quot;margins&amp;quot; -&amp;gt; {{50, 10}, {50, 10}}, &amp;quot;metaMarks&amp;quot; -&amp;gt; ({} &amp;amp;)};
plot[marks___, OptionsPattern[]] := Module[{
   dims = OptionValue[&amp;quot;dims&amp;quot;],
   margins = OptionValue[&amp;quot;margins&amp;quot;],
   markList = 
    Join[{xAxis[], yAxis[]}, removeNulls[Flatten[{marks}]]],
   markTypes,
   channelValues,
   channelNumericQ,
   scales,
   markScales,
   filledMarks,
   renderedMarks
   },
  channelValues = 
   Merge[Map[markChannelValues, Select[markList, AssociationQ]], 
    Catenate];
  channelNumericQ = Map[AllTrue[NumericQ], channelValues];
  scales = Association[
    &amp;lt;|&amp;quot;x&amp;quot; -&amp;gt; &amp;lt;|&amp;quot;domain&amp;quot; -&amp;gt; {}, &amp;quot;ticks&amp;quot; -&amp;gt; {}, &amp;quot;apply&amp;quot; -&amp;gt; (0 &amp;amp;)|&amp;gt;, 
     &amp;quot;y&amp;quot; -&amp;gt; &amp;lt;|&amp;quot;domain&amp;quot; -&amp;gt; {}, &amp;quot;ticks&amp;quot; -&amp;gt; {}, &amp;quot;apply&amp;quot; -&amp;gt; (0 &amp;amp;)|&amp;gt;|&amp;gt;,
    channelValues &#x2F;&#x2F; kvMap[Module[{
         channel = #, values = #2, in = Identity, dom, norm, out, 
         domain},
        {dom, norm, out} =
         
         ConstantArray[Automatic, 3] &#x2F;&#x2F;. 
          opts_ :&amp;gt; inferScale[channel, channelNumericQ[channel], opts];
        domain = dom[Map[in, values]];
        &amp;lt;|&amp;quot;domain&amp;quot; -&amp;gt; domain, &amp;quot;ticks&amp;quot; -&amp;gt; inferTicks[in, norm, domain],
          &amp;quot;apply&amp;quot; -&amp;gt; Composition[out, norm[domain], in]|&amp;gt;
        ] &amp;amp;]
    ];
  markList = 
   FixedPoint[Replace[Flatten[#], f_Function :&amp;gt; f[scales], {1}] &amp;amp;, 
    markList];
  markScales = 
   markList &#x2F;&#x2F; Map[Merge[{scales, #scales}, Merge[Last]] &amp;amp;];
  markTypes = Comap[markList, &amp;quot;type&amp;quot;];
  renderedMarks = 
   Thread[render[markTypes, markList, markScales, &amp;lt;|&amp;quot;dims&amp;quot; -&amp;gt; dims|&amp;gt;]];
  Graphics[
   Translate[renderedMarks, Map[First, margins]],
   ImageSize -&amp;gt; dims + Map[Total, margins],
   PlotRange -&amp;gt; Transpose[{{0, 0}, dims + Map[Total, margins]}]]]

raw = ExampleData[{&amp;quot;MachineLearning&amp;quot;, &amp;quot;FisherIris&amp;quot;}, &amp;quot;Data&amp;quot;];
data = raw &#x2F;&#x2F; Map[Append[First[#], Last[#]] &amp;amp;];

plot[
 dot[
  &amp;quot;x&amp;quot; -&amp;gt; data[[All, 1]],
  &amp;quot;y&amp;quot; -&amp;gt; data[[All, 2]],
  &amp;quot;stroke&amp;quot; -&amp;gt; data[[All, 5]]]]
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>It depends what race you’re running</title>
        <published>2024-09-20T20:04:25-04:00</published>
        <updated>2025-03-15T23:06:49-04:00</updated>
        
        <author>
          <name>
            Yuri Vishnevsky
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://yuri.is/writing/it-depends-what-race-you-re-running/"/>
        <id>https://yuri.is/writing/it-depends-what-race-you-re-running/</id>
        
        <content type="html" xml:base="https://yuri.is/writing/it-depends-what-race-you-re-running/">&lt;p&gt;Imagine there are three teams of runners who are about to compete against each other in a team-vs-team race.&lt;&#x2F;p&gt;
&lt;p&gt;The first team has some very fast runners, but also some that are very slow. Despite the slowpokes, this team has highest average speed.&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;🯅 🯅 🯅                🯅 🯅 🯅 🯅      
-----------------------------
Slow                     Fast
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;The second team is made up entirely of unremarkable runners, all of whom run at roughly the same middling pace.&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;          🯅🯅🯅🯅🯅🯅🯅             
-----------------------------
Slow                     Fast
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;The third team consists of runners who are far below average. But it has one incredible athlete – a world-class talent who’s easily the fastest runner on any of the teams.&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code&gt;🯅🯅🯅🯅🯅🯅                      🯅
-----------------------------
Slow                     Fast
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Which of these teams is most likely to win the race? Well, it depends entirely on what kind of race it is.&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;It might be a relay race, in which the total time taken by a team is the sum of the times taken by the individual runners. In this scenario, the first team would be most likely to win since it has the highest average speed.&lt;&#x2F;li&gt;
&lt;li&gt;Or maybe the race is one in which all of the runners start at the same time and teams are scored by how long it takes everyone on the team to finish the race. In this case, the time for a team depends exclusively on its slowest runner, which would give the second team the advantage since the other two teams both have slower runners.&lt;&#x2F;li&gt;
&lt;li&gt;Or maybe teams are scored not by their slowest runner, but by their fastest. Then the third team would be the favorite since it has the fastest individual runner.&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;hr &#x2F;&gt;
&lt;p&gt;This analogy is an attempt to explain some simple but deep ideas in software performance that I came to relatively recently, despite writing software for more than a decade:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;It’s useful to think of the execution time of a program not as a number, but as a distribution.&lt;&#x2F;li&gt;
&lt;li&gt;Which aspects of this distribution are worth optimizing depends not on the program, but on the context in which the program is invoked.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;In the analogy, each team represents a computer program, and each runner represents a single execution of that program. The differences in the runner’s “run times” captures the idea of performance variation across multiple executions of the same program.&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;The relay race is like a plain &lt;code&gt;for&lt;&#x2F;code&gt; loop. Executions of the program occur serially, and the total time taken is the sum of the times of the individual executions. To win this race, it makes sense to optimize the mean execution time, which is just a scaled version of the sum.&lt;&#x2F;li&gt;
&lt;li&gt;The second race, in which all of the runners have to cross the finish line,  is like invoking a program many times in parallel, then waiting for all of them to complete, as one might in a &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;en.wikipedia.org&#x2F;wiki&#x2F;MapReduce&quot;&gt;map-reduce&lt;&#x2F;a&gt; computation. For example, imagine you want to search a partitioned dataset. Individual partitions can be searched in parallel, and then the results combined, with the total run time depending on the maximum time it takes to conduct an individual search (since you need to wait for the slowest search to finish before returning the combined results).&lt;&#x2F;li&gt;
&lt;li&gt;The third race, in which the team with the fastest runner wins, is like issuing a bunch of parallel calls and but only waiting around for the first result. A practical example of this pattern is the idea of &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;blog.acolyer.org&#x2F;2015&#x2F;01&#x2F;15&#x2F;the-tail-at-scale&#x2F;&quot;&gt;hedged requests&lt;&#x2F;a&gt; where, simplifying somewhat, to improve the response times from a service you send it multiple identical requests and take the response that arrives first, ie. the one with the minimum run time.&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;!--
- The same program may run all three races at different times, or even simultaneously in different contexts.
- There are other external constraints that may cause you to prefer optimizing for latency or throughput, or meeting a particular performance constraint.
--&gt;
&lt;h2 id=&quot;references&quot;&gt;References&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;cacm.acm.org&#x2F;research&#x2F;the-tail-at-scale&#x2F;&quot;&gt;The Tail at Scale&lt;&#x2F;a&gt; (&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;blog.acolyer.org&#x2F;2015&#x2F;01&#x2F;15&#x2F;the-tail-at-scale&#x2F;&quot;&gt;summary&lt;&#x2F;a&gt;)&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;Just as fault-tolerant computing aims to create a reliable whole out of less-reliable parts, large online services need to create a predictably responsive whole out of less-predictable parts; we refer to such systems as “latency tail-tolerant,” or simply “tail-tolerant.” Here, we outline some common causes for high-latency episodes in large online services and describe techniques that reduce their severity or mitigate their effect on whole-system performance.&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;tratt.net&#x2F;laurie&#x2F;blog&#x2F;2019&#x2F;minimum_times_tend_to_mislead_when_benchmarking.html&quot;&gt;Minimum Times Tend to Mislead When Benchmarking&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;In this short post, I want to make a simple argument against using the minimum time of a benchmark as a proxy for that benchmark’s performance.&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;brooker.co.za&#x2F;blog&#x2F;2021&#x2F;04&#x2F;19&#x2F;latency.html&quot;&gt;Tail Latency Might Matter More Than You Think&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;I continue to believe that if you’re going to measure just one thing, make it the mean. However, you probably want to measure more than one thing.&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;This draft is based on ideas I learned about in discussions with Yao Yue, Dan Luu, and Rebecca Isaacs over the last few years, all of whom know way more about this stuff than I do and &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;danluu.com&#x2F;perf-tracing&#x2F;&quot;&gt;have&lt;&#x2F;a&gt; &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;iop.systems&#x2F;blog&#x2F;latency-is-a-curve&#x2F;&quot;&gt;written&lt;&#x2F;a&gt; &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;danluu.com&#x2F;latency-pitfalls&#x2F;&quot;&gt;articles&lt;&#x2F;a&gt; &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;iop.systems&#x2F;blog&#x2F;composite-latency&#x2F;&quot;&gt;and&lt;&#x2F;a&gt;  &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;jasony.me&#x2F;publication&#x2F;socc23-latenseer.pdf&quot;&gt;papers&lt;&#x2F;a&gt;  that cover the same ground but in much more conceptual depth and technical detail.&lt;&#x2F;p&gt;
&lt;p&gt;Also, all this assumes that you care about &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;www.youtube.com&#x2F;watch?v=5iTTNRE-njM&amp;amp;t=50s&quot;&gt;winning the race&lt;&#x2F;a&gt; in the first place.&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Indie Apps I Enjoy</title>
        <published>2024-09-13T00:11:01-04:00</published>
        <updated>2025-10-15T02:36:53-04:00</updated>
        
        <author>
          <name>
            Yuri Vishnevsky
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://yuri.is/writing/indie-apps-i-enjoy/"/>
        <id>https://yuri.is/writing/indie-apps-i-enjoy/</id>
        
        <content type="html" xml:base="https://yuri.is/writing/indie-apps-i-enjoy/">&lt;h3 id=&quot;sublime-text&quot;&gt;&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;www.sublimetext.com&#x2F;&quot;&gt;Sublime Text&lt;&#x2F;a&gt;&lt;&#x2F;h3&gt;
&lt;p&gt;This is my text editor of choice for many years and I’m a happy paying customer. It’s fast with a very light feel, and does the basics well – editing, navigation, search – while staying responsive and almost bug-free.&lt;&#x2F;p&gt;
&lt;p&gt;I’ve tried VSCode before but couldn’t get past inelegancies in the user interface or the laggy feeling of the editor. The folks behind Sublime Text also make a visual Git client, &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;www.sublimemerge.com&#x2F;&quot;&gt;Sublime Merge&lt;&#x2F;a&gt;, which I also use.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;superwhisper&quot;&gt;&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;superwhisper.com&#x2F;&quot;&gt;superwhisper&lt;&#x2F;a&gt;&lt;&#x2F;h3&gt;
&lt;p&gt;This is a great little app for speech-to-text transcription. In fact, I’m writing this post with it.
I like it for its relatively reliable and responsive text-to-speech functionality (press a keyboard shortcut, talk, press another one, and have it paste the transcribed text). I haven’t been interested in any of their more recent developments that add additional AI features.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;little-snitch&quot;&gt;&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;www.obdev.at&#x2F;products&#x2F;littlesnitch&#x2F;index.html&quot;&gt;Little Snitch&lt;&#x2F;a&gt;&lt;&#x2F;h3&gt;
&lt;p&gt;A nice firewall and network monitor app for Mac that comes with some nice visualizations of data rates over time, and geo-located servers that your computer is talking to.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;things&quot;&gt;&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;culturedcode.com&#x2F;things&#x2F;&quot;&gt;Things&lt;&#x2F;a&gt;&lt;&#x2F;h3&gt;
&lt;p&gt;This is a to-do app that does the basics well and has an extremely pleasant user interface. I mostly use it to add tasks on the go using a shortcut on my home screen. These tasks go into an Inbox area that I can triage later. It’s mostly helpful during the times of my life when things get busy and I need some external memory to stay organized.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;bike&quot;&gt;&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;www.hogbaysoftware.com&#x2F;&quot;&gt;Bike&lt;&#x2F;a&gt;&lt;&#x2F;h3&gt;
&lt;p&gt;Bike is a text editor and outlining tool. I like its approach to text editing and use it for some of my freeform notes because it makes it easy to create a hierarchy and jump around a big outline. There are some features that I have consistently missed (I want to be able to paste in images &amp;amp; files), so I’m in the process of moving on to other apps for my writing, but I still like many of its ideas, including a novel &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;support.hogbaysoftware.com&#x2F;t&#x2F;bike-1-4-preview-70-rich-text&#x2F;5034&quot;&gt;caret affinity&lt;&#x2F;a&gt; system that solves a common issue when editing rich text.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;weather-strip&quot;&gt;&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;www.weatherstrip.app&#x2F;&quot;&gt;Weather strip&lt;&#x2F;a&gt;&lt;&#x2F;h3&gt;
&lt;p&gt;This app has a really nice visual way of presenting the weather. I’m always excited about clear and clever data visualizations.
&lt;img src=&quot;https:&#x2F;&#x2F;yuri.is&#x2F;writing&#x2F;indie-apps-i-enjoy&#x2F;iphone-extended.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;h3 id=&quot;capo&quot;&gt;&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;supermegaultragroovy.com&#x2F;products&#x2F;capo&#x2F;&quot;&gt;Capo&lt;&#x2F;a&gt;&lt;&#x2F;h3&gt;
&lt;p&gt;This is a fun app that helps you learn how to play a song by doing some helpful signal processing and showing you an annotated spectrogram with chord changes, and gives you the ability to slow down and loop particular sections of a song.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;others&quot;&gt;Others&lt;&#x2F;h3&gt;
&lt;p&gt;This is getting a bit long so I’ll end with a few other indie apps (all created by independent developers or small teams) that I either use or spiritually approve of:
&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;bear.app&#x2F;&quot;&gt;Bear&lt;&#x2F;a&gt;, &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;overcast.fm&#x2F;&quot;&gt;Overcast&lt;&#x2F;a&gt;, &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;halide.cam&#x2F;&quot;&gt;Halide&lt;&#x2F;a&gt;, &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;www.puzzmo.com&#x2F;&quot;&gt;Puzzmo&lt;&#x2F;a&gt;, &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;datasette.io&#x2F;&quot;&gt;Datasette&lt;&#x2F;a&gt;, and &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;getkirby.com&#x2F;&quot;&gt;Kirby&lt;&#x2F;a&gt;.&lt;&#x2F;p&gt;
&lt;p&gt;And on a related note, there are a few individuals who make fonts I like a lot: &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;mbtype.com&#x2F;&quot;&gt;MB Type&lt;&#x2F;a&gt; and &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;fsd.it&#x2F;shop&#x2F;fonts&#x2F;pragmatapro&#x2F;&quot;&gt;Fabrizio Schiavi&lt;&#x2F;a&gt;.&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Setting up an auto-publishing workflow</title>
        <published>2024-09-12T23:08:24-04:00</published>
        <updated>2024-12-12T12:13:45-05:00</updated>
        
        <author>
          <name>
            Yuri Vishnevsky
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://yuri.is/writing/foo/"/>
        <id>https://yuri.is/writing/foo/</id>
        
        <content type="html" xml:base="https://yuri.is/writing/foo/">&lt;p&gt;A quick note on how I’m syncing Bear with this site, since I haven’t done this before.&lt;&#x2F;p&gt;
&lt;p&gt;I’m using &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;en.wikipedia.org&#x2F;wiki&#x2F;Launchd&quot;&gt;&lt;code&gt;launchd&lt;&#x2F;code&gt;&lt;&#x2F;a&gt; to update this site the with the most recent content from &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;bear.app&#x2F;&quot;&gt;Bear&lt;&#x2F;a&gt; every minute.  Right now, there’s hardly any content here so the inefficiency of re-generating and re-deploying everything is not a problem.&lt;&#x2F;p&gt;
&lt;p&gt;The &lt;code&gt;launchd&lt;&#x2F;code&gt; bit was a bit prickly to figure out, so I figured I’d write it down here. ChatGPT helped with a lot of the basics. Here are the instructions I ended up with (&lt;code&gt;just pub&lt;&#x2F;code&gt; is a recipe that uses the excellent &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;casey&#x2F;just&quot;&gt;Just&lt;&#x2F;a&gt; command runner):&lt;&#x2F;p&gt;
&lt;p&gt;Save this to &lt;code&gt;~&#x2F;Library&#x2F;LaunchAgents&#x2F;is.yuri.pubcommand.plist&lt;&#x2F;code&gt;:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code data-lang=&quot;xml&quot;&gt;&amp;lt;?xml version=&amp;quot;1.0&amp;quot; encoding=&amp;quot;UTF-8&amp;quot;?&amp;gt;
&amp;lt;!DOCTYPE plist PUBLIC &amp;quot;-&#x2F;&#x2F;Apple&#x2F;&#x2F;DTD PLIST 1.0&#x2F;&#x2F;EN&amp;quot; &amp;quot;http:&#x2F;&#x2F;www.apple.com&#x2F;DTDs&#x2F;PropertyList-1.0.dtd&amp;quot;&amp;gt;
&amp;lt;plist version=&amp;quot;1.0&amp;quot;&amp;gt;
  &amp;lt;dict&amp;gt;
    &amp;lt;key&amp;gt;Label&amp;lt;&#x2F;key&amp;gt;
    &amp;lt;string&amp;gt;is.yuri.pubcommand&amp;lt;&#x2F;string&amp;gt;

    &amp;lt;key&amp;gt;StartInterval&amp;lt;&#x2F;key&amp;gt;
    &amp;lt;integer&amp;gt;60&amp;lt;&#x2F;integer&amp;gt;

    &amp;lt;key&amp;gt;ProgramArguments&amp;lt;&#x2F;key&amp;gt;
    &amp;lt;array&amp;gt;
      &amp;lt;string&amp;gt;&#x2F;bin&#x2F;zsh&amp;lt;&#x2F;string&amp;gt;
      &amp;lt;string&amp;gt;-c&amp;lt;&#x2F;string&amp;gt;
      &amp;lt;string&amp;gt;just pub&amp;lt;&#x2F;string&amp;gt;
    &amp;lt;&#x2F;array&amp;gt;

    &amp;lt;key&amp;gt;WorkingDirectory&amp;lt;&#x2F;key&amp;gt;
    &amp;lt;string&amp;gt;&#x2F;Users&#x2F;yurivish&#x2F;Dropbox&#x2F;Projects&#x2F;what&amp;lt;&#x2F;string&amp;gt;

    &amp;lt;key&amp;gt;StandardOutPath&amp;lt;&#x2F;key&amp;gt;
    &amp;lt;string&amp;gt;&#x2F;tmp&#x2F;what_yuri_is_output.log&amp;lt;&#x2F;string&amp;gt;

    &amp;lt;key&amp;gt;StandardErrorPath&amp;lt;&#x2F;key&amp;gt;
    &amp;lt;string&amp;gt;&#x2F;tmp&#x2F;what_yuri_is_error.log&amp;lt;&#x2F;string&amp;gt;

    &amp;lt;key&amp;gt;EnvironmentVariables&amp;lt;&#x2F;key&amp;gt;
    &amp;lt;dict&amp;gt;
      &amp;lt;key&amp;gt;PATH&amp;lt;&#x2F;key&amp;gt;
      &amp;lt;string&amp;gt;&#x2F;Users&#x2F;yurivish&#x2F;.cargo&#x2F;bin&#x2F;:&#x2F;usr&#x2F;local&#x2F;go&#x2F;bin:&#x2F;usr&#x2F;local&#x2F;bin:&#x2F;usr&#x2F;bin:&#x2F;bin:&#x2F;usr&#x2F;sbin:&#x2F;sbin&amp;lt;&#x2F;string&amp;gt;
    &amp;lt;&#x2F;dict&amp;gt;

    &amp;lt;key&amp;gt;RunAtLoad&amp;lt;&#x2F;key&amp;gt;
    &amp;lt;true&#x2F;&amp;gt;
  &amp;lt;&#x2F;dict&amp;gt;
&amp;lt;&#x2F;plist&amp;gt;
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Then run:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code data-lang=&quot;bash&quot;&gt;launchctl unload ~&#x2F;Library&#x2F;LaunchAgents&#x2F;is.yuri.pubcommand.plist
launchctl load   ~&#x2F;Library&#x2F;LaunchAgents&#x2F;is.yuri.pubcommand.plist
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;This will automatically run the publication command every minute, publishing the &lt;code&gt;#pub&lt;&#x2F;code&gt; notes from Bear.&lt;&#x2F;p&gt;
&lt;p&gt;To temporarily disable the scheduled job, you can unload the plist using launchctl:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code data-lang=&quot;bash&quot;&gt;launchctl unload ~&#x2F;Library&#x2F;LaunchAgents&#x2F;com.user.pubcommand.plist
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;This will stop the job from running without deleting the plist file.
To re-enable it later, simply load it again:&lt;&#x2F;p&gt;
&lt;pre&gt;&lt;code data-lang=&quot;bash&quot;&gt;launchctl load ~&#x2F;Library&#x2F;LaunchAgents&#x2F;com.user.pubcommand.plist
&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;You can see logs by &lt;code&gt;tail&lt;&#x2F;code&gt;ing the paths specified in the plist file under &lt;code&gt;StandardOutPath&lt;&#x2F;code&gt; and &lt;code&gt;StandardErrorPath&lt;&#x2F;code&gt;.&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Trying something new.</title>
        <published>2024-09-12T20:52:36-04:00</published>
        <updated>2024-12-12T20:24:33-05:00</updated>
        
        <author>
          <name>
            Yuri Vishnevsky
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://yuri.is/writing/trying-something-new/"/>
        <id>https://yuri.is/writing/trying-something-new/</id>
        
        <content type="html" xml:base="https://yuri.is/writing/trying-something-new/">&lt;p&gt;This is my place for rough ideas and half-baked thoughts. I’ve set it up to automatically synchronize with a new note app that I’m trying out, &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;bear.app&#x2F;&quot;&gt;Bear&lt;&#x2F;a&gt;.&lt;&#x2F;p&gt;
&lt;p&gt;I hope this workflow will lower the barrier to writing online. I’ve wanted to be a writer for a long time but have never been very comfortable with the process, and I often feel like my taste handily exceeds my skills. This seems &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;www.youtube.com&#x2F;watch?v=91FQKciKfHI&quot;&gt;fairly common&lt;&#x2F;a&gt; and I’m finally going to do something about it.&lt;&#x2F;p&gt;
&lt;p&gt;In the spirit of making things easy I’m going to try another experiment, which is to draft my notes with voice using &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;superwhisper.com&#x2F;&quot;&gt;superwhisper&lt;&#x2F;a&gt;. This will either increase the length of my posts or improve my speaking concision…&lt;&#x2F;p&gt;
&lt;p&gt;The main goal is to get comfortable publishing messy notes and unrefined ideas. I’m curious to see how it goes.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;yuri.is&#x2F;writing&#x2F;trying-something-new&#x2F;image.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Euclidean Distance and Pearson Correlation</title>
        <published>2021-12-28T00:00:00+00:00</published>
        <updated>2025-10-19T17:04:14-04:00</updated>
        
        <author>
          <name>
            Yuri Vishnevsky
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://yuri.is/writing/euclidean-distance-and-pearson-correlation/"/>
        <id>https://yuri.is/writing/euclidean-distance-and-pearson-correlation/</id>
        
        <content type="html" xml:base="https://yuri.is/writing/euclidean-distance-and-pearson-correlation/">&lt;p&gt;I recently learned there’s a useful connection between &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;en.wikipedia.org&#x2F;wiki&#x2F;Euclidean_distance&quot;&gt;Euclidean distance&lt;&#x2F;a&gt; and &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;en.wikipedia.org&#x2F;wiki&#x2F;Pearson_correlation_coefficient&quot;&gt;Pearson correlation&lt;&#x2F;a&gt;: the squared Euclidean distance between two &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;en.wikipedia.org&#x2F;wiki&#x2F;Standard_score#Calculation&quot;&gt;z-normalized&lt;&#x2F;a&gt; vectors equals their &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;en.wikipedia.org&#x2F;wiki&#x2F;Pearson_correlation_coefficient#Pearson&amp;#x27;s_distance&quot;&gt;Pearson distance&lt;&#x2F;a&gt;, up to a scale factor.&lt;&#x2F;p&gt;
&lt;p&gt;This short note walks through a derivation.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;z-normalization&quot;&gt;Z-normalization&lt;&#x2F;h3&gt;
&lt;p&gt;Let’s say we have two z-normalized vectors $x$ and $y$ in $N$ dimensions.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;en.wikipedia.org&#x2F;wiki&#x2F;Standard_score#Calculation&quot;&gt;Z-normalizing&lt;&#x2F;a&gt; a vector involves subtracting its mean then dividing by its standard deviation, so we know that the means $\mu_x$ and $\mu_y$ of our vectors $x$ and $y$ are zero:&lt;&#x2F;p&gt;
&lt;p&gt;$$
\mu_x = \mu_y = 0
$$&lt;&#x2F;p&gt;
&lt;p&gt;and their standard deviations $\sigma_x$ and $\sigma_y$ are one:&lt;&#x2F;p&gt;
&lt;p&gt;$$
\sigma_x = \sigma_y = 1
$$&lt;&#x2F;p&gt;
&lt;p&gt;We’re going to introduce some statistical concepts and see how they simplify under these constraints. Expressions in &lt;span class=special&gt;color&lt;&#x2F;span&gt; are simplifications valid only under z-normalization.&lt;&#x2F;p&gt;
&lt;p&gt;Summations implicity sum over all terms: $\small{\sum{x_i} = \sum_i^Nx_i}$&lt;&#x2F;p&gt;
&lt;h3 id=&quot;covariance&quot;&gt;Covariance&lt;&#x2F;h3&gt;
&lt;p&gt;Since the means $\mu_x$ and $\mu_y$ are both zero, the numerator reduces to a dot product:&lt;&#x2F;p&gt;
&lt;p&gt;$$
\operatorname{cov}(x, y) = {\sum{(x_i - \mu_{x})(y_i - \mu_{y})} \over N}
$$&lt;&#x2F;p&gt;
&lt;p&gt;$$
\htmlClass{special}{\operatorname{cov}(x, y) = {{\sum{x_iy_i}} \over N}}
$$&lt;&#x2F;p&gt;
&lt;h3 id=&quot;standard-deviation&quot;&gt;Standard deviation&lt;&#x2F;h3&gt;
&lt;p&gt;The standard deviations $\sigma_x$ and $\sigma_y$ both simplify to one by the definition of z-normalization:&lt;&#x2F;p&gt;
&lt;p&gt;$$
\sigma_x = \sqrt{cov(x, x)}
$$&lt;&#x2F;p&gt;
&lt;p&gt;$$
\htmlClass{special}{\sigma_x = \sqrt{\sum{x_i^2} \over N} = 1}
$$&lt;&#x2F;p&gt;
&lt;h3 id=&quot;pearson-correlation&quot;&gt;Pearson correlation&lt;&#x2F;h3&gt;
&lt;p&gt;The denominator reduces to one, leaving us with just the covariance:&lt;&#x2F;p&gt;
&lt;p&gt;$$
\operatorname{P}(x, y) = {\operatorname{cov}(x, y) \over \sigma_x\sigma_y}
$$&lt;&#x2F;p&gt;
&lt;p&gt;$$
\htmlClass{special}{\operatorname{P}(x, y) = \operatorname{cov}(x, y) = {{\sum{x_iy_i}} \over N}}
$$&lt;&#x2F;p&gt;
&lt;h3 id=&quot;euclidean-distance&quot;&gt;Euclidean distance&lt;&#x2F;h3&gt;
&lt;p&gt;Now let’s look at &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;en.wikipedia.org&#x2F;wiki&#x2F;Euclidean_distance#Squared_Euclidean_distance&quot;&gt;squared Euclidean distance&lt;&#x2F;a&gt; between z-normalized vectors.&lt;&#x2F;p&gt;
&lt;p&gt;$$
\operatorname{dist^2}(x, y) = \sum{(x_i - y_i)^2}
$$&lt;&#x2F;p&gt;
&lt;p&gt;We can expand out the squared differences inside the sum:&lt;&#x2F;p&gt;
&lt;p&gt;$$
{\operatorname{dist^2}(x,y) =}\sum{(x_i^2 - 2x_iy_i + y_i^2)}
$$
then separate out the terms into their own sums so we can tackle them one by one:&lt;&#x2F;p&gt;
&lt;p&gt;$$
{\operatorname{dist^2}(x,y) =} \sum{x_i^2} - 2\sum{x_iy_i} + \sum{y_i^2}
$$&lt;&#x2F;p&gt;
&lt;p&gt;Let’s examine this. The two sum-of-squares terms, $\sum{x_i^2}$ and $\sum{y_i^2}$, are both $N$ times the squared standard deviation, which reduces to $N$:&lt;&#x2F;p&gt;
&lt;div&gt;&lt;!-- prevent underscores from messing with the markdown --&gt;
$$
\underbrace{\sum{x_i^2}}_{N \text{ times } \sigma^2_x ~=~ N}
\htmlClass{light}{ - 2\sum{x_iy_i}  +}
\underbrace{\sum{y_i^2}}_{N \text{ times } \sigma^2_y ~=~ N}
$$
&lt;&#x2F;div&gt;
&lt;p&gt;Meanwhile, the middle term is $2N$ times $\operatorname{P}(x, y)$:&lt;&#x2F;p&gt;
&lt;p&gt;$$
\htmlClass{light}{\sum{x_i^2} -}
\underbrace{2\sum{x_iy_i}}_{2N \text { times } \operatorname{P}(x, y)}
\htmlClass{light}{+ \sum{y_i^2}}
$$
Putting these pieces together, our formula becomes:&lt;&#x2F;p&gt;
&lt;p&gt;$$
\htmlClass{special}{\operatorname{dist^2}(x, y) = 2N - 2N \cdot \operatorname{P}(x, y)}
$$&lt;&#x2F;p&gt;
&lt;p&gt;which can be simplified to:&lt;&#x2F;p&gt;
&lt;p&gt;$$
\htmlClass{special}{\operatorname{dist^2}(x, y) = 2N(1 - \operatorname{P}(x, y))}
$$&lt;&#x2F;p&gt;
&lt;p&gt;showing that the squared Euclidean distance between two z-normalized vectors is $2N$ times their &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;en.wikipedia.org&#x2F;wiki&#x2F;Pearson_correlation_coefficient#Pearson&amp;#x27;s_distance&quot;&gt;Pearson distance&lt;&#x2F;a&gt;.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;why-does-this-matter&quot;&gt;Why does this matter?&lt;&#x2F;h3&gt;
&lt;p&gt;A common trick to improve Euclidean nearest-neighbor searches is to z-normalize the vectors that you’re searching over. The connection to Pearson correlation gives us an intuition for why this works so well in practice.&lt;&#x2F;p&gt;
&lt;p&gt;Additionally, representing correlation as a distance metric allows us to use it with algorithms that rely on properties of a distance metric, such as the &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;en.wikipedia.org&#x2F;wiki&#x2F;Triangle_inequality&quot;&gt;triangle inequality&lt;&#x2F;a&gt;. Despite the name, Pearson distance is &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;arxiv.org&#x2F;abs&#x2F;1908.06029&quot;&gt;not actually a distance&lt;&#x2F;a&gt;.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;references&quot;&gt;References&lt;&#x2F;h3&gt;
&lt;p&gt;&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;arxiv.org&#x2F;abs&#x2F;1601.02213&quot;&gt;On Clustering Time Series Using Euclidean Distance and Pearson Correlation&lt;&#x2F;a&gt;:&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;For time series comparisons, it has often been observed that z-score normalized Euclidean distances far outperform the unnormalized variant. In this paper we show that a z-score normalized, squared Euclidean Distance is, in fact, equal to a distance based on Pearson Correlation.&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;style&gt;
.special { color: #ED1E43; }
.light { color: #ccc; }
&lt;&#x2F;style&gt;</content>
        
    </entry>
</feed>
