- {%- endif %}
-
- {%- if site.disqus.shortname -%}
- {%- include disqus_comments.html -%}
- {%- endif %}
-
-
-
diff --git a/_posts/2022-05-13-college-application-presentation-korean.md b/_posts/2022-05-13-college-application-presentation-korean.md
deleted file mode 100644
index aca9bd54..00000000
--- a/_posts/2022-05-13-college-application-presentation-korean.md
+++ /dev/null
@@ -1,8 +0,0 @@
----
-layout: post
-title: "The college application problem, presented in Korean"
----
-
-I gave a brief
-[presentation about the college application problem](https://www.youtube.com/watch?v=tZCSwCPd1Qc)
-at a research fair hosted by our department. English subtitles are included.
diff --git a/_posts/2022-09-01-apply-fifty-colleges.md b/_posts/2022-09-01-apply-fifty-colleges.md
deleted file mode 100644
index 989c9a50..00000000
--- a/_posts/2022-09-01-apply-fifty-colleges.md
+++ /dev/null
@@ -1,120 +0,0 @@
----
-layout: post
-title: "Apply to fifty colleges"
----
-
-In this post, we’ll use fake numbers and a simplified model to argue that the most college applicants should apply to far more colleges than they actually do. You can probably extend the argument to job applications and the dating game. I’ll also compute a few fake comparative statics and speculate about why real students don’t apply to more schools.[^obvious]
-
-[^obvious]: We will avoid the obvious argument that goes, “If you are willing to spend six figures on an a college education, then what’s a few hundred extra in application fees?” This argument is incorrect, because it assumes that each additional application is worth the value of attending that college times the probability of getting in. In actuality, you can only attend *one* of the colleges you get into; thus additional applications make a sublinear contribution to the overall expected utility. Our model will account for this.
-
-## Fake numbers and a simplified model
-
-We’ll start by making a few simplifying, conservative assumptions about the college admissions process. Here “conservative” means that we will err on the side of *understating* the value of applying to college, thereby arriving at an *underestimate* of the optimal number of college applications. Thus, our choices make it challenging to arrive at this post’s headline conclusion, such that if we *nonetheless* draw the conclusion that students should apply to more colleges, we have some degree of confidence that this statement applies to the actual numbers, too.
-
-First, let’s assume that you always have the option of attending a “safety school.” The safety school could also represent a community college or entering the workforce directly after high school. In addition, there are many “reach schools,” or elite universities, with competitive admissions. It is better to attend a reach school than the safety school, but applying to a reach school takes time and effort.
-
-Let $$t$$ denote the utility of attending an elite university, measured in monetary units relative to the safety school. By a conservative estimate, an elite university might offer us an expected salary increase of \$20,000 per year, at a tuition cost of \$30,000 dollars per year higher than the safety school. Thus, aggregating over 40 years of employment and 5 years of college attendance, we can estimate
-
-$$t = 40 \times 20000 - 5 \times 30000 = 650000.$$
-
-Let’s assume, for simplicity, that all the reach schools are equally competitive, and let $$p$$ denote the probability of getting into an elite university. Assume that admissions at each school are probabilistically independent. Let’s pick $$p = 0.1$$, which is near the acceptance rate (and therefore, the average admissions probability across all applicants) at many elite US universities. If you apply to $$x$$ reach schools, the probability of getting into *at least* one is $$1 - (1 - p)^x$$, in which case you receive $$t$$ units of utility.
-
-However, applying to college is not free: You have to pay an application fee, various clerical fees for submitting transcripts and test scores, and then there are time costs associated with writing essays, requesting recommendation letters, and filling out the online form. Let $$c$$ denote the cost of applying to a reach school, so the cost of applying to $$x$$ schools is $$cx$$, and the expected utility of the entire college application process can be written as follows:[^sublinear]
-
-[^sublinear]: Note that our assumption that the cost is linear in $$x$$ is conservative: In reality, since you can recycle essays and recommendation letters, the marginal costs diminish as $$x$$ increases; a function like $$c \ln x$$ or $$c \sqrt{x}$$ is arguably more realistic.
-
-$$f(x) = t\bigl(1 - (1 - p)^x\bigr) - cx.$$
-
-Typical college application fees are \$50 or so; let’s take $$c = 200$$ to account generously for the other costs. Our goal is to maximize $$f(x)$$.
-
-## The model says apply to fifty colleges
-
-With these values of $$t$$, $$p$$, and $$c$$, here’s the graph of $$f(x)$$. As you can see, the maximum occurs at about $$x^* = 55$$ applications!
-
-
-
-We can solve for the maximum analytically using a method you might have learned in high school calculus: $$x^*$$ represents the point where $$f(x)$$ stops increasing and starts decreasing. That is, the point where its slope $$f'(x)$$ is zero.
-
-With a little bit of legwork, you can check that the derivative of $$f(x)$$ is
-
-$$f'(x) = -t (1-p)^m \ln(1-p) - c$$
-
-and setting this equal to zero and solving for $$x$$ yields
-
-$$x^* = \frac{\ln c - \ln t - \ln \bigl(- \ln\left(1-p\right)\bigr)}{\ln\left(1-p\right)}.$$
-
-(It’s a pleasant surprise to encounter a nested logarithm somewhere other than computer science.) Plug in $$t$$, $$p$$, and $$c$$ to obtain $$x^* = 55$$, as promised.[^exact]
-
-[^exact]: The exact answer is $$x^* = 55.39$$, but you can only apply to a whole number of colleges. Since $$f(x)$$ is a concave, univariate function, the optimal integer solution has to be either 55 or 56, and you can verify that $$f(55) > f(56)$$. We’ll use the same approach for the rest of the post.
-
-## Checking our work with comparative statics
-
-The reflexive economist move is to stick the expression for $$x^*$$ in a [Desmos box](https://www.desmos.com/calculator/yhxxla3wko) as follows, start dragging the sliders around, and see if we can spot any patterns. For example, click the “edit graph on Desmos” box and try increasing the value of $$c$$ slightly: You should see the vertical line, which represents the optimal number of applications, move to the left.
-
-
-
-If we’re feeling especially inspired, we might take the derivative of $$x^*$$ in each of the parameters. Computing these so-called comparative statics will help us verify that our “conservative” assumptions are actually conservative in the way that we think we are. For example,
-
-$$\frac{\partial x^*}{\partial c} = \frac{1}{c \ln (1- p)} < 0$$
-
-(since $$1-p$$ is less than one, its logarithm is negative) tells us that increasing the costs *decreases* the optimal number of applications. But (in my opinion) the linear function $$cx$$ is an *overestimate* of the cost, and the true optimum $$x^*$$ should be even higher: conservatism fulfilled.
-
-Similarly, $$\partial x^* / \partial t > 0$$, meaning that in the [(probable)](https://money.com/wage-gap-college-high-school-grads/) case that an elite college degree is worth more than \$650k, the central argument of this post is even truer.
-
-$$p$$ is where we get into trouble. I lowballed $$p$$ above on the assumption that the “value” of a college application correlates with how likely it is to yield an admission letter. However, the *marginal value* of a college application is actually greatest when colleges are hard to get into, and therefore so is your willingness to invest in an additional application. In other words, I should have tried to overshoot $$p$$ instead.
-
-To understand this counterintuitive result, let’s compute
-
-$$\frac{\partial x^*}{\partial p} = \frac{ \ln{c} - \ln{t} - \ln{\bigl(- \ln{\left(1 - p \right)} \bigr)} + 1}{\left(1 - p\right) \ln^2{\left(1 - p \right)}},$$
-
-whose sign tells us whether the optimal number of applications is increasing or decreasing in the admissions odds $$p$$. The denominator of this expression is positive, so the overall sign depends on the sign of the numerator, which is positive only when
-
-$$p < 1 - \exp(-ce / t).$$
-
-But in college admissions, this is seldom the case: Typically, $$t$$ is much larger than $$c$$, meaning that $$1 - \exp(-ce / t) \approx 1 - \exp(0) = 0$$, breaking the condition. Lowballing $$p$$ is only conservative if $$p$$ is very small in the first place, and $$c$$ is large relative to $$t$$.[^exceptionalregime]
-
-[^exceptionalregime]: For example, to see a regime where $$x^*$$ is increasing in $$p$$, try setting $$t = 10000$$ and $$c = 1000$$ in the Desmos box, and vary $$p$$ between 0.1 and 0.2.
-
-## Oops, apply to thirty colleges
-
-So, if we are being conservative, we should try to *overestimate* $$p$$. If we double our previous estimate to $$p = 0.20$$ and repeat the calculation above, we get a more modest result of $$x^* = 30$$. But this is still much higher than the number of colleges real students apply to, right?
-
-We have to increment $$p$$ much further before $$x^*$$ starts to align with typical applicant behavior: When $$p = 0.7$$, for instance, you get $$x^* = 7$$. Now, you might object that $$p = 0.7$$ sounds like a perfectly reasonable admissions probability for an ambitious student. To this, I offer two responses:
-
-1. As someone who reads blog posts about economics in their free time, your social circle may have instilled in you an biased sense of what is achievable for the typical student.
-2. *Some* students have high admissions odds, but others don’t: The odds have to average out to around 10 percent for elite colleges to maintain acceptance rates in that neighborhood.
-
-In other words, our comparative statics analysis revealed an error in our so-called conservative fake numbers, but the error wasn’t quite large enough to undermine our central argument.
-
-## Why don’t students apply to more colleges?
-
-Most students apply to ten or so colleges. Why not more? What do they know that our model doesn’t?
-
-One idea that might come to mind if you’ve played with [Mulberry]({{ site.url }}/mulberry/) is that most students apply not just to elite schools and a safety school, but also to a continuum of “target schools” in between. Target schools have higher admissions odds, and indeed make up the [bulk of the admissions market](https://www.pewresearch.org/fact-tank/2019/04/09/a-majority-of-u-s-colleges-admit-most-students-who-apply/). This observation isn’t enough to explain real students’ small application portfolios, however, because elite universities also are (perceived as) much more valuable than target schools.
-
-The model above can also be applied to a student who has already applied to safety and target schools and is trying to decide how many reach schools to add to her portfolio: Simply discount $$t$$ to reflect the value of a degree from an elite school relative to the (actuarial) value of a degree from a target school. Conservatively, if we hack $$t$$ all the way down to \$200k, take $$p = 0.2$$, and $$c = 200$$, we *still* get a hefty $$x^* = 24$$.
-
-Here’s what I think is really going on: Students don’t apply to zillions of colleges because they aren’t optimization machines. When I was a senior in high school, I categorically refused to apply to more than five colleges because I didn’t want to look like a tryhard.[^idw] The psychological costs of college application are *really large,* because they include not only the exhaustion of composing and submitting applications, but also the cost of *imagining* the disappointment of being rejected from a dream school.
-
-[^idw]: It didn’t work.
-
-## Revealed cost of college application
-
-What, in dollar terms, is the value of the subjective cost students associate with college application, after taking into account this risk aversion? Let’s stick with our conservative $$t = 200000$$ and $$p = 0.2$$ scenario, and turn the question around: Given that most students apply to about a dozen colleges, what application cost $$c$$ makes this number of applications optimal?
-
-To figure this out, we just need to solve our $$x^*$$ equation for $$c$$. The answer is
-
-$$c = - t (1 - p)^x \log{\left(1 - p \right)}$$
-
-and plugging in the numbers yields $$c = 3067$$ dollars per application.
-
-Remember: These numbers are fake. But we have made a plausible argument that the majority of the costs incurred in the college application process are psychological, rather than material. If you can overcome the psychological costs, then by applying to thirty or fifty colleges, you may be able to reap significant material rewards.
-
-## Reader exercises
-
-- Repeat the calculations above for another situation, such as job application. How many jobs should you apply to? Is the model a good choice?
-- Identify a regime in which $$\partial x^* / \partial p$$ is positive and explain, in qualitative terms, why this is the case.
-- If every student adopts the optimal application strategy, then everyone might apply to twice as many schools. But then each school will become twice as competitive, changing the admissions odds and therefore the optimal strategies. Can you sketch the equilibrium criteria for this market? What other information would you need? [(Paywalled academic paper about this.)](https://www.journals.uchicago.edu/doi/10.1086/675503)
-- Suppose that the admissions probabilities and utility values for the reach schools are all different. What is the utility expression in this case? *Hint:* Replace $$x$$ with a set indicating which colleges you apply to. You can find the answer [here]({{ site.url }}/mulberry/) or in my [master’s thesis](https://github.com/maxkapur/CollegeApplication).
-
-
diff --git a/_posts/2024-09-24-bimatrix-game-equilibrium.md b/_posts/2024-09-24-bimatrix-game-equilibrium.md
deleted file mode 100644
index 314d377f..00000000
--- a/_posts/2024-09-24-bimatrix-game-equilibrium.md
+++ /dev/null
@@ -1,102 +0,0 @@
----
-layout: post
-title: "Bimatrix game equilibrium via nonlinear programming"
----
-
-[These lecture notes
-(PDF)](https://ocw.mit.edu/courses/6-254-game-theory-with-engineering-applications-spring-2010/9cca6ef4a5399a4e05891f471d667441_MIT6_254S10_lec09.pdf)
-from Asu Ozdaglar give a bilinear program whose solution is the mixed-strategy
-equilibrium for a two-player, non-zero-sum game with finite action spaces—also
-known as a bimatrix game. I wanted to reproduce this highly practical result in
-a more accessible format and offer some implementation notes.
-
-# Definitions
-
-A *bimatrix game* is characterized by two matrices $$A, B \in \mathbb{R}^{n
-\times m}.$$ When Alice chooses strategy $$i$$ and Bob chooses strategy $$j$$,
-their payoffs are $$A_{ij}$$ and $$B_{ij},$$ respectively. Mixed-strategy
-probability vectors $$x$$ and $$y$$ yield expected payoffs of $$x^T A y$$ and
-$$x^T B y,$$ respectively.
-
-$$(x^*, y^*)$$ is an *equilibrium* if $$x^T A y^* \leq x^{*T} A y^*$$ and
-$$x^{*T} A y \leq x^{*T} A y^*$$ for all probability vectors $$x$$ and $$y.$$
-
-# Result
-
-**Theorem:** $$(\implies)$$ If $$(x^*, y^*)$$ is an equilibrium, then there
-exist $$p^*$$ and $$q^*$$ such that $$(x^*, y^*, p^*, q^*)$$ is the optimal
-solution to the following bilinear program:
-
-$$
-\begin{aligned}
- \text{maximize} \quad & f(x, y, p, q) = x^T A y + x^T B y - p - q \\
- \text{subject to} \quad & A y \leq p \mathbf{1} & \text{(OptA)} \\
- & B^T x \leq q \mathbf{1} & \text{(OptB)} \\
- & \sum x_i = \sum y_i = 1 & \text{(ProbVec1)} \\
- & x \geq \mathbf{0}, y \geq \mathbf{0} & \text{(ProbVec2)}
-\end{aligned}
-$$
-
-$$(\impliedby)$$ Conversely, the optimal solution of the bilinear program is an
-equilibrium for the game.
-
-**Proof:** $$(\implies)$$ For any feasible solution to the bilinear program,
-each element of $$x$$ is nonnegative (by ProbVec2), so we can use $$x$$ to
-combine the rows of the condition OptA to obtain a new valid inequality $$x^T A
-y \leq x^T p \mathbf{1} = p \sum x_i = p$$ (by ProbVec1). Applying the same
-logic to $$y$$ and $$q,$$ we find that $$f(x, y, p, q) \leq 0.$$
-
-Now consider the equilibrium probability vectors $$(x^*, y^*)$$ and set $$p^* =
-x^{*T} A y^*$$ and $$q^* = x^{*T} B y^{*}.$$ Then $$f(x^*, y^*, p^*, q^*) = 0,$$
-and we have only to show that this solution is feasible. To be an equilibrium,
-$$x^*$$ must earn Alice a better payoff against $$y^{*}$$ than does the $$i$$th
-pure strategy: $$p^* = x^{*T} A y^* \geq (A y^*)_i.$$ This is precisely the
-$$i$$th row of OptA. OptB follows similarly.
-
-$$(\impliedby)$$ The bilinear program is clearly feasible and bounded (as shown
-a moment ago). Let $$(\bar x, \bar y, \bar p, \bar q)$$ denote the optimal
-solution. By Nash’s theorem on the existence of mixed-strategy equilibria, we
-know that an equilibrium exists, and from the first part of the proof, we know
-how to use this equilibrium to produce a feasible solution to the bilinear
-program with an objective value of zero. Thus, $$f(\bar x, \bar y, \bar p, \bar
-q) \geq 0,$$ which rearranges to
-
-$$(\bar x^T A \bar y - \bar p) + (\bar x^T B \bar y - \bar q) \geq 0.$$
-
-By the constraints OptA and OptB, each of the terms in parentheses is less than
-or equal to zero; thus, the inequality on $$f(\bar x, \bar y, \bar p, \bar q)$$
-can hold only when each of these terms *equals* zero exactly.
-
-Now consider any probability vector $$x$$ and use it to combine the rows of
-OptA: We have $$x^T A \bar y \leq x^T \bar p \mathbf{1} = \bar p = \bar x^T A
-\bar y,$$ which says that $$\bar x$$ is a best response to $$\bar y$$. Applying
-the same logic to $$\bar y$$ and $$\bar q$$ completes the proof. ◼
-
-# Remarks
-
-Solving the bilinear program is not necessarily easy. If merely solving a
-bimatrix game of this form is your goal, then mitigations against local optima
-such as [iterated local
-search](https://en.wikipedia.org/wiki/Iterated_local_search) are essential. You
-can check whether a solution is local or global by comparing the objective value
-to zero, which is the global optimum guaranteed by Nash’s theorem.
-
-In practice, I have used the bilinear program above when implementing the
-[double oracle algorithm](https://arxiv.org/abs/2009.12185) for games with
-complex action spaces. For example, in a modeling problem I am working on, pure
-strategies are subsets of $$\mathbb{R}^n$$ with additional inequality and
-integrality constraints.
-
-The “first oracle” in the double-oracle algorithm has you compute the
-equilibrium of a subgame with discrete action sets and uses the bilinear program
-above. In the second oracle (typically the hard part), you then augment these
-action sets by computing a pure-strategy best response for each player against
-the mixed-strategy equilibrium obtained from the first oracle. You can use the
-double oracle algorithm to approximate either pure- *or* mixed-strategy
-equilibria by checking convergence and terminating after either the first or
-second oracle, respectively.
-
-In my use case, the open-source solver [Ipopt](https://github.com/coin-or/Ipopt)
-gives acceptable results for the first oracle. I also learned about
-[cyipopt](https://github.com/mechmotum/cyipopt), a nice set of Python bindings
-for Ipopt that lets you `@jit` your functions with Numba or Jax for performance.
diff --git a/_sass/minima.scss b/_sass/minima.scss
deleted file mode 100644
index 57ff2c69..00000000
--- a/_sass/minima.scss
+++ /dev/null
@@ -1,74 +0,0 @@
-@charset "utf-8";
-@use "sass:color";
-
-// Base and monospace font
-$base-line-height: 1.5;
-
-$base-font-family: "IBM Plex Sans", "IBM Plex Sans KR", sans-serif;
-$base-font-size: 1rem;
-$base-font-weight: 400;
-$base-font-weight-strong: 600; // 700 is too intense
-
-$monospace-font-family: "IBM Plex Mono", monospace;
-$monospace-font-size: $base-font-size * 0.9;
-$monospace-font-strong-weight: 600;
-
-// Used in figcaption, footer
-$small-font-size: $base-font-size * 0.875;
-
-// Used by minima layout
-$spacing-unit: 30px;
-
-// Obligatory black text. Muted variant for post titles, blockquote text, code
-// comments.
-$text-color: black;
-$text-color-light: #546270;
-
-// Primary background color, and a brighter alternate background color for code
-// blocks and table body
-$background-color: #ffffeb;
-$background-color-light: #fffff5;
-
-// Plain brand color is used for links (there are plenty of them). Desaturated
-// variant is more legible, for visited links and strings in syntax
-// highlighting. Light variant is for secondary design elements: horizontal
-// rules, vertical rule left of blockquotes, table header background. Should
-// mainly be referenced via border-heavy to ensure consistent width.
-$brand-color: #008000;
-$brand-color-desaturated: color.adjust($brand-color, $saturation: -50%);
-$brand-color-light: #b7ccb7;
-$border-heavy: 2px solid $brand-color-light;
-
-// Width of the content area
-$content-width: 800px;
-$on-palm: 600px;
-$on-laptop: 800px;
-
-// These two mixins are intended to differentiate normal links
-// from hovered links:
-@mixin subtle-underline {
- text-decoration: underline solid currentColor 1px;
-}
-
-@mixin heavy-underline {
- text-decoration: underline solid currentColor 1.5px;
-}
-
-// For completeness' sake, this mixin undoes the other two in
-// case one of those styles was inherited:
-@mixin no-underline {
- text-decoration: none;
-}
-
-// Use media queries like this:
-// @include media-query($on-palm) {
-// .wrapper {
-// padding-right: $spacing-unit / 2;
-// padding-left: $spacing-unit / 2;
-// }
-// }
-@mixin media-query($device) {
- @media screen and (max-width: $device) {
- @content;
- }
-}
diff --git a/_sass/minima/_base.scss b/_sass/minima/_base.scss
deleted file mode 100644
index 10a2cf28..00000000
--- a/_sass/minima/_base.scss
+++ /dev/null
@@ -1,337 +0,0 @@
-@use "sass:color";
-@use "sass:math";
-@use "minima" as *;
-
-/**
- * Reset some basic elements
- */
-body,
-h1,
-h2,
-h3,
-h4,
-h5,
-h6,
-aside,
-p,
-blockquote,
-pre,
-hr,
-dl,
-dd,
-ol,
-ul,
-figure {
- margin: 0;
- padding: 0;
-}
-
-html {
- font-size: $base-font-size;
-}
-
-/**
- * Basic styling
- */
-body {
- font: $base-font-weight #{$base-font-size}/#{$base-line-height} $base-font-family;
- color: $text-color;
- background-color: $background-color;
- -webkit-text-size-adjust: 100%;
- -webkit-font-feature-settings: "kern" 1;
- -moz-font-feature-settings: "kern" 1;
- -o-font-feature-settings: "kern" 1;
- font-feature-settings: "kern" 1;
- font-kerning: normal;
- display: flex;
- min-height: 100vh;
- flex-direction: column;
-}
-
-strong {
- font-weight: $base-font-weight-strong;
-}
-
-// This keeps sup elements from making their lines awkwardly higher than others
-// https://stackoverflow.com/questions/1530685/how-can-i-keep-consistent-line-height-with-superscript-elements
-sub,
-sup {
- line-height: 100%;
-}
-
-/**
- * Set `margin-bottom` to maintain vertical rhythm
- */
-h1,
-h2,
-h3,
-h4,
-h5,
-h6,
-p,
-aside,
-blockquote,
-pre,
-table,
-ul,
-ol,
-dl,
-figure,
-%vertical-rhythm {
- margin-bottom: math.div($spacing-unit, 2);
-}
-
-
-
-/**
- * `main` element
- */
-main {
- display: block;
- /* Default value of `display` of `main` element is 'inline' in IE 11. */
-}
-
-
-
-/**
- * Images
- */
-img {
- max-width: 100%;
- border-radius: 8px;
-}
-
-// By default, images are the full width of the text. This is a little loud for
-// some images; add a .compact class to those that we want to be a bit smaller.
-img.compact {
- max-width: min(100%, 600px);
-}
-
-// Add this style to remove the border radius from images (e.g. graphs) that
-// have content all the way to the corners.
-img.squareborder {
- border-radius: 0;
-}
-
-
-
-/**
- * Figures
- */
-figure {
- display: flex;
- flex-direction: column;
- align-items: left;
-}
-
-figcaption {
- font-size: $small-font-size;
- color: $text-color-light;
-}
-
-
-
-/**
- * Lists
- */
-ul,
-ol {
- margin-left: $spacing-unit;
-}
-
-
-
-/**
- * Headings
- */
-// h1,
-// h2,
-// h3,
-// h4,
-// h5,
-// h6 {
-// }
-
-
-
-/**
- * Links
- */
-a {
- color: $brand-color;
- @include subtle-underline;
-
- &:visited {
- color: $brand-color-desaturated;
- }
-
- &:hover {
- @include heavy-underline;
- }
-
- // Couple of places where we don't want links underlined until hovered:
- h1 &,
- h2 &,
- h3 &,
- h4 &,
- h5 &,
- h6 &,
- .site-header &,
- .excerpt-warning & {
- @include no-underline;
-
- &:hover {
- @include heavy-underline;
- }
- }
-
- // This is a weird case: Without this block, the space before the username
- // also gets underlined, which looks bad; we only want the username itself to
- // be highlighted. Moreover, selector specificity requires this rule to be
- // last so that it overrides the &:hover rule above (or else the space is
- // again underlined on hover). Ditto the RSS link (which appears in the social
- // list).
- .social-media-list & {
- @include no-underline;
-
- .username,
- .rss-link {
- @include subtle-underline;
-
- &:hover {
- @include heavy-underline;
- }
- }
- }
-}
-
-/**
- * Blockquotes
- */
-blockquote {
- color: $text-color-light;
- border-left: $border-heavy;
- padding-left: math.div($spacing-unit, 3);
-
- > :last-child {
- margin-bottom: 0;
- }
-}
-
-
-
-/**
- * Code
- */
-
-// Inline code
-code {
- font-size: $monospace-font-size;
- padding: 0 0.1em;
-}
-
-// Code blocks
-pre:has(code) {
- font-size: $monospace-font-size;
- border: $border-heavy;
- padding: 8px 12px;
- overflow-x: auto;
- resize: both;
- background: $background-color-light;
-
- code {
- padding: 0;
- }
-}
-
-
-
-/**
- * Keyboard
- */
-kbd {
- font-family: $base-font-family;
- font-size: $small-font-size;
- border: 1px solid $text-color-light;
- border-radius: 3px;
- padding: 3px 5px 2px 5px;
-}
-
-
-
-/**
- * Wrapper
- */
-.wrapper {
- max-width: $content-width - $spacing-unit * 2;
- margin-right: auto;
- margin-left: auto;
- padding-right: $spacing-unit;
- padding-left: $spacing-unit;
-}
-
-
-
-/**
- * Icons
- */
-.svg-icon {
- width: 16px;
- height: 16px;
- display: inline-block;
- fill: $text-color-light;
-
- padding-right: 5px;
- vertical-align: text-top;
-}
-
-
-
-/**
- * Tables
- */
-table {
- background-color: $background-color-light;
- text-align: left;
- border: 0.5px solid $text-color-light;
- border-collapse: collapse;
-
- th,
- td {
- // NOTE: Padding matches
-
-{%- endif -%}
diff --git a/check_reserved_paths.py b/check_reserved_paths.py
new file mode 100755
index 00000000..76d5f70a
--- /dev/null
+++ b/check_reserved_paths.py
@@ -0,0 +1,49 @@
+#!/usr/bin/env python
+"""Check that Hugo build is free of collisions with reserved GitHub Pages paths.
+
+The main site is hosted at `maxkapur.com` via GitHub Pages, but I also have Pages
+set up for other repos, which automatically get mapped to paths like
+`maxkapur.com/jekyll-related`.
+
+This script queries the GitHub API to get any repos that have Pages configured,
+then checks that the site build doesn't have any content at the corresponding
+paths.
+"""
+
+import argparse
+from pathlib import Path
+
+import requests
+
+GH_USERNAME = "maxkapur"
+
+
+def main():
+ hugo_build_dir = get_build_dir()
+ if not (sentinel := hugo_build_dir / "index.html").exists():
+ raise FileNotFoundError(sentinel)
+
+ print("Querying GitHub API for reserved paths")
+ response = requests.get(f"https://api.github.com/users/{GH_USERNAME}/repos")
+ pages_names = [entry["name"] for entry in response.json() if entry.get("has_pages")]
+
+ print(
+ f"Checking build at {hugo_build_dir} against {len(pages_names)} reserved paths"
+ )
+ exceptions = []
+ for name in pages_names:
+ if (directory := hugo_build_dir / name).exists():
+ exceptions.append(IsADirectoryError(directory))
+ print(f"Reserved path {directory} does not exist")
+ if exceptions:
+ raise ExceptionGroup("One or more path collisions occured", exceptions)
+
+
+def get_build_dir():
+ parser = argparse.ArgumentParser()
+ parser.add_argument("--build-dir", default=Path(__file__).parent / "public")
+ return Path(parser.parse_args().build_dir)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/content/_index.md b/content/_index.md
new file mode 100644
index 00000000..833c136a
--- /dev/null
+++ b/content/_index.md
@@ -0,0 +1,6 @@
++++
+title = "Home"
+# Suppress default lorem ipsum content from theme
++++
+
+Welcome to my site. I have rearranged things a bit! Actually, [kind of a lot](https://github.com/maxkapur/maxkapur.github.io/pull/98). Please {{< mailto "email me" >}} if you can't find anything.
diff --git a/content/pages/_index.md b/content/pages/_index.md
new file mode 100644
index 00000000..6e76b7f4
--- /dev/null
+++ b/content/pages/_index.md
@@ -0,0 +1,4 @@
++++
+title = "Pages"
+# Suppress default lorem ipsum content from theme
++++
diff --git a/about.md b/content/pages/about.md
similarity index 84%
rename from about.md
rename to content/pages/about.md
index e63d5492..2749ae68 100644
--- a/about.md
+++ b/content/pages/about.md
@@ -1,9 +1,14 @@
----
-layout: page
-title: About
-sort_order: 10
-permalink: /about/
----
++++
+title = "About"
+menus = [ "main",]
+aliases = [ "/about/",]
+weight = 10
+date = 2024-12-01T11:22:03-06:00
+lastmod = 2025-03-16T13:21:35-04:00
+
+[params]
+id = "tag:max@maxkapur.com,2026-05-27:pages/About"
++++
Illusion Slopes is a personal blog. I write about whatever’s on my mind. My goal
is to document my thoughts and gain perspective on how they have evolved since
@@ -11,10 +16,12 @@ starting this site as a college student in 2016. I’m also interested in
connecting with readers who have similar life experiences or contrasting
viewpoints.
+
+
I’m enthusiastic about the indie web and the possibilities the internet holds as
a platform for personal expression and human connection—you know, what social
media was supposed to be. I try, on this site, to be maximally
-[authentic]({% post_url 2025-03-12-authenticity-ai-policy %}), which means no
+[authentic](/posts/2025-03-12-authenticity-ai-policy/), which means no
AI and no marketing.
Here’s what I look like:
@@ -48,10 +55,10 @@ video](https://www.youtube.com/playlist?list=PL06yMQdXWrp9VsynaPe2mg2gjHfr45VZO)
# Contact
-The best way to reach me is by emailing me at [{{ site.email }}](mailto:{{ site.email }}).
+The best way to reach me is by emailing me at [max@maxkapur.com](mailto:max@maxkapur.com).
You can also find me on
-[GitHub](https://github.com/{{ site.github_username }}),
-[LinkedIn](https://www.linkedin.com/in/{{ site.linkedin_username }}), and
+[GitHub](https://github.com/maxkapur),
+[LinkedIn](https://www.linkedin.com/in/maxkapur), and
[YouTube](https://www.youtube.com/channel/UCRfryFgD6JkqJiTA_MTkHXw).
I especially love hearing from applicants and current participants about the US
@@ -67,6 +74,6 @@ including but not limited to the CLS Program, the US Department of State,
American Councils for International Education, the Fulbright program, Seoul
National University, and their partners.
+
diff --git a/chipotle.md b/content/pages/chipotle.md
similarity index 77%
rename from chipotle.md
rename to content/pages/chipotle.md
index c739f48b..99592f59 100644
--- a/chipotle.md
+++ b/content/pages/chipotle.md
@@ -1,10 +1,14 @@
----
-layout: page
-title: Chipotle
-sort_order: 999
-hidden: true
-permalink: /chipotle/
----
++++
+title = "Chipotle"
+aliases = [ "/chipotle/",]
+menus = [ "footer",]
+weight = 999
+date = 2025-02-03T18:41:46-05:00
+lastmod = 2025-02-09T12:29:26-06:00
+
+[params]
+id = "tag:max@maxkapur.com,2026-05-27:pages/Chipotle"
++++
[IndieWebCamp wiki:](https://indieweb.org/chipotle)
@@ -12,6 +16,8 @@ permalink: /chipotle/
> you, you can send them a link instead of having to redescribe/retype your
> Chipotle order.
+
+
# Chipotle (to-do)
OK, so I have only actually been to Chipotle a few times and can’t remember what
diff --git a/colophon.md b/content/pages/colophon.md
similarity index 53%
rename from colophon.md
rename to content/pages/colophon.md
index 04bcec86..c9cca17b 100644
--- a/colophon.md
+++ b/content/pages/colophon.md
@@ -1,15 +1,22 @@
----
-layout: page
-title: Colophon
-hidden: true
-permalink: /colophon/
----
++++
+title = "Colophon"
+aliases = [ "/colophon/",]
+menus = [ "footer",]
+weight = 500
+date = 2025-02-03T19:16:30-05:00
+lastmod = 2025-02-09T12:29:26-06:00
+
+[params]
+id = "tag:max@maxkapur.com,2026-05-27:pages/Colophon"
++++
[IndieWebCamp wiki:](https://indieweb.org/colophon)
> A colophon … describes how the site is made, with what tools, supporting what
> technologies.
+
+
# Local tools
- [Micro](https://micro-editor.github.io/) (on desktop) and
@@ -22,21 +29,18 @@ permalink: /colophon/
# Fonts
- [IBM Plex](https://github.com/IBM/plex) family
-- Default $$\KaTeX$$ font
-- I have a stylesheet
- demo post
+- I have a [stylesheet demo post](/posts/1970-01-01-formats-masterpost/)
# Site build
-- Conda ([Miniforge](https://github.com/conda-forge/miniforge)) for environment
- management (why)
-- [Jekyll](https://jekyllrb.com/) v{{ jekyll.version }} to build the HTML
-- GitHub Actions (wrapping Bash scripts) for orchestration
+- [Hugo](https://gohugo.io/) to build the HTML
+- Python for maintenance scripts
+- [Nix](https://nix.dev/concepts/flakes.html) flake for development container
# Hosting
-- Web hosting: GitHub Pages free tier
-- Domain: Squarespace (automatically migrated from Google Domains)
+- Web hosting: GitHub Pages
+- Domain: Porkbun
# AI
diff --git a/projects.md b/content/pages/projects.md
similarity index 85%
rename from projects.md
rename to content/pages/projects.md
index 2d024369..8bae8919 100644
--- a/projects.md
+++ b/content/pages/projects.md
@@ -1,16 +1,15 @@
----
-layout: page
-title: Projects
-permalink: /projects/
-sort_order: 20
-redirect_from:
- - /publications/
----
-
-{%- comment -%}
- Redirect above is because I used to have separate pages for projects and
- publications.
-{%- endcomment -%}
++++
+title = "Projects"
+aliases = [ "/projects/", "/publications/",]
+menus = [ "main",]
+weight = 20
+date = 2024-12-01T11:22:03-06:00
+lastmod = 2025-10-07T19:07:29-05:00
+summary = "A list of some of my work projects, open-source contributions, and academic research."
+
+[params]
+id = "tag:max@maxkapur.com,2026-05-27:pages/Projects"
++++
# Professional projects
@@ -22,7 +21,7 @@ all my clients and work projects here on Illusion Slopes would undermine my goal
of maintaining a healthy separation between my personal and professional
identities. As a compromise, I hope this spiel from my [People and Blogs
interview](https://manuelmoreale.com/pb-max-kapur) ([local
-mirror]({% post_url 2025-02-14-people-and-blogs-local %})) will suffice as an
+mirror](/posts/2025-02-14-people-and-blogs-local/)) will suffice as an
introduction:
> At work, I build numerical modeling tools (picture something like [Universal
@@ -36,9 +35,9 @@ introduction:
> learned in music school, and my ability to translate specialized concepts into
> accessible terms.
-If you are interested in professional collaboration, please [email me](mailto:{{ site.email }})
+If you are interested in professional collaboration, please [email me](mailto:max@maxkapur.com)
or reach out on
-[LinkedIn](https://www.linkedin.com/in/{{ site.linkedin_username | cgi_escape | escape }}).
+[LinkedIn](https://www.linkedin.com/in/maxkapur).
# Open source contributions
@@ -63,13 +62,13 @@ attractive numerical properties. I estimate the model using an open dataset and
produce an intuitive ranking of American universities.
[“Cyborgs, Cyphers, and Feminist Compromise in Contemporary South Korean
-Science Fiction,”]({{ site.url }}/cyborgs-and-ciphers/) *Emory Journal of East
+Science Fiction,”](https://maxkapur.com/cyborgs-and-ciphers/) *Emory Journal of East
Asian Studies,* 2020. I examine two contemporary South Korean science fiction
authors and situate their complex relationship to the SF genre in the context of
a longer history of “bargains” that Korean feminist writers have made in search
of a literary platform.
-“ ‘The Shoes of the Trees’: How Trees Find Their Footing in Contemporary Korean Poetry,”
+“ ‘The Shoes of the Trees’: How Trees Find Their Footing in Contemporary Korean Poetry,”
BA honors thesis, May 2018. A study of the symbolic and discursive role of trees in Korean lyric poetry and ecocriticism.
[“The Making of the Kim Ku Mystique,”](https://www.scribd.com/document/364446649/OAPS-2016-2017)
@@ -79,12 +78,12 @@ activist’s high standing in contemporary Korean political and social thought.
# Essays
-[“Using Data to Understand ELL Students,”]({{ site.url }}/esl-data/)
+[“Using Data to Understand ELL Students,”](https://maxkapur.com/esl-data/)
August 2019. A tour of some rudimentary data analysis techniques that ELL
teachers can use to better understand their students.
[“How Do We Learn What’s True? Narrative Fluency and the Pedagogy of
-Information,”]({{ site.url }}/how-do-we/) February 2019. I created this digital
+Information,”](https://maxkapur.com/how-do-we/) February 2019. I created this digital
essay in response to what I saw as an unquestioning embrace of trendy
information-literacy pedagogies.
diff --git a/_posts/1970-01-01-formats-masterpost.md b/content/posts/1970-01-01-formats-masterpost.md
similarity index 85%
rename from _posts/1970-01-01-formats-masterpost.md
rename to content/posts/1970-01-01-formats-masterpost.md
index 2e4bfec8..143cfee3 100644
--- a/_posts/1970-01-01-formats-masterpost.md
+++ b/content/posts/1970-01-01-formats-masterpost.md
@@ -1,11 +1,16 @@
----
-layout: post
-title: "Formats masterpost"
-hidden: true
-permalink: /formatting/
----
++++
+title = "Formats masterpost"
+aliases = [ "/formatting/",]
+lastmod = 2026-06-28T11:09:09-05:00
-Here is a post with all kinds of crazy formatting so that I can test out my CSS.
+[params]
+hidden = true
+id = "https://maxkapur.com/1970/01/01/formats-masterpost"
++++
+
+Here is a post with all kinds of crazy formatting so that I can test out my CSS.
+
+
# Heading alpha
@@ -15,7 +20,9 @@ Make sure the keyboard element Ctrl + Alt + Delete
> Ab numquam optio cumque perspiciatis voluptatum quis accusantium, atque excepturi, laboriosam similique! Laboriosam sed dignissimos quasi non animi!
+## Simple lists
+
Things I like to eat:
- Apples
@@ -65,6 +74,21 @@ Items on my desk:
3. Coffee
4. Notepad
+List with internal paragraphs:
+
+- Aliquam ipsa ducimus laudantium, ab numquam optio cumque perspiciatis voluptatum quis accusantium, atque excepturi, laboriosam similique!
+
+ Laboriosam sed dignissimos quasi non animi!
+
+- Ab numquam optio cumque perspiciatis voluptatum quis accusantium, atque excepturi, laboriosam similique!
+
+Nested list:
+
+- I don't really
+ - Use these
+ - A lot
+- But might as well check
+
# Code
I’ve been using the following fish alias `fzl` (“fuzzy list”) to run `fzf` (“fuzzy find”) in the current directory with a `bat` preview. It’s just a couple of light tweaks to the examples listed in the `fzf` [readme](https://github.com/junegunn/fzf#preview-window).
@@ -166,16 +190,20 @@ end
In this post, we will show that functions of the form
-$$
+{{< math >}}
f(X) = 1 -
\prod_{i \in \Omega \setminus X} (1 - p_i)
\prod_{i \in X} (1 - q_i)
-$$
+{{< /math >}}
-are submodular for $$p_i, q_i \in [0, 1]$$ where each $$p_i \leq q_i,$$
+are submodular for {{< math "p_i, q_i \in [0, 1]" />}} where each {{< math "p_i \leq q_i" />}},
and examine an application of this small result that demonstrates its
practical value.[^footnote]
[^footnote]: Lorem ipsum dolor sit amet consectetur adipisicing elit. Aliquam ipsa ducimus laudantium, ab numquam optio cumque perspiciatis voluptatum quis accusantium, atque excepturi, laboriosam similique! Laboriosam sed dignissimos quasi non animi!
Ab numquam optio cumque perspiciatis voluptatum quis accusantium, atque excepturi, laboriosam similique! Laboriosam sed dignissimos quasi non animi!
+
+Footnote[^inlinefootnote] in the middle of a sentence.
+
+[^inlinefootnote]: Single-paragraph footnote.
diff --git a/_posts/2016-03-12-cls-korea.md b/content/posts/2016-03-12-cls-korea.md
similarity index 87%
rename from _posts/2016-03-12-cls-korea.md
rename to content/posts/2016-03-12-cls-korea.md
index cd0bb6e8..bea52f43 100644
--- a/_posts/2016-03-12-cls-korea.md
+++ b/content/posts/2016-03-12-cls-korea.md
@@ -1,7 +1,10 @@
----
-layout: post
-title: CLS Korean
----
++++
+title = "CLS Korean"
+aliases = [ "/2016/03/12/cls-korea.html",]
+
+[params]
+id = "https://maxkapur.com/2016/03/12/cls-korea"
++++
I am blessed and honored to have received a Critical Language Scholarship from the US Department of State to study Korean language this summer at Chonnam University. This will be my first visit to Korea, and I am incredibly excited as well as anxious about becoming a study abroad student for the first time.
diff --git a/_posts/2016-06-25-first-week-class-cnu.md b/content/posts/2016-06-25-first-week-class-cnu.md
similarity index 88%
rename from _posts/2016-06-25-first-week-class-cnu.md
rename to content/posts/2016-06-25-first-week-class-cnu.md
index 36ff0f47..33367ab2 100644
--- a/_posts/2016-06-25-first-week-class-cnu.md
+++ b/content/posts/2016-06-25-first-week-class-cnu.md
@@ -1,7 +1,10 @@
----
-layout: post
-title: First week of class at CNU
----
++++
+title = "First week of class at CNU"
+aliases = [ "/2016/06/25/first-week-class-cnu.html",]
+
+[params]
+id = "https://maxkapur.com/2016/06/25/first-week-class-cnu"
++++
I’ve just finished my first week of class at Chonnam National Universty here in Gwangju with CLS and everything is amazing! I am staying with a host family in a suburb about 40 minutes from school by bus, so I get to practice my Korean 24/7.
diff --git a/_posts/2016-08-07-sorites.md b/content/posts/2016-08-07-sorites.md
similarity index 98%
rename from _posts/2016-08-07-sorites.md
rename to content/posts/2016-08-07-sorites.md
index e3de75fd..05b9e97e 100644
--- a/_posts/2016-08-07-sorites.md
+++ b/content/posts/2016-08-07-sorites.md
@@ -1,7 +1,10 @@
----
-layout: post
-title: Sorites
----
++++
+title = "Sorites"
+aliases = [ "/2016/08/07/sorites.html",]
+
+[params]
+id = "https://maxkapur.com/2016/08/07/sorites"
++++
We pretend stability by tracking our life’s progress as a series of marginal changes, so that from each day to the next we can see that we are still ourselves, changed perhaps in substance, but never in identity.
diff --git a/_posts/2017-08-31-students-as-creators.md b/content/posts/2017-08-31-students-as-creators.md
similarity index 97%
rename from _posts/2017-08-31-students-as-creators.md
rename to content/posts/2017-08-31-students-as-creators.md
index 11d2d2d5..4150d3ef 100644
--- a/_posts/2017-08-31-students-as-creators.md
+++ b/content/posts/2017-08-31-students-as-creators.md
@@ -1,7 +1,10 @@
----
-layout: post
-title: Centering students as creators
----
++++
+title = "Centering students as creators"
+aliases = [ "/2017/08/31/students-as-creators.html",]
+
+[params]
+id = "https://maxkapur.com/2017/08/31/students-as-creators"
++++
In an influential article
reimagining
diff --git a/_posts/2018-05-02-fulbright-korea.md b/content/posts/2018-05-02-fulbright-korea.md
similarity index 66%
rename from _posts/2018-05-02-fulbright-korea.md
rename to content/posts/2018-05-02-fulbright-korea.md
index 9d314134..c797dc4c 100644
--- a/_posts/2018-05-02-fulbright-korea.md
+++ b/content/posts/2018-05-02-fulbright-korea.md
@@ -1,9 +1,12 @@
----
-layout: post
-title: Fulbright
----
++++
+title = "Fulbright"
+aliases = [ "/2018/05/02/fulbright-korea.html",]
-Fun fact: the most-viewed post on this blog, by a long shot, is the one where [I shared that I’d received a Critical Language Scholarship]({% post_url 2016-03-12-cls-korea %}) from the US Department of State to study Korean in Gwangju. Today, I’m pleased to announce there will soon be another addition to the State Department–mandated disclaimer at the bottom of this blog.
+[params]
+id = "https://maxkapur.com/2018/05/02/fulbright-korea"
++++
+
+Fun fact: the most-viewed post on this blog, by a long shot, is the one where [I shared that I’d received a Critical Language Scholarship](/posts/2016-03-12-cls-korea/) from the US Department of State to study Korean in Gwangju. Today, I’m pleased to announce there will soon be another addition to the State Department–mandated disclaimer at the bottom of this blog.
Beginning early this July, I’ll be spending a full year in South Korea as a Fulbright fellow. I’ll be serving as an English teaching assistant at the elementary level. After our six-week orientation in Seoul, I don’t yet know the city I’ll be working in. That said, though Korea isn’t a small country, it possesses an excellent transportation infrastructure. If you’ll be there at all during the next year, send me an email so we can catch up!
diff --git a/_posts/2018-08-25-a-thing-here.md b/content/posts/2018-08-25-a-thing-here.md
similarity index 96%
rename from _posts/2018-08-25-a-thing-here.md
rename to content/posts/2018-08-25-a-thing-here.md
index 5ba63e23..ab39eab1 100644
--- a/_posts/2018-08-25-a-thing-here.md
+++ b/content/posts/2018-08-25-a-thing-here.md
@@ -1,7 +1,10 @@
----
-layout: post
-title: Things that are a thing here
----
++++
+title = "Things that are a thing here"
+aliases = [ "/2018/08/25/a-thing-here.html",]
+
+[params]
+id = "https://maxkapur.com/2018/08/25/a-thing-here"
++++
They say that major lifestyle changes—even positive ones, like starting a new
job or moving—are one of the biggest stressors out there. By that measure, it’s
diff --git a/_posts/2019-10-18-using-data-ell.md b/content/posts/2019-10-18-using-data-ell.md
similarity index 74%
rename from _posts/2019-10-18-using-data-ell.md
rename to content/posts/2019-10-18-using-data-ell.md
index dcc95895..990d3911 100644
--- a/_posts/2019-10-18-using-data-ell.md
+++ b/content/posts/2019-10-18-using-data-ell.md
@@ -1,7 +1,10 @@
----
-layout: post
-title: Using data to understand ELL students
----
++++
+title = "Using data to understand ELL students"
+aliases = [ "/2019/10/18/using-data-ell.html",]
+
+[params]
+id = "https://maxkapur.com/2019/10/18/using-data-ell"
++++
In August, I surveyed my students about their motivation for learning
English, their opinions about our class and my teaching style, and
@@ -15,5 +18,5 @@ about that project at Fulbright Korea’s fall conference in
Gyeongju. Here’s
[a digital version of the talk](https://www.youtube.com/watch?v=jgAhgY3TVEc).
-And here’s a detailed
+And here’s a detailed
write-up of my survey design and results.
diff --git a/_posts/2020-01-02-houseplant-measurements.md b/content/posts/2020-01-02-houseplant-measurements.md
similarity index 75%
rename from _posts/2020-01-02-houseplant-measurements.md
rename to content/posts/2020-01-02-houseplant-measurements.md
index 9128b209..360e61e0 100644
--- a/_posts/2020-01-02-houseplant-measurements.md
+++ b/content/posts/2020-01-02-houseplant-measurements.md
@@ -1,7 +1,10 @@
----
-layout: post
-title: "Houseplant measurements"
----
++++
+title = "Houseplant measurements"
+aliases = [ "/2020/01/02/houseplant-measurements.html",]
+
+[params]
+id = "https://maxkapur.com/2020/01/02/houseplant-measurements"
++++
I have been measuring the leaves of my houseplants.
@@ -11,6 +14,7 @@ I have been measuring the leaves of my houseplants.
alt="alt"
/>
+
It is surprising how normal these distributions are.
@@ -36,4 +40,4 @@ I’m putting my stats notes
>
> I’ve updated the “my stats notes” link to point to the latest version of that Jupyter notebook in GitHub’s view of the Git source tree for this site, in the unlikely event that it is useful to anyone.
>
-> With four years of additional perspective, I would be remiss not to point out that leaf geometry in which both the area $$XY$$ and the aspect ratio $$X/Y$$ are both normally distributed would be serendipitous indeed—and that the plots above don’t look particularly linear to me anymore.
+> With four years of additional perspective, I would be remiss not to point out that leaf geometry in which both the area {{< math "XY" />}} and the aspect ratio {{< math "X/Y" />}} are both normally distributed would be serendipitous indeed—and that the plots above don’t look particularly linear to me anymore.
diff --git a/_posts/2020-01-19-what-you-learn-humanities.md b/content/posts/2020-01-19-what-you-learn-humanities.md
similarity index 93%
rename from _posts/2020-01-19-what-you-learn-humanities.md
rename to content/posts/2020-01-19-what-you-learn-humanities.md
index 2dde4e6a..cbd865f8 100644
--- a/_posts/2020-01-19-what-you-learn-humanities.md
+++ b/content/posts/2020-01-19-what-you-learn-humanities.md
@@ -1,7 +1,11 @@
----
-layout: post
-title: What you learn in a humanities degree
----
++++
+title = "What you learn in a humanities degree"
+aliases = [ "/2020/01/19/what-you-learn-humanities.html",]
+lastmod = 2026-05-27
+
+[params]
+id = "https://maxkapur.com/2020/01/19/what-you-learn-humanities"
++++
The more I learn, I find myself with fewer and fewer things to say.
@@ -17,7 +21,7 @@ When I was a student, my favorite theory was what we might call discursivism, a
Theories are quite powerful, because they allow us to convert small amounts of information into large amounts of information. (More words have been written about Shakespeare than Shakespeare ever wrote.) We use theories to put words into others’ mouths. For example, Americans tend to think of themselves as formidable amateur psychoanalysts. It is not uncommon to hear Americans dismiss each other by saying they are “projecting” or “deflecting” or “repressing” something. Psychoanalysis is a theory, a theory according to which we spend life restaging traumatic incidents from childhood in a hopeless search for resolution. Like many theories, psychoanalysis rejects the possibility of objective observation; thus you can talk in circles by telling your opponent, “No, I think you’re repressing!” Without realizing it, we are two levels of abstraction away from the issues at hand, but because culture has made us comfortable with pushing theories around like pieces on a chess board, both parties proceed in the illusion that material gains are at stake. In fact, when we pit theories against each other, what we are gaining is information: about the matter in question, yes, but also about the theories themselves, their breadth and depth.
-We cannot break free of theories. Whether or not you have received a traditional education, your thought patterns have been shaped by your experiences of people and your interactions with “texts.” A theory doesn’t need to have a Wikipedia entry for it to color your thoughts. Thus, one of the goals of a liberal education is to equip you with a wide range of theories, so that in an unfamiliar situation, you can try out various theories in succession as a corrective against your default narrative, the hope being that you will test out various responses and select the best one. However, being theoretically versatile—I have called this narrative fluency—is more likely to make us good at justifying our prior beliefs (whether or not they are correct) than at reexamining and overturning them.
+We cannot break free of theories. Whether or not you have received a traditional education, your thought patterns have been shaped by your experiences of people and your interactions with “texts.” A theory doesn’t need to have a Wikipedia entry for it to color your thoughts. Thus, one of the goals of a liberal education is to equip you with a wide range of theories, so that in an unfamiliar situation, you can try out various theories in succession as a corrective against your default narrative, the hope being that you will test out various responses and select the best one. However, being theoretically versatile—I have called this narrative fluency—is more likely to make us good at justifying our prior beliefs (whether or not they are correct) than at reexamining and overturning them.
Thus, I am finding myself with less and less to say, because I am paralyzed by the sense that if I manage to say something convincing, it will be so only because I have applied a theory in a novel or resonant way, and not because the underlying idea has merit. I have spent so much time playing with theories that nowadays, I sometimes myself testing facts against beliefs rather than beliefs against facts, so that when I encounter a set of facts that resist one of my beloved theories, I search for better facts instead of modifying my assumptions.
diff --git a/_posts/2020-04-16-accidental-signals.md b/content/posts/2020-04-16-accidental-signals.md
similarity index 91%
rename from _posts/2020-04-16-accidental-signals.md
rename to content/posts/2020-04-16-accidental-signals.md
index 5f488cbb..f39625f3 100644
--- a/_posts/2020-04-16-accidental-signals.md
+++ b/content/posts/2020-04-16-accidental-signals.md
@@ -1,9 +1,13 @@
----
-layout: post
-title: Accidental signals
----
++++
+title = "Accidental signals"
+aliases = [ "/2020/04/16/accidental-signals.html",]
+summary = "It is a well-known result in psychology that measures of intelligence correlate across domains. If you are good at math, you are probably also good at verbal reasoning, and vice versa. Therefore, if a company is hiring for a position that requires a variety of skills (as most jobs do), it should pick a candidate who has demonstrated impressive skill in one domain over someone who has demonstrated middling proficiency in a number of different domains."
-It is a well-known result in psychology that measures of intelligence correlate across domains. If you are good at math, you are probably also good at verbal reasoning, and vice versa. Therefore, if a company is hiring for a position that requires a variety of skills (as most jobs do), it should pick a candidate who has demonstrated impressive skill in one domain over someone who has demonstrated middling proficiency in a number of different domains.[^1]
+[params]
+id = "https://maxkapur.com/2020/04/16/accidental-signals"
++++
+
+It is a well-known result in psychology that measures of intelligence correlate across domains. If you are good at math, you are probably also good at verbal reasoning, and vice versa. Therefore, if a company is hiring for a position that requires a variety of skills (as most jobs do), it should pick a candidate who has demonstrated impressive skill in one domain over someone who has demonstrated middling proficiency in a number of different domains.[^1]
Hiring managers’ preference for specialists is widely known. So, smart people, who tend to be good at estimating job-market incentives (thanks to the correlation between domains of intelligence), gravitate toward programs that award narrow, in-demand credentials—even when the credentials are nominally equivalent (a BA in linguistics vs. a BA in English). This can create a feedback loop in which fields that are perceived as “challenging,” like computer science, attract challenge-seeking entrants, raising the standards of proficiency within the field and making it more difficult for a newcomer to rise above average. On the other hand, majors that are perceived as relatively easy, like sociology, may attract less-ambitious students, leading to deflated expectations and dissuading hard workers. (I have met a few people who told me that they majored in the liberal arts because they were “bad at math.” I have never met a STEM major who chose their field because they were “bad at writing.”)
diff --git a/_posts/2020-05-06-nonnative-teacher.md b/content/posts/2020-05-06-nonnative-teacher.md
similarity index 97%
rename from _posts/2020-05-06-nonnative-teacher.md
rename to content/posts/2020-05-06-nonnative-teacher.md
index f744103c..2aceafba 100644
--- a/_posts/2020-05-06-nonnative-teacher.md
+++ b/content/posts/2020-05-06-nonnative-teacher.md
@@ -1,7 +1,10 @@
----
-layout: post
-title: The benefits of a nonnative teacher
----
++++
+title = "The benefits of a nonnative teacher"
+aliases = [ "/2020/05/06/nonnative-teacher.html",]
+
+[params]
+id = "https://maxkapur.com/2020/05/06/nonnative-teacher"
++++
In addition to the myth that it’s impossible for adults to “really” learn a
foreign language, a widely held misconception about language learning is the
diff --git a/_posts/2020-07-07-next.md b/content/posts/2020-07-07-next.md
similarity index 95%
rename from _posts/2020-07-07-next.md
rename to content/posts/2020-07-07-next.md
index 9c47bd27..ace9bc90 100644
--- a/_posts/2020-07-07-next.md
+++ b/content/posts/2020-07-07-next.md
@@ -1,7 +1,10 @@
----
-layout: post
-title: Next
----
++++
+title = "Next"
+aliases = [ "/2020/07/07/next.html",]
+
+[params]
+id = "https://maxkapur.com/2020/07/07/next"
++++
They say that we perceive time by the accumulation of novel experiences, so that if you want to have a subjectively long life, you ought to do many spontaneous and hard-to-repeat things, but if you want to have a happy life, you ought to find one or two high pleasures that you can enjoy on a spiritual level and repeat the hell out of them, because they also say that on average, people derive more happiness from repeating a good experience than from trying something new.
diff --git a/_posts/2021-03-07-stable-matching-planet-money.md b/content/posts/2021-03-07-stable-matching-planet-money.md
similarity index 95%
rename from _posts/2021-03-07-stable-matching-planet-money.md
rename to content/posts/2021-03-07-stable-matching-planet-money.md
index 3aa2b39d..a168dc98 100644
--- a/_posts/2021-03-07-stable-matching-planet-money.md
+++ b/content/posts/2021-03-07-stable-matching-planet-money.md
@@ -1,7 +1,10 @@
----
-layout: post
-title: Stable matching on Planet Money
----
++++
+title = "Stable matching on Planet Money"
+aliases = [ "/2021/03/07/stable-matching-planet-money.html",]
+
+[params]
+id = "https://maxkapur.com/2021/03/07/stable-matching-planet-money"
++++
Planet Money, an NPR show about economics, recently ran an episode entitled “The Marriage Pact” that deals precisely with my research topic. It’s a great episode that discusses both the basic ideas behind stable assignment as well as its applications in organ donation, job placement, and (my area of focus) school choice.
diff --git a/_posts/2021-05-16-economic-view-korean-admissions.md b/content/posts/2021-05-16-economic-view-korean-admissions.md
similarity index 97%
rename from _posts/2021-05-16-economic-view-korean-admissions.md
rename to content/posts/2021-05-16-economic-view-korean-admissions.md
index b7dab368..6b8a0c52 100644
--- a/_posts/2021-05-16-economic-view-korean-admissions.md
+++ b/content/posts/2021-05-16-economic-view-korean-admissions.md
@@ -1,7 +1,10 @@
----
-layout: post
-title: An economic view of the Korean college admissions market
----
++++
+title = "An economic view of the Korean college admissions market"
+aliases = [ "/2021/05/16/economic-view-korean-admissions.html",]
+
+[params]
+id = "https://maxkapur.com/2021/05/16/economic-view-korean-admissions"
++++
Existing computational models of admissions markets tend to fall at one of two extremes: Either they envision a centralized admissions process in which the school board runs an algorithm that says which students go where, or a decentralized process in which colleges compete for the best students. But the Korean college admissions process cannot be adequately described in either of these terms.
diff --git a/_posts/2021-10-21-administrative-allocative-efficiency.md b/content/posts/2021-10-21-administrative-allocative-efficiency.md
similarity index 84%
rename from _posts/2021-10-21-administrative-allocative-efficiency.md
rename to content/posts/2021-10-21-administrative-allocative-efficiency.md
index 27167eb0..1bbda975 100644
--- a/_posts/2021-10-21-administrative-allocative-efficiency.md
+++ b/content/posts/2021-10-21-administrative-allocative-efficiency.md
@@ -1,7 +1,10 @@
----
-layout: post
-title: Administrative vs. allocative efficiency
----
++++
+title = "Administrative vs. allocative efficiency"
+aliases = [ "/2021/10/21/administrative-allocative-efficiency.html",]
+
+[params]
+id = "https://maxkapur.com/2021/10/21/administrative-allocative-efficiency"
++++
This fall marks my final semester of coursework, and penultimate semester overall, of the master’s course in industrial engineering here at Seoul National University. I’m taking courses in combinatorial optimization and advanced microeconomics, as well as continuing my study of college admissions markets as a research assistant in the Management Science/Optimization Lab.
@@ -9,7 +12,7 @@ Recently, I have been focusing on risk-averse behavior in college applications.
-In Korea, such strategic behavior is [baked into the college admissions process]({% post_url 2021-05-16-economic-view-korean-admissions %}) at the policy level: No student may apply to more than six colleges in a given year, and colleges are allowed to adopt more generous admissions standards for students who list the college as their first choice. A student who wishes to apply to a top university therefore needs more than just confidence in her academic talent; she must also be willing to accept the risk of being rejected and ending up at the bottom of the pile at her second choice.
+In Korea, such strategic behavior is [baked into the college admissions process](/posts/2021-05-16-economic-view-korean-admissions/) at the policy level: No student may apply to more than six colleges in a given year, and colleges are allowed to adopt more generous admissions standards for students who list the college as their first choice. A student who wishes to apply to a top university therefore needs more than just confidence in her academic talent; she must also be willing to accept the risk of being rejected and ending up at the bottom of the pile at her second choice.
We know, however, that risk aversion is not homogeneous across the population. Some of the variance is just temperamental: There are people who will continue working at an unsatisfying job because they fear that whatever job they switch to will be even worse, and there are other people who happily take the leap (paywall). But some of the variance in risk aversion is correlated with demographic traits. For example, all other things being equal, women are less likely than men to guess randomly on a hard multiple-choice test like the SAT that penalizes wrong answers (paywall). And we know that kids from low-income backgrounds tend to fail the marshmallow test; perhaps it is not stunted patience, but deliberate, risk-mitigating behavior that makes them favor the sure bet.
diff --git a/_posts/2022-04-14-school-location-problem.md b/content/posts/2022-04-14-school-location-problem.md
similarity index 70%
rename from _posts/2022-04-14-school-location-problem.md
rename to content/posts/2022-04-14-school-location-problem.md
index e976fb2c..2bb888a4 100644
--- a/_posts/2022-04-14-school-location-problem.md
+++ b/content/posts/2022-04-14-school-location-problem.md
@@ -1,13 +1,16 @@
----
-layout: post
-title: The school location problem
----
++++
+title = "The school location problem"
+aliases = [ "/2022/04/14/school-location-problem.html",]
+
+[params]
+id = "https://maxkapur.com/2022/04/14/school-location-problem"
++++
I’ve spent a few days thinking about a facility location problem that
-we might call the *school location problem.* The goal is to place $$n$$
-schools to serve $$m$$ families, such that the *furthest* distance
+we might call the *school location problem.* The goal is to place {{< math "n" />}}
+schools to serve {{< math "m" />}} families, such that the *furthest* distance
between any family and their *nearest* school is *minimized.* This
-problem is almost a $$k$$-means or ellipsoid fitting problem, but its
+problem is almost a {{< math "k" />}}-means or ellipsoid fitting problem, but its
unusual “minimin” form makes it computationally challenging.
In the video below, and associated Jupyter notebook ([GitHub](https://gist.github.com/maxkapur/e907289b457ebee8e3a191cbda7f7381), [HTML viewer](https://nbviewer.org/urls/gist.githubusercontent.com/maxkapur/e907289b457ebee8e3a191cbda7f7381/raw/37cd78433076b0f880ef3d645a93ef278abce33d/SchoolLocation.ipynb)), I discuss the formulation of this problem, show to express it as a mixed-integer second-order convex program, and then solve a small instance using the Julia/JuMP/Juniper/SCS stack.
diff --git a/_posts/2022-05-04-college-application-on-arxiv.md b/content/posts/2022-05-04-college-application-on-arxiv.md
similarity index 86%
rename from _posts/2022-05-04-college-application-on-arxiv.md
rename to content/posts/2022-05-04-college-application-on-arxiv.md
index b85345de..27cb97cb 100644
--- a/_posts/2022-05-04-college-application-on-arxiv.md
+++ b/content/posts/2022-05-04-college-application-on-arxiv.md
@@ -1,7 +1,10 @@
----
-layout: post
-title: "“College Application” on arXiv"
----
++++
+title = "“College Application” on arXiv"
+aliases = [ "/2022/05/04/college-application-on-arxiv.html",]
+
+[params]
+id = "https://maxkapur.com/2022/05/04/college-application-on-arxiv"
++++
In advance of a conference I will be attending with my labmates next month in Jeju, my thesis advisor and I have posted a preliminary version of [“The College Application Problem” on arXiv](https://arxiv.org/abs/2205.01869). Here’s the abstract:
diff --git a/content/posts/2022-05-13-college-application-presentation-korean.md b/content/posts/2022-05-13-college-application-presentation-korean.md
new file mode 100644
index 00000000..0a444ec8
--- /dev/null
+++ b/content/posts/2022-05-13-college-application-presentation-korean.md
@@ -0,0 +1,11 @@
++++
+title = "The college application problem, presented in Korean"
+aliases = [ "/2022/05/13/college-application-presentation-korean.html",]
+
+[params]
+id = "https://maxkapur.com/2022/05/13/college-application-presentation-korean"
++++
+
+I gave a brief
+[presentation about the college application problem](https://www.youtube.com/watch?v=tZCSwCPd1Qc)
+at a research fair hosted by our department. English subtitles are included.
diff --git a/_posts/2022-06-19-thesis-defense.md b/content/posts/2022-06-19-thesis-defense.md
similarity index 71%
rename from _posts/2022-06-19-thesis-defense.md
rename to content/posts/2022-06-19-thesis-defense.md
index 0bdfb876..e1cac4d5 100644
--- a/_posts/2022-06-19-thesis-defense.md
+++ b/content/posts/2022-06-19-thesis-defense.md
@@ -1,9 +1,12 @@
----
-layout: post
-title: "Thesis defense"
----
++++
+title = "Thesis defense"
+aliases = [ "/2022/06/19/thesis-defense.html",]
-This week, I defended my master’s thesis at Seoul National University. My thesis concerns an NP-hard portfolio optimization problem that I call the college application problem. My slides, presentation script, and the thesis itself live on [this GitHub repository](https://github.com/maxkapur/CollegeApplication), and all are provided in both English and Korean versions. I also have a very brief deck of [reveal.js slides]({{ site.url }}/CollegeApplication/) introducing the problem, and I recently wrote some [documentation for OptimalApplication.jl]({{ site.url }}/OptimalApplication.jl/stable/), the Julia implementation of my solution algorithms.
+[params]
+id = "https://maxkapur.com/2022/06/19/thesis-defense"
++++
+
+This week, I defended my master’s thesis at Seoul National University. My thesis concerns an NP-hard portfolio optimization problem that I call the college application problem. My slides, presentation script, and the thesis itself live on [this GitHub repository](https://github.com/maxkapur/CollegeApplication), and all are provided in both English and Korean versions. I also have a very brief deck of [reveal.js slides](https://maxkapur.com/CollegeApplication/) introducing the problem, and I recently wrote some [documentation for OptimalApplication.jl](https://maxkapur.com/OptimalApplication.jl/stable/), the Julia implementation of my solution algorithms.
The defense process was relatively painless: I received some very helpful comments and reference suggestions from the professors on my committee, and I was pleased that they were persuaded by my argument that the model of the college admissions process I have chosen represents the best tradeoff between realism and tractability.
diff --git a/_posts/2022-06-25-migrating-to-jekyll.md b/content/posts/2022-06-25-migrating-to-jekyll.md
similarity index 94%
rename from _posts/2022-06-25-migrating-to-jekyll.md
rename to content/posts/2022-06-25-migrating-to-jekyll.md
index 9853e458..1cf93f40 100644
--- a/_posts/2022-06-25-migrating-to-jekyll.md
+++ b/content/posts/2022-06-25-migrating-to-jekyll.md
@@ -1,8 +1,10 @@
----
-layout: post
-title: "Migrating to Jekyll"
----
++++
+title = "Migrating to Jekyll"
+aliases = [ "/2022/06/25/migrating-to-jekyll.html",]
+[params]
+id = "https://maxkapur.com/2022/06/25/migrating-to-jekyll"
++++
After reflecting on my unsustainable dependency on Google services, I have decided to bite
the bullet and migrate my blog from Blogger to the open-source [Jekyll](https://github.com/jekyll/jekyll),
@@ -51,4 +53,4 @@ Jekyll offers some tools for automatically importing past posts from other blogg
# New license
-The previous instance of Illusion Slopes featured a prominent copyright notice in the footer. Going forward, this blog will be licensed under CC BY-SA 4.0, a copyleft license that grants you the right to remix and reuse these materials without asking for permission as long as you credit me (preferably by linking to [maxkapur.com]({{ site.url }})) and release your work under a similar open license.
+The previous instance of Illusion Slopes featured a prominent copyright notice in the footer. Going forward, this blog will be licensed under CC BY-SA 4.0, a copyleft license that grants you the right to remix and reuse these materials without asking for permission as long as you credit me (preferably by linking to [maxkapur.com](https://maxkapur.com)) and release your work under a similar open license.
diff --git a/_posts/2022-07-01-opening-pandoras-box.md b/content/posts/2022-07-01-opening-pandoras-box.md
similarity index 96%
rename from _posts/2022-07-01-opening-pandoras-box.md
rename to content/posts/2022-07-01-opening-pandoras-box.md
index 1f4e3e9d..f2b13f2f 100644
--- a/_posts/2022-07-01-opening-pandoras-box.md
+++ b/content/posts/2022-07-01-opening-pandoras-box.md
@@ -1,7 +1,10 @@
----
-layout: post
-title: Opening Pandora’s Box
----
++++
+title = "Opening Pandora’s Box"
+aliases = [ "/2022/07/01/opening-pandoras-box.html",]
+
+[params]
+id = "https://maxkapur.com/2022/07/01/opening-pandoras-box"
++++
A harrowing thing that happens in research is that occasionally, you stumble upon a
[paper from 1979 (paywall)](https://doi.org/10.2307/1910412) that appears to solve
@@ -15,6 +18,7 @@ difference: The Pandora’s Box problem has a time dimension, whereas the colleg
problem is static.
The unusual thing is that the static problem appears more difficult.
+
## College application vs. the Pandora’s Box problem
diff --git a/_posts/2022-07-15-srt-delay.md b/content/posts/2022-07-15-srt-delay.md
similarity index 90%
rename from _posts/2022-07-15-srt-delay.md
rename to content/posts/2022-07-15-srt-delay.md
index 8a913fb7..f85deb1b 100644
--- a/_posts/2022-07-15-srt-delay.md
+++ b/content/posts/2022-07-15-srt-delay.md
@@ -1,14 +1,19 @@
----
-layout: post
-title: Python utility for manipulating subtitle timings
----
++++
+title = "Python utility for manipulating subtitle timings"
+aliases = [ "/2022/07/15/srt-delay.html",]
+
+[params]
+id = "https://maxkapur.com/2022/07/15/srt-delay"
++++
One of my current projects involves editing several hours’ worth of subtitle files in the
`.srt` format to accompany an online course in numerical optimization. Because we haven’t
decided exactly how we want to break up the videos, I needed an efficient way to delay
or advance all the subtitles in time, so I wrote a small Python program called `srt_delay.py` today to help me with this task.
-Usage examples below the cut, or you can just go straight to the `README` in the [GitHub repository](https://github.com/maxkapur/srt_delay).
+Usage examples below the cut, or you can just go straight to the `README` in the [GitHub repository](https://github.com/maxkapur/srt_delay).
+
+
## Usage
diff --git a/_posts/2022-08-02-mulberry.md b/content/posts/2022-08-02-mulberry.md
similarity index 77%
rename from _posts/2022-08-02-mulberry.md
rename to content/posts/2022-08-02-mulberry.md
index 2b8fd64c..51621ac8 100644
--- a/_posts/2022-08-02-mulberry.md
+++ b/content/posts/2022-08-02-mulberry.md
@@ -1,14 +1,18 @@
----
-layout: post
-title: "Mulberry: A college application strategist"
----
++++
+title = "Mulberry: A college application strategist"
+aliases = [ "/2022/08/02/mulberry.html",]
-I recently created [Mulberry]({{ site.url }}/mulberry), a web app students
+[params]
+id = "https://maxkapur.com/2022/08/02/mulberry"
++++
+
+I recently created [Mulberry](https://maxkapur.com/mulberry), a web app students
can use to compute their optimal college application strategy. It is a free,
open-source, user-friendly interface to Algorithm 1 from my
[MS thesis](https://github.com/maxkapur/CollegeApplication).
+
Why Mulberry? There are boatloads of “chance me!” websites (some more trustworthy
@@ -23,5 +27,5 @@ each school and the amount of utility you associate with going there,
and Mulberry sorts the schools in the order you should apply
to them to maximize your expected utility.
-The interface is at [maxkapur.com/mulberry]({{ site.url }}/mulberry),
+The interface is at [maxkapur.com/mulberry](https://maxkapur.com/mulberry),
and the code is on [GitHub](https://github.com/maxkapur/mulberry).
diff --git a/_posts/2022-08-18-multiple-selection.md b/content/posts/2022-08-18-multiple-selection.md
similarity index 67%
rename from _posts/2022-08-18-multiple-selection.md
rename to content/posts/2022-08-18-multiple-selection.md
index 019a63ed..5dcb14c9 100644
--- a/_posts/2022-08-18-multiple-selection.md
+++ b/content/posts/2022-08-18-multiple-selection.md
@@ -1,7 +1,10 @@
----
-layout: post
-title: "Grading multiple-selection questions"
----
++++
+title = "Grading multiple-selection questions"
+aliases = [ "/2022/08/18/multiple-selection.html",]
+
+[params]
+id = "https://maxkapur.com/2022/08/18/multiple-selection"
++++
Consider the following test question:
@@ -14,7 +17,8 @@ Consider the following test question:
>
Memphis
>
-This is a multiple-selection question, so the correct answer is `a` and `b`. How would you grade a question [like this]({% post_url 2020-05-06-nonnative-teacher %}) on an exam?
+This is a multiple-selection question, so the correct answer is `a` and `b`. How would you grade a question [like this](/posts/2020-05-06-nonnative-teacher/) on an exam?
+
When I was in school, the Scantron machines we used supported these kinds of multiple-selection questions, but they were graded all or nothing: You got one point if your answer matched the key exactly, and zero points otherwise. I have it on good authority that the MOOC site [edX](https://www.edx.org/) works the same way.
@@ -31,9 +35,9 @@ A student who answers `a` is clearly *more correct* than a student who answers `
## An alternative to all-or-nothing grading
-I propose grading multiple-selection problems as $$n$$ true-or-false questions, each weighted $$1/n$$ points. In this example, a response of `a` is correct with respect to questions a, c, and d, and receives 0.75 points. A response of `c` and `d` is correct with respect to none of the items and receives zero points.
+I propose grading multiple-selection problems as {{< math "n" />}} true-or-false questions, each weighted {{< math "1/n" />}} points. In this example, a response of `a` is correct with respect to questions a, c, and d, and receives 0.75 points. A response of `c` and `d` is correct with respect to none of the items and receives zero points.
-What about students who guess randomly? A student who guesses randomly receives, in expectation, 0.5 points under my proposal and $$1/ 2^n$$ points under the all-or-nothing method. Therefore, one objection to my proposal is that it provides students with a strong incentive to guess. I contend that this is not a real problem, because
+What about students who guess randomly? A student who guesses randomly receives, in expectation, 0.5 points under my proposal and {{< math "1/ 2^n" />}} points under the all-or-nothing method. Therefore, one objection to my proposal is that it provides students with a strong incentive to guess. I contend that this is not a real problem, because
- The incentive for students to guess is still positive under all-or-nothing grading,
- In creating examinations, there is no objective reason to make “discourage students from guessing” a design goal,
@@ -45,4 +49,4 @@ Wrong-answer penalties, however, are a [potential source of bias (open-access ar
Multiple-selection questions are not very user friendly to begin with. Some students fail to realize that they are allowed to choose more than one answer and agonize over technicalities (*Maybe he means Washington, DC?*). Others (mistakenly) *assume* that they will be graded all or nothing, recognize the slim chances of success, and just skip the question to pursue lower-hanging fruit.
-Therefore, as long as you accept my argument that multiple-selection questions should be regarded as $$n$$ true-or-false questions for grading purposes, why not simply write them out that way on the test sheet? Then there is no way to misread the question as a single selection, and the grading scheme is obvious.
+Therefore, as long as you accept my argument that multiple-selection questions should be regarded as {{< math "n" />}} true-or-false questions for grading purposes, why not simply write them out that way on the test sheet? Then there is no way to misread the question as a single selection, and the grading scheme is obvious.
diff --git a/content/posts/2022-09-01-apply-fifty-colleges.md b/content/posts/2022-09-01-apply-fifty-colleges.md
new file mode 100644
index 00000000..ecb6337a
--- /dev/null
+++ b/content/posts/2022-09-01-apply-fifty-colleges.md
@@ -0,0 +1,138 @@
++++
+title = "Apply to fifty colleges"
+aliases = [ "/2022/09/01/apply-fifty-colleges.html",]
+summary = "In this post, we’ll use fake numbers and a simplified model to argue that the most college applicants should apply to far more colleges than they actually do. You can probably extend the argument to job applications and the dating game. I’ll also compute a few fake comparative statics and speculate about why real students don’t apply to more schools."
+
+[params]
+id = "https://maxkapur.com/2022/09/01/apply-fifty-colleges"
++++
+
+In this post, we’ll use fake numbers and a simplified model to argue that the most college applicants should apply to far more colleges than they actually do. You can probably extend the argument to job applications and the dating game. I’ll also compute a few fake comparative statics and speculate about why real students don’t apply to more schools.[^obvious]
+
+[^obvious]: We will avoid the obvious argument that goes, “If you are willing to spend six figures on an a college education, then what’s a few hundred extra in application fees?” This argument is incorrect, because it assumes that each additional application is worth the value of attending that college times the probability of getting in. In actuality, you can only attend *one* of the colleges you get into; thus additional applications make a sublinear contribution to the overall expected utility. Our model will account for this.
+
+## Fake numbers and a simplified model
+
+We’ll start by making a few simplifying, conservative assumptions about the college admissions process. Here “conservative” means that we will err on the side of *understating* the value of applying to college, thereby arriving at an *underestimate* of the optimal number of college applications. Thus, our choices make it challenging to arrive at this post’s headline conclusion, such that if we *nonetheless* draw the conclusion that students should apply to more colleges, we have some degree of confidence that this statement applies to the actual numbers, too.
+
+First, let’s assume that you always have the option of attending a “safety school.” The safety school could also represent a community college or entering the workforce directly after high school. In addition, there are many “reach schools,” or elite universities, with competitive admissions. It is better to attend a reach school than the safety school, but applying to a reach school takes time and effort.
+
+Let {{< math "t" />}} denote the utility of attending an elite university, measured in monetary units relative to the safety school. By a conservative estimate, an elite university might offer us an expected salary increase of \$20,000 per year, at a tuition cost of \$30,000 dollars per year higher than the safety school. Thus, aggregating over 40 years of employment and 5 years of college attendance, we can estimate
+
+{{< math >}}
+t = 40 \times 20000 - 5 \times 30000 = 650000.
+{{< /math >}}
+
+Let’s assume, for simplicity, that all the reach schools are equally competitive, and let {{< math "p" />}} denote the probability of getting into an elite university. Assume that admissions at each school are probabilistically independent. Let’s pick {{< math "p = 0.1" />}}, which is near the acceptance rate (and therefore, the average admissions probability across all applicants) at many elite US universities. If you apply to {{< math "x" />}} reach schools, the probability of getting into *at least* one is {{< math "1 - (1 - p)^x" />}}, in which case you receive {{< math "t" />}} units of utility.
+
+However, applying to college is not free: You have to pay an application fee, various clerical fees for submitting transcripts and test scores, and then there are time costs associated with writing essays, requesting recommendation letters, and filling out the online form. Let {{< math "c" />}} denote the cost of applying to a reach school, so the cost of applying to {{< math "x" />}} schools is {{< math "cx" />}}, and the expected utility of the entire college application process can be written as follows:[^sublinear]
+
+[^sublinear]: Note that our assumption that the cost is linear in {{< math "x" />}} is conservative: In reality, since you can recycle essays and recommendation letters, the marginal costs diminish as {{< math "x" />}} increases; a function like {{< math "c \ln x" />}} or {{< math "c \sqrt{x}" />}} is arguably more realistic.
+
+{{< math >}}
+f(x) = t\bigl(1 - (1 - p)^x\bigr) - cx.
+{{< /math >}}
+
+Typical college application fees are \$50 or so; let’s take {{< math "c = 200" />}} to account generously for the other costs. Our goal is to maximize {{< math "f(x)" />}}.
+
+## The model says apply to fifty colleges
+
+With these values of {{< math "t" />}}, {{< math "p" />}}, and {{< math "c" />}}, here’s the graph of {{< math "f(x)" />}}. As you can see, the maximum occurs at about {{< math "x^* = 55" />}} applications!
+
+
+
+We can solve for the maximum analytically using a method you might have learned in high school calculus: {{< math "x^*" />}} represents the point where {{< math "f(x)" />}} stops increasing and starts decreasing. That is, the point where its slope {{< math "f'(x)" />}} is zero.
+
+With a little bit of legwork, you can check that the derivative of {{< math "f(x)" />}} is
+
+{{< math >}}
+f'(x) = -t (1-p)^m \ln(1-p) - c
+{{< /math >}}
+
+and setting this equal to zero and solving for {{< math "x" />}} yields
+
+{{< math >}}
+x^* = \frac{\ln c - \ln t - \ln \bigl(- \ln\left(1-p\right)\bigr)}{\ln\left(1-p\right)}.
+{{< /math >}}
+
+(It’s a pleasant surprise to encounter a nested logarithm somewhere other than computer science.) Plug in {{< math "t" />}}, {{< math "p" />}}, and {{< math "c" />}} to obtain {{< math "x^* = 55" />}}, as promised.[^exact]
+
+[^exact]: The exact answer is {{< math "x^* = 55.39" />}}, but you can only apply to a whole number of colleges. Since {{< math "f(x)" />}} is a concave, univariate function, the optimal integer solution has to be either 55 or 56, and you can verify that {{< math "f(55) > f(56)" />}}. We’ll use the same approach for the rest of the post.
+
+## Checking our work with comparative statics
+
+The reflexive economist move is to stick the expression for {{< math "x^*" />}} in a [Desmos box](https://www.desmos.com/calculator/yhxxla3wko) as follows, start dragging the sliders around, and see if we can spot any patterns. For example, click the “edit graph on Desmos” box and try increasing the value of {{< math "c" />}} slightly: You should see the vertical line, which represents the optimal number of applications, move to the left.
+
+
+
+If we’re feeling especially inspired, we might take the derivative of {{< math "x^*" />}} in each of the parameters. Computing these so-called comparative statics will help us verify that our “conservative” assumptions are actually conservative in the way that we think we are. For example,
+
+{{< math >}}
+\frac{\partial x^*}{\partial c} = \frac{1}{c \ln (1- p)} < 0
+{{< /math >}}
+
+(since {{< math "1-p" />}} is less than one, its logarithm is negative) tells us that increasing the costs *decreases* the optimal number of applications. But (in my opinion) the linear function {{< math "cx" />}} is an *overestimate* of the cost, and the true optimum {{< math "x^*" />}} should be even higher: conservatism fulfilled.
+
+Similarly, {{< math "\partial x^* / \partial t > 0" />}}, meaning that in the [(probable)](https://money.com/wage-gap-college-high-school-grads/) case that an elite college degree is worth more than \$650k, the central argument of this post is even truer.
+
+{{< math "p" />}} is where we get into trouble. I lowballed {{< math "p" />}} above on the assumption that the “value” of a college application correlates with how likely it is to yield an admission letter. However, the *marginal value* of a college application is actually greatest when colleges are hard to get into, and therefore so is your willingness to invest in an additional application. In other words, I should have tried to overshoot {{< math "p" />}} instead.
+
+To understand this counterintuitive result, let’s compute
+
+{{< math >}}
+\frac{\partial x^*}{\partial p} = \frac{ \ln{c} - \ln{t} - \ln{\bigl(- \ln{\left(1 - p \right)} \bigr)} + 1}{\left(1 - p\right) \ln^2{\left(1 - p \right)}},
+{{< /math >}}
+
+whose sign tells us whether the optimal number of applications is increasing or decreasing in the admissions odds {{< math "p" />}}. The denominator of this expression is positive, so the overall sign depends on the sign of the numerator, which is positive only when
+
+{{< math >}}
+p < 1 - \exp(-ce / t).
+{{< /math >}}
+
+But in college admissions, this is seldom the case: Typically, {{< math "t" />}} is much larger than {{< math "c" />}}, meaning that {{< math "1 - \exp(-ce / t) \approx 1 - \exp(0) = 0" />}}, breaking the condition. Lowballing {{< math "p" />}} is only conservative if {{< math "p" />}} is very small in the first place, and {{< math "c" />}} is large relative to {{< math "t" />}}.[^exceptionalregime]
+
+[^exceptionalregime]: For example, to see a regime where {{< math "x^*" />}} is increasing in {{< math "p" />}}, try setting {{< math "t = 10000" />}} and {{< math "c = 1000" />}} in the Desmos box, and vary {{< math "p" />}} between 0.1 and 0.2.
+
+## Oops, apply to thirty colleges
+
+So, if we are being conservative, we should try to *overestimate* {{< math "p" />}}. If we double our previous estimate to {{< math "p = 0.20" />}} and repeat the calculation above, we get a more modest result of {{< math "x^* = 30" />}}. But this is still much higher than the number of colleges real students apply to, right?
+
+We have to increment {{< math "p" />}} much further before {{< math "x^*" />}} starts to align with typical applicant behavior: When {{< math "p = 0.7" />}}, for instance, you get {{< math "x^* = 7" />}}. Now, you might object that {{< math "p = 0.7" />}} sounds like a perfectly reasonable admissions probability for an ambitious student. To this, I offer two responses:
+
+1. As someone who reads blog posts about economics in their free time, your social circle may have instilled in you an biased sense of what is achievable for the typical student.
+2. *Some* students have high admissions odds, but others don’t: The odds have to average out to around 10 percent for elite colleges to maintain acceptance rates in that neighborhood.
+
+In other words, our comparative statics analysis revealed an error in our so-called conservative fake numbers, but the error wasn’t quite large enough to undermine our central argument.
+
+## Why don’t students apply to more colleges?
+
+Most students apply to ten or so colleges. Why not more? What do they know that our model doesn’t?
+
+One idea that might come to mind if you’ve played with [Mulberry](https://maxkapur.com/mulberry/) is that most students apply not just to elite schools and a safety school, but also to a continuum of “target schools” in between. Target schools have higher admissions odds, and indeed make up the [bulk of the admissions market](https://www.pewresearch.org/fact-tank/2019/04/09/a-majority-of-u-s-colleges-admit-most-students-who-apply/). This observation isn’t enough to explain real students’ small application portfolios, however, because elite universities also are (perceived as) much more valuable than target schools.
+
+The model above can also be applied to a student who has already applied to safety and target schools and is trying to decide how many reach schools to add to her portfolio: Simply discount {{< math "t" />}} to reflect the value of a degree from an elite school relative to the (actuarial) value of a degree from a target school. Conservatively, if we hack {{< math "t" />}} all the way down to \$200k, take {{< math "p = 0.2" />}}, and {{< math "c = 200" />}}, we *still* get a hefty {{< math "x^* = 24" />}}.
+
+Here’s what I think is really going on: Students don’t apply to zillions of colleges because they aren’t optimization machines. When I was a senior in high school, I categorically refused to apply to more than five colleges because I didn’t want to look like a tryhard.[^idw] The psychological costs of college application are *really large,* because they include not only the exhaustion of composing and submitting applications, but also the cost of *imagining* the disappointment of being rejected from a dream school.
+
+[^idw]: It didn’t work.
+
+## Revealed cost of college application
+
+What, in dollar terms, is the value of the subjective cost students associate with college application, after taking into account this risk aversion? Let’s stick with our conservative {{< math "t = 200000" />}} and {{< math "p = 0.2" />}} scenario, and turn the question around: Given that most students apply to about a dozen colleges, what application cost {{< math "c" />}} makes this number of applications optimal?
+
+To figure this out, we just need to solve our {{< math "x^*" />}} equation for {{< math "c" />}}. The answer is
+
+{{< math >}}
+c = - t (1 - p)^x \log{\left(1 - p \right)}
+{{< /math >}}
+
+and plugging in the numbers yields {{< math "c = 3067" />}} dollars per application.
+
+Remember: These numbers are fake. But we have made a plausible argument that the majority of the costs incurred in the college application process are psychological, rather than material. If you can overcome the psychological costs, then by applying to thirty or fifty colleges, you may be able to reap significant material rewards.
+
+## Reader exercises
+
+- Repeat the calculations above for another situation, such as job application. How many jobs should you apply to? Is the model a good choice?
+- Identify a regime in which {{< math "\partial x^* / \partial p" />}} is positive and explain, in qualitative terms, why this is the case.
+- If every student adopts the optimal application strategy, then everyone might apply to twice as many schools. But then each school will become twice as competitive, changing the admissions odds and therefore the optimal strategies. Can you sketch the equilibrium criteria for this market? What other information would you need? [(Paywalled academic paper about this.)](https://www.journals.uchicago.edu/doi/10.1086/675503)
+- Suppose that the admissions probabilities and utility values for the reach schools are all different. What is the utility expression in this case? *Hint:* Replace {{< math "x" />}} with a set indicating which colleges you apply to. You can find the answer [here](https://maxkapur.com/mulberry/) or in my [master’s thesis](https://github.com/maxkapur/CollegeApplication).
diff --git a/_posts/2022-09-04-masters-graduation.md b/content/posts/2022-09-04-masters-graduation.md
similarity index 89%
rename from _posts/2022-09-04-masters-graduation.md
rename to content/posts/2022-09-04-masters-graduation.md
index 2cefb9fa..78908e10 100644
--- a/_posts/2022-09-04-masters-graduation.md
+++ b/content/posts/2022-09-04-masters-graduation.md
@@ -1,13 +1,18 @@
----
-layout: post
-title: "Master’s graduation and two free optimization courses"
----
++++
+title = "Master’s graduation and two free optimization courses"
+aliases = [ "/2022/09/04/masters-graduation.html",]
+
+[params]
+id = "https://maxkapur.com/2022/09/04/masters-graduation"
++++
On September 29, I received my master’s degree in industrial engineering from Seoul National University!

-It’s been a wonderful two years. Despite the challenges of social distancing, I was able to take a diverse assortment of courses in optimization, economics, and human factors; participate in research projects in two different labs; present at a conference in Jeju; and write (in both Korean and English) a thesis I’m [rather proud of](https://github.com/maxkapur/CollegeApplication).
+It’s been a wonderful two years. Despite the challenges of social distancing, I was able to take a diverse assortment of courses in optimization, economics, and human factors; participate in research projects in two different labs; present at a conference in Jeju; and write (in both Korean and English) a thesis I’m [rather proud of](https://github.com/maxkapur/CollegeApplication).
+
+
As part of my duties in the Management Science/Optimization Lab, I had the opportunity to serve as the course TA for two of my advisor’s optimization courses, both of which have now been published as free online courses:
diff --git a/_posts/2022-11-06-two-truths-one-lie.md b/content/posts/2022-11-06-two-truths-one-lie.md
similarity index 92%
rename from _posts/2022-11-06-two-truths-one-lie.md
rename to content/posts/2022-11-06-two-truths-one-lie.md
index 7fbb78f4..2657880e 100644
--- a/_posts/2022-11-06-two-truths-one-lie.md
+++ b/content/posts/2022-11-06-two-truths-one-lie.md
@@ -1,7 +1,10 @@
----
-layout: post
-title: "Two truths and one lie"
----
++++
+title = "Two truths and one lie"
+aliases = [ "/2022/11/06/two-truths-one-lie.html",]
+
+[params]
+id = "https://maxkapur.com/2022/11/06/two-truths-one-lie"
++++
A puzzle based on the icebreaker game.
@@ -11,7 +14,9 @@ A puzzle based on the icebreaker game.
> 2. I used a random number generator to decide whether to place a true or false statement in #1.
> 3. I do not understand the rules of “two truths and one lie.”
-Solution inside.
+Solution inside.
+
+
Hint
diff --git a/_posts/2022-12-17-this-years-reading.md b/content/posts/2022-12-17-this-years-reading.md
similarity index 88%
rename from _posts/2022-12-17-this-years-reading.md
rename to content/posts/2022-12-17-this-years-reading.md
index 5d338757..0eae3d25 100644
--- a/_posts/2022-12-17-this-years-reading.md
+++ b/content/posts/2022-12-17-this-years-reading.md
@@ -1,7 +1,10 @@
----
-layout: post
-title: "This year’s reading"
----
++++
+title = "This year’s reading"
+aliases = [ "/2022/12/17/this-years-reading.html",]
+
+[params]
+id = "https://maxkapur.com/2022/12/17/this-years-reading"
++++
A subset of the books I enjoyed this year:
- Barbara Kingsolver, *The Poisonwood Bible,* a novel about evangelical Christianity and cultural arrogance.
diff --git a/_posts/2023-01-27-kiss-guitar-modeling.md b/content/posts/2023-01-27-kiss-guitar-modeling.md
similarity index 93%
rename from _posts/2023-01-27-kiss-guitar-modeling.md
rename to content/posts/2023-01-27-kiss-guitar-modeling.md
index cf12d9d2..9be411fd 100644
--- a/_posts/2023-01-27-kiss-guitar-modeling.md
+++ b/content/posts/2023-01-27-kiss-guitar-modeling.md
@@ -1,7 +1,10 @@
----
-layout: post
-title: "Applying the KISS principle to amp modeling"
----
++++
+title = "Applying the KISS principle to amp modeling"
+aliases = [ "/2023/01/27/kiss-guitar-modeling.html",]
+
+[params]
+id = "https://maxkapur.com/2023/01/27/kiss-guitar-modeling"
++++
I’ve been playing electric guitar using only computer modeling software (without a physical amplifier) for about five years now.
I started out with Garageband and Mainstage, then briefly experimented with [Amplifikation Vermilion](https://www.kuassa.com/products/amplifikation-vermilion/), and for the last year and half have turned to a free, open-source package called [Guitarix](https://guitarix.org/) and a small collection of impulse response files culled from guitar forums.
diff --git a/_posts/2023-02-05-korean-idioms-fortunes.md b/content/posts/2023-02-05-korean-idioms-fortunes.md
similarity index 86%
rename from _posts/2023-02-05-korean-idioms-fortunes.md
rename to content/posts/2023-02-05-korean-idioms-fortunes.md
index ff9c01a9..484809cf 100644
--- a/_posts/2023-02-05-korean-idioms-fortunes.md
+++ b/content/posts/2023-02-05-korean-idioms-fortunes.md
@@ -1,7 +1,10 @@
----
-layout: post
-title: "Learning Korean idioms with UNIX fortune"
----
++++
+title = "Learning Korean idioms with UNIX fortune"
+aliases = [ "/2023/02/05/korean-idioms-fortunes.html",]
+
+[params]
+id = "https://maxkapur.com/2023/02/05/korean-idioms-fortunes"
++++
[fortune](https://en.wikipedia.org/wiki/Fortune_(Unix)) is a classic UNIX program that displays random fortunes, pithy quotes, and the like:
@@ -20,9 +23,10 @@ $ fortune korean
This one says “Chanting sutras into the ears of a cow,“ defined as “A phrase used when someone fails to understand what you are trying to teach them no matter how well you choose your words.”
-I set `fortune korean` (with some gentle text styling) as my fish greeting, so I can learn a new idiom every time I open a terminal—see below.
+I set `fortune korean` (with some gentle text styling) as my fish greeting, so I can learn a new idiom every time I open a terminal—see below.
+
+

You can find my database and `fish_greeter.fish` script on [GitHub](https://github.com/maxkapur/korean-fortunes).
-
diff --git a/_posts/2023-06-17-fzl.fish.md b/content/posts/2023-06-17-fzl.fish.md
similarity index 88%
rename from _posts/2023-06-17-fzl.fish.md
rename to content/posts/2023-06-17-fzl.fish.md
index 37a26f4f..b2818545 100644
--- a/_posts/2023-06-17-fzl.fish.md
+++ b/content/posts/2023-06-17-fzl.fish.md
@@ -1,7 +1,10 @@
----
-layout: post
-title: "fzl.fish"
----
++++
+title = "fzl.fish"
+aliases = [ "/2023/06/17/fzl.fish.html",]
+
+[params]
+id = "https://maxkapur.com/2023/06/17/fzl.fish"
++++
I’ve been using the following fish alias `fzl` (“fuzzy list”) to run `fzf` (“fuzzy find”) in the current directory with a `bat` preview. It’s just a couple of light tweaks to the examples listed in the [`fzf README`](https://github.com/junegunn/fzf#preview-window).
@@ -35,6 +38,8 @@ function fzl
end
```
-Example below.
+Example below.
+
+

diff --git a/_posts/2023-07-22-guitarix-troubleshooting.md b/content/posts/2023-07-22-guitarix-troubleshooting.md
similarity index 79%
rename from _posts/2023-07-22-guitarix-troubleshooting.md
rename to content/posts/2023-07-22-guitarix-troubleshooting.md
index 8a963d38..33d7d5d3 100644
--- a/_posts/2023-07-22-guitarix-troubleshooting.md
+++ b/content/posts/2023-07-22-guitarix-troubleshooting.md
@@ -1,7 +1,10 @@
----
-layout: post
-title: "Guitarix troubleshooting"
----
++++
+title = "Guitarix troubleshooting"
+aliases = [ "/2023/07/22/guitarix-troubleshooting.html",]
+
+[params]
+id = "https://maxkapur.com/2023/07/22/guitarix-troubleshooting"
++++
For the past few updates of [Guitarix](https://guitarix.org/), I have been
having issues where changing the JACK server latency causes the audio output
diff --git a/_posts/2023-11-05-recent-reading.md b/content/posts/2023-11-05-recent-reading.md
similarity index 86%
rename from _posts/2023-11-05-recent-reading.md
rename to content/posts/2023-11-05-recent-reading.md
index 001a6aaf..bac534ed 100644
--- a/_posts/2023-11-05-recent-reading.md
+++ b/content/posts/2023-11-05-recent-reading.md
@@ -1,7 +1,10 @@
----
-layout: post
-title: "Recent reading"
----
++++
+title = "Recent reading"
+aliases = [ "/2023/11/05/recent-reading.html",]
+
+[params]
+id = "https://maxkapur.com/2023/11/05/recent-reading"
++++
Finished reading, but still thinking about:
diff --git a/_posts/2024-01-01-handy-submodular.md b/content/posts/2024-01-01-handy-submodular.md
similarity index 54%
rename from _posts/2024-01-01-handy-submodular.md
rename to content/posts/2024-01-01-handy-submodular.md
index 1321fe26..7706b853 100644
--- a/_posts/2024-01-01-handy-submodular.md
+++ b/content/posts/2024-01-01-handy-submodular.md
@@ -1,77 +1,84 @@
----
-layout: post
-title: "A handy class of submodular functions"
----
++++
+title = "A handy class of submodular functions"
+aliases = [ "/2024/01/01/handy-submodular.html",]
+
+[params]
+id = "https://maxkapur.com/2024/01/01/handy-submodular"
++++
In this post, we will show that functions of the form
-$$
+{{< math >}}
f(X) = 1 -
\prod_{i \in \Omega \setminus X} (1 - p_i)
\prod_{i \in X} (1 - q_i)
-$$
+{{< /math >}}
-are submodular for $$p_i, q_i \in [0, 1]$$ where each $$p_i \leq q_i,$$
+are submodular for {{< math "p_i, q_i \in [0, 1]" />}} where each {{< math "p_i \leq q_i," />}}
and examine an application of this small result that demonstrates its
-practical value.
+practical value.
+
+
# Background on submodular functions
-Given a ground set $$\Omega,$$ a function $$f: 2^\Omega \mapsto \mathbb{R}$$
+Given a ground set {{< math "\Omega," />}} a function {{< math "f: 2^\Omega \mapsto \mathbb{R}" />}}
is called [*submodular*](https://en.wikipedia.org/wiki/Submodular_set_function)
-if, for any $$X \subseteq Y \subseteq \Omega$$ and $$j \in \Omega \setminus Y,$$
+if, for any {{< math "X \subseteq Y \subseteq \Omega" />}} and {{< math "j \in \Omega \setminus Y," />}}
we have
-$$f(X \cup \{j\}) - f(X) \geq f(Y \cup \{j\}) - f(Y).$$
+{{< math >}}
+f(X \cup \{j\}) - f(X) \geq f(Y \cup \{j\}) - f(Y).
+{{< /math >}}
-In plain language, this says that the marginal value of adding $$j$$ to $$X$$
-is greater than when adding it to $$Y$$. Thus, submodularity expresses a
+In plain language, this says that the marginal value of adding {{< math "j" />}} to {{< math "X" />}}
+is greater than when adding it to {{< math "Y" />}}. Thus, submodularity expresses a
form of diminishing marginal returns (for set functions) that is analogous to
concavity (for continuous functions).
Submodular functions have a number of desirable traits. A famous result of
Nemhauser et al. (1978) holds that when maximizing a monotone[^monotone]
submodular function over a cardinality constraint, the greedy algorithm that
-iteratively adds to $$X$$ the element that yields the greatest increase in
-$$f(X)$$ is $$1 - \frac{1}{e} \approx 63.2\%$$ optimal.[^optimal] Efficient algorithms
+iteratively adds to {{< math "X" />}} the element that yields the greatest increase in
+{{< math "f(X)" />}} is {{< math "1 - \frac{1}{e} \approx 63.2\%" />}} optimal.[^optimal] Efficient algorithms
that provide the same approximation ratio for more complex constraint structures
have also been identified (Badanidiyuru et al. 2014, Chekuri et al. 2014,
Kulik et al. 2013). See Vondrák (2017) for an introduction to submodular functions
with many examples.
-# Preliminary interpretation of $$f(X)$$
+# Preliminary interpretation of {{< math "f(X)" />}}
-The function $$f(X)$$ defined above can be used to model the probability of
+The function {{< math "f(X)" />}} defined above can be used to model the probability of
a parallel system failing when optional repairs are made to its components.
For example, suppose there are a dozen roads between our town and the next,
and the probabilities of each road being open in the coming winter are
-given by $$p_1$$ through $$p_{12}$$. For each of the roads, we can engage
-in repairs to increase the availability to $$q_i$$. Letting $$X$$ denote the set
+given by {{< math "p_1" />}} through {{< math "p_{12}" />}}. For each of the roads, we can engage
+in repairs to increase the availability to {{< math "q_i" />}}. Letting {{< math "X" />}} denote the set
of roads that we repair, the probability of *at least* one road being open in the
-winter is one minus the probability that all of them are closed, which is $$f(X).$$
+winter is one minus the probability that all of them are closed, which is {{< math "f(X)." />}}
-With this interpretation in mind, we are tempted to give $$f(X)$$ a
+With this interpretation in mind, we are tempted to give {{< math "f(X)" />}} a
tortured-but-accurate name such as the “parallel process success probability
with quality upgrades” function—but we’ll just stick with
-$$f(X)$$ for now.
+{{< math "f(X)" />}} for now.
-The applicability of $$f(X)$$ in its current form is, admittedly, a bit
+The applicability of {{< math "f(X)" />}} in its current form is, admittedly, a bit
narrow. The versatility of this functional form shows itself more clearly
when we start taking conic combinations—see the examples below.
# Monotonicity and submodularity proof
-**Theorem:** $$f(X)$$ as defined above is a monotone submodular function.
+**Theorem:** {{< math "f(X)" />}} as defined above is a monotone submodular function.
-**Proof:** The monotonicity of $$f(X)$$ is clear from the constraints
-on $$p$$ and $$q$$: Adding any element $$j$$ to $$X$$ replaces the
-$$1 - p_j$$ term with the (smaller) $$1 - q_j$$ term, increasing the
+**Proof:** The monotonicity of {{< math "f(X)" />}} is clear from the constraints
+on {{< math "p" />}} and {{< math "q" />}}: Adding any element {{< math "j" />}} to {{< math "X" />}} replaces the
+{{< math "1 - p_j" />}} term with the (smaller) {{< math "1 - q_j" />}} term, increasing the
overall function value.
-To prove submodularity, pick any $$X \subseteq Y \subseteq \Omega$$ and
-$$j \in \Omega \setminus Y.$$ If $$p_j < 1,$$ we have
+To prove submodularity, pick any {{< math "X \subseteq Y \subseteq \Omega" />}} and
+{{< math "j \in \Omega \setminus Y." />}} If {{< math "p_j < 1," />}} we have
-$$
+{{< math >}}
\begin{aligned}
f(X \cup \{j\}) - f(X)
&= \biggl[1 -
@@ -94,99 +101,99 @@ f(X \cup \{j\}) - f(X)
\Bigl(1 - f(Y)\Bigr) \\
&= f(Y \cup \{j\}) - f(Y).
\end{aligned}
-$$
+{{< /math >}}
The penultimate inequality follows from monotonicity, and the final
-equality is just applying the first three steps in reverse, with $$Y$$
-instead of $$X.$$
+equality is just applying the first three steps in reverse, with {{< math "Y" />}}
+instead of {{< math "X." />}}
-In the $$p_j = 1$$ case, $$f(X \cup \{j\}) = 1$$ for any $$X$$ and
-the inequality collapses to $$- f(X) \geq - f(Y),$$ which is just
+In the {{< math "p_j = 1" />}} case, {{< math "f(X \cup \{j\}) = 1" />}} for any {{< math "X" />}} and
+the inequality collapses to {{< math "- f(X) \geq - f(Y)," />}} which is just
monotonicity. ◼
-# Application: Optimal scrum velocity, aka the $$m$$-dimensional Zeno walk
+# Application: Optimal scrum velocity, aka the {{< math "m" />}}-dimensional Zeno walk
Let’s examine a plausible(ish) application of this functional form.
**The Zeno walk:** Consider a
[Zeno walk](https://en.wikipedia.org/wiki/Zeno%27s_paradoxes#Dichotomy_paradox)
-between an arbitrary origin and destination. Let $$\Omega$$ provide a set of
+between an arbitrary origin and destination. Let {{< math "\Omega" />}} provide a set of
possible *steps* that we can take toward our destination. Each step’s size
-is $$s_i \in [0, 1],$$ which means that if you choose to take step $$i,$$
-you get $$100 s_i$$ percent of the way from your current location to the
-destination.[^zeno] If $$X$$ is the set of steps you choose to take,
+is {{< math "s_i \in [0, 1]," />}} which means that if you choose to take step {{< math "i," />}}
+you get {{< math "100 s_i" />}} percent of the way from your current location to the
+destination.[^zeno] If {{< math "X" />}} is the set of steps you choose to take,
you will make it
-$$
+{{< math >}}
g(X) = 1 - \prod_{i \in X} (1 - s_i)
-$$
+{{< /math >}}
-of the way to the destination from your starting point. This $$g(X)$$
-is just $$f(X)$$ where each $$p_i = 0$$ and $$q_i = s_i$$.
+of the way to the destination from your starting point. This {{< math "g(X)" />}}
+is just {{< math "f(X)" />}} where each {{< math "p_i = 0" />}} and {{< math "q_i = s_i" />}}.
**The multidimensional Zeno walk:** Now, consider a process optimization
-problem. We have a set $$\Psi$$ (indexed by $$j$$) of *goals* to achieve,
-as well as a set $$\Omega$$ (indexed by $$i$$) of *tasks* we can complete
+problem. We have a set {{< math "\Psi" />}} (indexed by {{< math "j" />}}) of *goals* to achieve,
+as well as a set {{< math "\Omega" />}} (indexed by {{< math "i" />}}) of *tasks* we can complete
in service of the goals. Tasks can advance multiple goals at once. Let
-$$v_{ij}$$ denote the *effectiveness* of task $$i$$ against goal $$j,$$
-meaning that if you engage in task $$i,$$ then $$v_{ij}$$ of (whatever
-remains of)[^whatever] goal $$j$$ will be completed. Each goal is
-worth $$t_j \geq 0$$
+{{< math "v_{ij}" />}} denote the *effectiveness* of task {{< math "i" />}} against goal {{< math "j," />}}
+meaning that if you engage in task {{< math "i," />}} then {{< math "v_{ij}" />}} of (whatever
+remains of)[^whatever] goal {{< math "j" />}} will be completed. Each goal is
+worth {{< math "t_j \geq 0" />}}
*[story points](https://en.wikipedia.org/wiki/Burndown_chart),*
and goals can be completed fractionally.
-We would like to choose the set of tasks $$X$$ that maximizes the team’s
+We would like to choose the set of tasks {{< math "X" />}} that maximizes the team’s
[*velocity*](https://en.wikipedia.org/wiki/Velocity_(software_development))
in the current planning period, defined as the total number of story points
completed, which is
-$$
+{{< math >}}
h(X) = \sum_{j \in \Psi} \Bigl(
t_j - t_j \prod_{i \in X} (1 - v_{ij})
\Bigr).
-$$
+{{< /math >}}
Tasks are subject to a variety of constraints due to considerations such
as cost, scheduling, and dependency. It is not immediately obvious which
tasks we should prioritize—for example, should we favor tasks that
advance many different goals, or those that have high effectiveness
-against the most important goals? Well, the $$j$$th term of $$h(X)$$
-is just $$t_j g(X)$$ where $$s$$ is the $$j$$th column of $$v$$. Thus,
-$$h(X),$$ as a conic combination of submodular functions, is itself
+against the most important goals? Well, the {{< math "j" />}}th term of {{< math "h(X)" />}}
+is just {{< math "t_j g(X)" />}} where {{< math "s" />}} is the {{< math "j" />}}th column of {{< math "v" />}}. Thus,
+{{< math "h(X)," />}} as a conic combination of submodular functions, is itself
submodular,[^conic] and we can compute the optimal velocity using
any of the well-known techniques for submodular maximization.
Returning to Zeno’s paradox, the business process described above is
-akin to a *multidimensional* Zeno walk in which $$t_j$$ gives your distance
-from the target in the $$j$$th coordinate axis, and each step $$i$$ moves you
-$$v_{ij}$$ of the way towards the target along that axis. The function
-$$h(X)$$ measures the one-norm distance between your final position and the
+akin to a *multidimensional* Zeno walk in which {{< math "t_j" />}} gives your distance
+from the target in the {{< math "j" />}}th coordinate axis, and each step {{< math "i" />}} moves you
+{{< math "v_{ij}" />}} of the way towards the target along that axis. The function
+{{< math "h(X)" />}} measures the one-norm distance between your final position and the
target.
# Remarks
-**Linearization:** To maximize $$f(X)$$ as written, in typical cases, it
-will be simplest to minimize $$1 - f(X),$$ which is a *modular* function
+**Linearization:** To maximize {{< math "f(X)" />}} as written, in typical cases, it
+will be simplest to minimize {{< math "1 - f(X)," />}} which is a *modular* function
after taking the logarithm.[^modular] The logarithm trick doesn’t work,
-however, for conic combinations of $$f(X)$$; the best we can do is introduce
+however, for conic combinations of {{< math "f(X)" />}}; the best we can do is introduce
helper variables and reformulate the problem as an integer convex
-program.[^integerconvex] Thus, recognizing the submodularity of $$f(X)$$
+program.[^integerconvex] Thus, recognizing the submodularity of {{< math "f(X)" />}}
(and the strong approximation results implied) is worthwhile.
**Serial processes:** It is not difficult to show, using a similar proof
as above, that the “*serial* process success probability with quality upgrades”
function
-$$
+{{< math >}}
\tilde f(X) = \prod_{i \in \Omega \setminus X} p_i \prod_{i \in X} q_i
-$$
+{{< /math >}}
-with $$p_i \leq q_i$$ is monotone and *supermodular;* that is,
-$$- \tilde f(X)$$ is monotone *decreasing* and submodular. Note that
-$$1 - \tilde f(X)$$ has the same functional form as the original $$f(X),$$
-but the inequality on $$p_i$$ and $$q_i$$ comes out backwards. This turns
-back into the original result (that $$f(X)$$ is increasing and submodular)
-if you interchange the roles of $$X$$ and $$\Omega \setminus X$$.
+with {{< math "p_i \leq q_i" />}} is monotone and *supermodular;* that is,
+{{< math "- \tilde f(X)" />}} is monotone *decreasing* and submodular. Note that
+{{< math "1 - \tilde f(X)" />}} has the same functional form as the original {{< math "f(X)," />}}
+but the inequality on {{< math "p_i" />}} and {{< math "q_i" />}} comes out backwards. This turns
+back into the original result (that {{< math "f(X)" />}} is increasing and submodular)
+if you interchange the roles of {{< math "X" />}} and {{< math "\Omega \setminus X" />}}.
# References
@@ -207,16 +214,16 @@ Paywall:
-[^monotone]: A set function is called *monotone* if $$X \subseteq Y$$ implies $$f(X) \leq f(Y)$$.
+[^monotone]: A set function is called *monotone* if {{< math "X \subseteq Y" />}} implies {{< math "f(X) \leq f(Y)" />}}.
-[^optimal]: This means that if $$X^*$$ is the set that maximizes $$f(X)$$ and $$\tilde X$$ is the set produced by the greedy algorithm, we are guaranteed to have $$f(\tilde X) \geq 0.632\,f(X^*)$$. In practice, the optimality gap is often much narrower.
+[^optimal]: This means that if {{< math "X^*" />}} is the set that maximizes {{< math "f(X)" />}} and {{< math "\tilde X" />}} is the set produced by the greedy algorithm, we are guaranteed to have {{< math "f(\tilde X) \geq 0.632\,f(X^*)" />}}. In practice, the optimality gap is often much narrower.
-[^zeno]: In the original Zeno walk, each $$s_i = \frac{1}{2},$$ and the (supposedly unachievable) “goal” is to reach the destination rather than to get as close as possible to it.
+[^zeno]: In the original Zeno walk, each {{< math "s_i = \frac{1}{2}," />}} and the (supposedly unachievable) “goal” is to reach the destination rather than to get as close as possible to it.
-[^whatever]: Depending on the context, it may be more appropriate to have tasks advance goals additively—but that would just be a knapsack problem. Here, we are interested in the more difficult case where tasks make diminishing marginal contributions against the goals. One way this arises in real life is when tasks address goals in redundant ways. For example, against the goal “get rid of all the junk in my house,” the tasks “throw away any items not used in the past six months” and “throw away any items that spark fewer than $$k$$ units of joy” may separately elicit a 50 percent reduction in the total junk, but doing *both* tasks won’t necessarily remove all the junk, because there is some overlap in the junk removed by both operations.
+[^whatever]: Depending on the context, it may be more appropriate to have tasks advance goals additively—but that would just be a knapsack problem. Here, we are interested in the more difficult case where tasks make diminishing marginal contributions against the goals. One way this arises in real life is when tasks address goals in redundant ways. For example, against the goal “get rid of all the junk in my house,” the tasks “throw away any items not used in the past six months” and “throw away any items that spark fewer than {{< math "k" />}} units of joy” may separately elicit a 50 percent reduction in the total junk, but doing *both* tasks won’t necessarily remove all the junk, because there is some overlap in the junk removed by both operations.
[^conic]: The fact that a conic combination of submodular functions is submodular isn’t mentioned on the [Wikipedia article](https://en.wikipedia.org/wiki/Submodular_set_function), but it falls right out of the second definition.
[^modular]: A set function is *modular* if it is both submodular and supermodular. This means that each item’s marginal value is constant—and thus modular functions are equivalent to *linear* functions of the characteristic vector. The knapsack objective function is an example of a monotone modular function.
-[^integerconvex]: [Email me](mailto:{{ site.email }}) if you would be interested in a follow-up post explaining how to do this.
+[^integerconvex]: [Email me](mailto:max@maxkapur.com) if you would be interested in a follow-up post explaining how to do this.
diff --git a/_posts/2024-03-10-housekeeping.md b/content/posts/2024-03-10-housekeeping.md
similarity index 92%
rename from _posts/2024-03-10-housekeeping.md
rename to content/posts/2024-03-10-housekeeping.md
index 5cee5f96..2cc2b9fa 100644
--- a/_posts/2024-03-10-housekeeping.md
+++ b/content/posts/2024-03-10-housekeeping.md
@@ -1,7 +1,10 @@
----
-layout: post
-title: "Housekeeping"
----
++++
+title = "Housekeeping"
+aliases = [ "/2024/03/10/housekeeping.html",]
+
+[params]
+id = "https://maxkapur.com/2024/03/10/housekeeping"
++++
Completed some overdue site maintenance over the past few weeks:
diff --git a/_posts/2024-03-18-recent-reading.md b/content/posts/2024-03-18-recent-reading.md
similarity index 91%
rename from _posts/2024-03-18-recent-reading.md
rename to content/posts/2024-03-18-recent-reading.md
index 139f5656..245af185 100644
--- a/_posts/2024-03-18-recent-reading.md
+++ b/content/posts/2024-03-18-recent-reading.md
@@ -1,7 +1,10 @@
----
-layout: post
-title: "Recent reading"
----
++++
+title = "Recent reading"
+aliases = [ "/2024/03/18/recent-reading.html",]
+
+[params]
+id = "https://maxkapur.com/2024/03/18/recent-reading"
++++
Mostly novels this time:
diff --git a/_posts/2024-04-14-new-old-device.md b/content/posts/2024-04-14-new-old-device.md
similarity index 95%
rename from _posts/2024-04-14-new-old-device.md
rename to content/posts/2024-04-14-new-old-device.md
index 2bef10e0..e1c1e9c6 100644
--- a/_posts/2024-04-14-new-old-device.md
+++ b/content/posts/2024-04-14-new-old-device.md
@@ -1,7 +1,10 @@
----
-layout: post
-title: "New old device"
----
++++
+title = "New old device"
+aliases = [ "/2024/04/14/new-old-device.html",]
+
+[params]
+id = "https://maxkapur.com/2024/04/14/new-old-device"
++++
On a recent visit home, I dug this 2007
[Sansa Clip](https://en.wikipedia.org/wiki/SanDisk_portable_media_players#Sansa_Clip)
diff --git a/_posts/2024-04-26-iso-dates-filenames.md b/content/posts/2024-04-26-iso-dates-filenames.md
similarity index 79%
rename from _posts/2024-04-26-iso-dates-filenames.md
rename to content/posts/2024-04-26-iso-dates-filenames.md
index ef5eebc4..dff5b346 100644
--- a/_posts/2024-04-26-iso-dates-filenames.md
+++ b/content/posts/2024-04-26-iso-dates-filenames.md
@@ -1,7 +1,11 @@
----
-layout: post
-title: "Why I put ISO dates in filenames"
----
++++
+title = "Why I put ISO dates in filenames"
+aliases = [ "/2024/04/26/iso-dates-filenames.html",]
+summary = "I am one of those obnoxious people who begins his filenames with an ISO date,\nlike `2024-03-06_blog_post_idea.md`. I know that file metadata already tracks\ncreation and revision dates, but I don't like those: The creation date is\nmisleading when you duplicate a file to reuse its format or content, and the\nrevision date is all but meaningless because modern software modifies files on\ndisk in all kinds of spurious ways. I put an ISO date in my filenames\nto assign them a *canonical* date. The canonical date means something like “the\nlast date at which this file underwent a significant change,” and I alone (not\nsoftware) can determine what that means."
+
+[params]
+id = "https://maxkapur.com/2024/04/26/iso-dates-filenames"
++++
I am one of those obnoxious people who begins his filenames with an ISO date,
like `2024-03-06_blog_post_idea.md`. I know that file metadata already tracks
@@ -11,7 +15,7 @@ revision date is all but meaningless because modern software modifies files on
disk in all kinds of spurious ways. I put an ISO date in my filenames
to assign them a *canonical* date. The canonical date means something like “the
last date at which this file underwent a significant change,” and I alone (not
-software) can determine what that means.[^spurious]
+software) can determine what that means.[^spurious]
[^spurious]: An example of the spurious ways in which modern software modifies files on disk: If you open a Microsoft Excel worksheet, adjust the zoom, and try to close Excel, it will issue a “Do you want to save your changes?” warning. I’m not sure why the zoom level is stored in the `.xlsx` file at all. In Word (as far as I can tell), the zoom level, like scroll position, belongs to your session state and will thus be remembered separately for different viewers of the document.
diff --git a/_posts/2024-05-03-best-thing-about-fish.md b/content/posts/2024-05-03-best-thing-about-fish.md
similarity index 87%
rename from _posts/2024-05-03-best-thing-about-fish.md
rename to content/posts/2024-05-03-best-thing-about-fish.md
index 0ce2f875..ee4118df 100644
--- a/_posts/2024-05-03-best-thing-about-fish.md
+++ b/content/posts/2024-05-03-best-thing-about-fish.md
@@ -1,7 +1,10 @@
----
-layout: post
-title: "The best thing about fish"
----
++++
+title = "The best thing about fish"
+aliases = [ "/2024/05/03/best-thing-about-fish.html",]
+
+[params]
+id = "https://maxkapur.com/2024/05/03/best-thing-about-fish"
++++
The best thing about fish is probably the
[lateral line](https://en.wikipedia.org/wiki/Lateral_line),
@@ -17,7 +20,7 @@ to scroll through them until you get the one you want.
This works fine if you put the most-unique part of your filenames at the
beginning. But I like to begin my filenames with
-[ISO dates]({% post_url 2024-04-26-iso-dates-filenames %}),
+[ISO dates](/posts/2024-04-26-iso-dates-filenames/),
so to tab-complete `2024-03-06_blog_post_idea.md` in PowerShell, I have to
type `2024` and then jab the arrow key a bunch of times, which is like using a
phone book that is sorted by phone number. In fish, I can just type `blogpost`
diff --git a/_posts/2024-05-12-new-logo.md b/content/posts/2024-05-12-new-logo.md
similarity index 55%
rename from _posts/2024-05-12-new-logo.md
rename to content/posts/2024-05-12-new-logo.md
index 00a9d24b..fdb9e2ba 100644
--- a/_posts/2024-05-12-new-logo.md
+++ b/content/posts/2024-05-12-new-logo.md
@@ -1,17 +1,24 @@
----
-layout: post
-title: "New logo"
----
++++
+title = "New logo"
+aliases = [ "/2024/05/12/new-logo.html",]
+
+[params]
+id = "https://maxkapur.com/2024/05/12/new-logo"
++++
I created a new logo for the site today:
-
+
+
+
The old one was just a square with my initials, MK, which seemed a little too
self-indulgent (not to mention that I share these initials with half of my
extended family, so I can hardly claim them as my own):
-
+
+
+
The new logo still kind of forms a stylized letter *m,* if you imagine the three
theme-colored vertical segments as hanging from the horizontal segment on top.
diff --git a/_posts/2024-06-07-sports-idea.md b/content/posts/2024-06-07-sports-idea.md
similarity index 85%
rename from _posts/2024-06-07-sports-idea.md
rename to content/posts/2024-06-07-sports-idea.md
index 76c6a351..149ab7e8 100644
--- a/_posts/2024-06-07-sports-idea.md
+++ b/content/posts/2024-06-07-sports-idea.md
@@ -1,7 +1,10 @@
----
-layout: post
-title: "Sports idea"
----
++++
+title = "Sports idea"
+aliases = [ "/2024/06/07/sports-idea.html",]
+
+[params]
+id = "https://maxkapur.com/2024/06/07/sports-idea"
++++
A footrace, but the objective is to run as far as possible in a fixed amount of
time rather than speeding through a fixed distance. The catch is that the time
diff --git a/_posts/2024-06-23-drivers-ed-error-recovery.md b/content/posts/2024-06-23-drivers-ed-error-recovery.md
similarity index 79%
rename from _posts/2024-06-23-drivers-ed-error-recovery.md
rename to content/posts/2024-06-23-drivers-ed-error-recovery.md
index 72279f70..bc41584e 100644
--- a/_posts/2024-06-23-drivers-ed-error-recovery.md
+++ b/content/posts/2024-06-23-drivers-ed-error-recovery.md
@@ -1,7 +1,11 @@
----
-layout: post
-title: "Driver’s ed should teach error recovery"
----
++++
+title = "Driver’s ed should teach error recovery"
+aliases = [ "/2024/06/23/drivers-ed-error-recovery.html",]
+summary = "I live near a one-way street, and drivers keep turning into it the wrong way by\nmistake. Most react by getting flustered and backing dangerously into the\narterial, perhaps following an instinct to “undo” the error. The safer solution\n(in my eyes) is to park the car, wait for traffic to clear, then use a driveway\nto point yourself in the right direction.\n\nUnfortunately, driver’s ed doesn’t teach you how to recover from mistakes like\nthis—at least, mine didn’t. Instead, driver’s ed teaches the rules of the\nroad and how to follow them. Once you have violated the law, even if by\naccident, you are in undefined territory. Hence, drivers develop a strong\nimpulse to immediately “reverse” errors rather than identify a safe course of\naction."
+
+[params]
+id = "https://maxkapur.com/2024/06/23/drivers-ed-error-recovery"
++++
I live near a one-way street, and drivers keep turning into it the wrong way by
mistake. Most react by getting flustered and backing dangerously into the
@@ -14,7 +18,7 @@ this—at least, mine didn’t. Instead, driver’s ed teaches the rules of
road and how to follow them. Once you have violated the law, even if by
accident, you are in undefined territory. Hence, drivers develop a strong
impulse to immediately “reverse” errors rather than identify a safe course of
-action.[^errors]
+action.[^errors]
[^errors]: Well, my driver’s ed course did talk about what to do after you get
in a *collision:* Exchange phone numbers, take lots of pictures, call the
diff --git a/_posts/2024-08-13-advanced-cafe.md b/content/posts/2024-08-13-advanced-cafe.md
similarity index 85%
rename from _posts/2024-08-13-advanced-cafe.md
rename to content/posts/2024-08-13-advanced-cafe.md
index 3e5a5e67..f9aecd0b 100644
--- a/_posts/2024-08-13-advanced-cafe.md
+++ b/content/posts/2024-08-13-advanced-cafe.md
@@ -1,7 +1,10 @@
----
-layout: post
-title: "Advanced café techniques"
----
++++
+title = "Advanced café techniques"
+aliases = [ "/2024/08/13/advanced-cafe.html",]
+
+[params]
+id = "https://maxkapur.com/2024/08/13/advanced-cafe"
++++
Most cafés have separate lines for placing your order and picking it up. After
you order, you move to the next line, and since drinks come out in more or less
diff --git a/content/posts/2024-09-24-bimatrix-game-equilibrium.md b/content/posts/2024-09-24-bimatrix-game-equilibrium.md
new file mode 100644
index 00000000..dc1e6cf9
--- /dev/null
+++ b/content/posts/2024-09-24-bimatrix-game-equilibrium.md
@@ -0,0 +1,111 @@
++++
+title = "Bimatrix game equilibrium via nonlinear programming"
+aliases = [ "/2024/09/24/bimatrix-game-equilibrium.html",]
+
+[params]
+id = "https://maxkapur.com/2024/09/24/bimatrix-game-equilibrium"
++++
+
+[These lecture notes
+(PDF)](https://ocw.mit.edu/courses/6-254-game-theory-with-engineering-applications-spring-2010/9cca6ef4a5399a4e05891f471d667441_MIT6_254S10_lec09.pdf)
+from Asu Ozdaglar give a bilinear program whose solution is the mixed-strategy
+equilibrium for a two-player, non-zero-sum game with finite action spaces—also
+known as a bimatrix game. I wanted to reproduce this highly practical result in
+a more accessible format and offer some implementation notes.
+
+
+
+# Definitions
+
+A *bimatrix game* is characterized by two matrices {{< math "A, B \in \mathbb{R}^{n \times m}." />}} When Alice chooses strategy {{< math "i" />}} and Bob chooses strategy {{< math "j" />}},
+their payoffs are {{< math "A_{ij}" />}} and {{< math "B_{ij}," />}} respectively. Mixed-strategy
+probability vectors {{< math "x" />}} and {{< math "y" />}} yield expected payoffs of {{< math "x^T A y" />}} and
+{{< math "x^T B y," />}} respectively.
+
+{{< math "(x^*, y^*)" />}}
+is an *equilibrium* if
+{{< math "x^T A y^* \leq x^{*T} A y^*" />}}
+and
+{{< math "x^{*T} A y \leq x^{*T} A y^*" />}}
+for all probability vectors
+{{< math "x" />}}
+and
+{{< math "y" />}}.
+
+# Result
+
+**Theorem:** {{< math "(\implies)" />}} If {{< math "(x^*, y^*)" />}} is an equilibrium, then there
+exist {{< math "p^*" />}} and {{< math "q^*" />}} such that {{< math "(x^*, y^*, p^*, q^*)" />}} is the optimal
+solution to the following bilinear program:
+
+{{< math >}}
+\begin{aligned}
+ \text{maximize} \quad & f(x, y, p, q) = x^T A y + x^T B y - p - q \\
+ \text{subject to} \quad & A y \leq p \mathbf{1} & \text{(OptA)} \\
+ & B^T x \leq q \mathbf{1} & \text{(OptB)} \\
+ & \sum x_i = \sum y_i = 1 & \text{(ProbVec1)} \\
+ & x \geq \mathbf{0}, y \geq \mathbf{0} & \text{(ProbVec2)}
+\end{aligned}
+{{< /math >}}
+
+{{< math "(\impliedby)" />}} Conversely, the optimal solution of the bilinear program is an
+equilibrium for the game.
+
+**Proof:** {{< math "(\implies)" />}} For any feasible solution to the bilinear program,
+each element of {{< math "x" />}} is nonnegative (by ProbVec2), so we can use {{< math "x" />}} to
+combine the rows of the condition OptA to obtain a new valid inequality {{< math "x^T A y \leq x^T p \mathbf{1} = p \sum x_i = p" />}} (by ProbVec1). Applying the same
+logic to {{< math "y" />}} and {{< math "q," />}} we find that {{< math "f(x, y, p, q) \leq 0." />}}
+
+Now consider the equilibrium probability vectors {{< math "(x^*, y^*)" />}} and set {{< math "p^* = x^{*T} A y^*" />}} and {{< math "q^* = x^{*T} B y^{*}." />}} Then {{< math "f(x^*, y^*, p^*, q^*) = 0," />}}
+and we have only to show that this solution is feasible. To be an equilibrium,
+{{< math "x^*" />}} must earn Alice a better payoff against {{< math "y^{*}" />}} than does the {{< math "i" />}}th
+pure strategy: {{< math "p^* = x^{*T} A y^* \geq (A y^*)_i." />}} This is precisely the
+{{< math "i" />}}th row of OptA. OptB follows similarly.
+
+{{< math "(\impliedby)" />}} The bilinear program is clearly feasible and bounded (as shown
+a moment ago). Let {{< math "(\bar x, \bar y, \bar p, \bar q)" />}} denote the optimal
+solution. By Nash’s theorem on the existence of mixed-strategy equilibria, we
+know that an equilibrium exists, and from the first part of the proof, we know
+how to use this equilibrium to produce a feasible solution to the bilinear
+program with an objective value of zero. Thus, {{< math "f(\bar x, \bar y, \bar p, \bar q) \geq 0," />}} which rearranges to
+
+{{< math >}}
+(\bar x^T A \bar y - \bar p) + (\bar x^T B \bar y - \bar q) \geq 0.
+{{< /math >}}
+
+By the constraints OptA and OptB, each of the terms in parentheses is less than
+or equal to zero; thus, the inequality on {{< math "f(\bar x, \bar y, \bar p, \bar q)" />}}
+can hold only when each of these terms *equals* zero exactly.
+
+Now consider any probability vector {{< math "x" />}} and use it to combine the rows of
+OptA: We have {{< math "x^T A \bar y \leq x^T \bar p \mathbf{1} = \bar p = \bar x^T A \bar y," />}} which says that {{< math "\bar x" />}} is a best response to {{< math "\bar y" />}}. Applying
+the same logic to {{< math "\bar y" />}} and {{< math "\bar q" />}} completes the proof. ◼
+
+# Remarks
+
+Solving the bilinear program is not necessarily easy. If merely solving a
+bimatrix game of this form is your goal, then mitigations against local optima
+such as [iterated local
+search](https://en.wikipedia.org/wiki/Iterated_local_search) are essential. You
+can check whether a solution is local or global by comparing the objective value
+to zero, which is the global optimum guaranteed by Nash’s theorem.
+
+In practice, I have used the bilinear program above when implementing the
+[double oracle algorithm](https://arxiv.org/abs/2009.12185) for games with
+complex action spaces. For example, in a modeling problem I am working on, pure
+strategies are subsets of {{< math "\mathbb{R}^n" />}} with additional inequality and
+integrality constraints.
+
+The “first oracle” in the double-oracle algorithm has you compute the
+equilibrium of a subgame with discrete action sets and uses the bilinear program
+above. In the second oracle (typically the hard part), you then augment these
+action sets by computing a pure-strategy best response for each player against
+the mixed-strategy equilibrium obtained from the first oracle. You can use the
+double oracle algorithm to approximate either pure- *or* mixed-strategy
+equilibria by checking convergence and terminating after either the first or
+second oracle, respectively.
+
+In my use case, the open-source solver [Ipopt](https://github.com/coin-or/Ipopt)
+gives acceptable results for the first oracle. I also learned about
+[cyipopt](https://github.com/mechmotum/cyipopt), a nice set of Python bindings
+for Ipopt that lets you `@jit` your functions with Numba or Jax for performance.
diff --git a/_posts/2024-11-02-gwangju-again.md b/content/posts/2024-11-02-gwangju-again.md
similarity index 93%
rename from _posts/2024-11-02-gwangju-again.md
rename to content/posts/2024-11-02-gwangju-again.md
index ced6d661..54dc1246 100644
--- a/_posts/2024-11-02-gwangju-again.md
+++ b/content/posts/2024-11-02-gwangju-again.md
@@ -1,7 +1,10 @@
----
-title: 'Gwangju again: CLS Media Literacy and Democracy seminar'
-layout: post
----
++++
+title = "Gwangju again: CLS Media Literacy and Democracy seminar"
+aliases = [ "/2024/11/02/gwangju-again.html",]
+
+[params]
+id = "https://maxkapur.com/2024/11/02/gwangju-again"
++++
I had the honor this week of participating in the first ever [Critical Language
Scholarship Alumni Seminar](https://clscholarship.org/news/2024/cls-international-alumni-seminars)
@@ -43,7 +46,7 @@ between the viewer and the events of 5/18 that rings “true” in a way that
journalistic accounts cannot.
As for me: Participating in this seminar meant that I got to return to the exact
-institution where I did [CLS in 2016]( {% post_url 2016-03-12-cls-korea %} )—my
+institution where I did [CLS in 2016](/posts/2016-03-12-cls-korea/)—my
first visit to Korea—with the added perspective of eight more years of Korean
study and four years of living in the country. It was a delight to show the
other participants around and see Korea again through the eyes of a newcomer.
diff --git a/_posts/2024-11-06-gwangju-fm.md b/content/posts/2024-11-06-gwangju-fm.md
similarity index 82%
rename from _posts/2024-11-06-gwangju-fm.md
rename to content/posts/2024-11-06-gwangju-fm.md
index 0428544e..5d6ae33a 100644
--- a/_posts/2024-11-06-gwangju-fm.md
+++ b/content/posts/2024-11-06-gwangju-fm.md
@@ -1,10 +1,12 @@
----
-title: 'Media literacy chat at 88.9 Gwangju FM'
-layout: post
----
++++
+title = "Media literacy chat at 88.9 Gwangju FM"
+aliases = [ "/2024/11/06/gwangju-fm.html",]
-I wanted to follow up on my [previous post]( {% post_url
-2024-11-02-gwangju-again %} ) about the CLS alumni seminar in Gwangju to
+[params]
+id = "https://maxkapur.com/2024/11/06/gwangju-fm"
++++
+
+I wanted to follow up on my [previous post](/posts/2024-11-02-gwangju-again/) about the CLS alumni seminar in Gwangju to
spotlight my favorite excursion of the week: our visit to the [88.9 Gwangju
FM]( https://kjfm.communityradio.kr/ ) community radio station. Gwangju FM has
no particular theme, but they dedicate part of their schedule each week to
@@ -25,7 +27,9 @@ will upload an MP3 instead.)
After our group disbanded in Gwangju, I got to spend a few days in Naju, my
“second hometown” where I taught English for two years as a Fulbright grantee. I
tried a new (to me) gomtang place called 나주곰탕사매기, which has a cool
-aquarium, a garden, and even birds! A few pics below.
+aquarium, a garden, and even birds! A few pics below.
+
+
Here is their garden:
diff --git a/_posts/2024-11-29-rbenv-vs-conda.md b/content/posts/2024-11-29-rbenv-vs-conda.md
similarity index 95%
rename from _posts/2024-11-29-rbenv-vs-conda.md
rename to content/posts/2024-11-29-rbenv-vs-conda.md
index 41ccd1bb..7f7af83e 100644
--- a/_posts/2024-11-29-rbenv-vs-conda.md
+++ b/content/posts/2024-11-29-rbenv-vs-conda.md
@@ -1,10 +1,15 @@
----
-layout: post
-title: Replacing rbenv with conda
----
++++
+title = "Replacing rbenv with conda"
+aliases = [ "/2024/11/29/rbenv-vs-conda.html",]
+
+[params]
+id = "https://maxkapur.com/2024/11/29/rbenv-vs-conda"
++++
Why I am using conda instead of rbenv to install Ruby inside of the CI job that
-builds this static site.
+builds this static site.
+
+
# The problem
diff --git a/_posts/2024-12-06-self-host-fonts.md b/content/posts/2024-12-06-self-host-fonts.md
similarity index 91%
rename from _posts/2024-12-06-self-host-fonts.md
rename to content/posts/2024-12-06-self-host-fonts.md
index f36aa415..7c4eb483 100644
--- a/_posts/2024-12-06-self-host-fonts.md
+++ b/content/posts/2024-12-06-self-host-fonts.md
@@ -1,14 +1,19 @@
----
-layout: post
-title: How I self-host fonts on this blog
----
++++
+title = "How I self-host fonts on this blog"
+aliases = [ "/2024/12/06/self-host-fonts.html",]
+
+[params]
+id = "https://maxkapur.com/2024/12/06/self-host-fonts"
++++
I recently transitioned this blog to self-host its fonts (IBM Plex Sans, Sans
KR, and Mono) from within the GitHub Pages artifact instead of calling the
Google Fonts API. This makes the site a little more “static” by eliminating the
need for your browser to download font files from an external domain. Here, I
explain how (and why) I self-host fonts without bringing them into the source
-tree.
+tree.
+
+
The easiest way to self-host fonts with a static site generator like Jekyll is
to store the `.ttf` or `.woff2` font files and their CSS inside of the `assets/`
@@ -40,7 +45,7 @@ IBM_PLEX_SANS_KR_SRC="https://github.com/IBM/plex/releases/download/%40ibm%2Fple
[pull the latest release](https://docs.github.com/en/repositories/releasing-projects-on-github/linking-to-releases)
instead of pinning a version.)
-[^packagemanager]: I [already use conda]( {% post_url 2024-11-29-rbenv-vs-conda %} ) as part
+[^packagemanager]: I [already use conda](/posts/2024-11-29-rbenv-vs-conda/) as part
of my `configure.sh` script, so installing a font with conda wouldn’t be a
heavy lift, but only the Sans variant of IBM Plex is
[available in conda-forge](https://anaconda.org/conda-forge/font-ttf-ibm-plex-sans),
diff --git a/_posts/2024-12-08-novels-to-understand-korea.md b/content/posts/2024-12-08-novels-to-understand-korea.md
similarity index 93%
rename from _posts/2024-12-08-novels-to-understand-korea.md
rename to content/posts/2024-12-08-novels-to-understand-korea.md
index a85e2e48..b243458c 100644
--- a/_posts/2024-12-08-novels-to-understand-korea.md
+++ b/content/posts/2024-12-08-novels-to-understand-korea.md
@@ -1,7 +1,10 @@
----
-layout: post
-title: Two novels to understand Korea’s political turmoil
----
++++
+title = "Two novels to understand Korea’s political turmoil"
+aliases = [ "/2024/12/08/novels-to-understand-korea.html",]
+
+[params]
+id = "https://maxkapur.com/2024/12/08/novels-to-understand-korea"
++++
On Tuesday night, South Korean President Yoon Suk Yeol imposed martial law
across the country for six hours. He retracted his declaration only after the
diff --git a/_posts/2024-12-26-server-side-katex.md b/content/posts/2024-12-26-server-side-katex.md
similarity index 88%
rename from _posts/2024-12-26-server-side-katex.md
rename to content/posts/2024-12-26-server-side-katex.md
index 666139f8..7a8239bd 100644
--- a/_posts/2024-12-26-server-side-katex.md
+++ b/content/posts/2024-12-26-server-side-katex.md
@@ -1,13 +1,18 @@
----
-layout: post
-title: Server-side KaTeX rendering with Jekyll
----
++++
+title = "Server-side KaTeX rendering with Jekyll"
+aliases = [ "/2024/12/26/server-side-katex.html",]
+
+[params]
+id = "https://maxkapur.com/2024/12/26/server-side-katex"
++++
This post explains how I set up server-side KaTeX rendering to improve the load
-time on pages that include mathematical notation.
+time on pages that include mathematical notation.
+
+
KaTeX is a math typesetting library that lets you render nice-looking math
-equations like $$f(t) = e^{rt}$$ inside of an HTML document. As the [KaTeX
+equations like {{< math "f(t) = e^{rt}" />}} inside of an HTML document. As the [KaTeX
documentation describes](https://katex.org/docs/autorender), the easiest way to
use KaTeX is to import their CSS and JavaScript from your website’s ``
element by adding these three lines:
@@ -20,7 +25,7 @@ element by adding these three lines:
```
With this client-side setup, visitors to your site only receive the KaTeX
-“source” (something like `$$f(t) = e^{rt}$$`) from your server, and then their
+“source” (something like `{{< math "f(t) = e^{rt}" />}}`) from your server, and then their
computer runs the code (KaTeX) that transforms this into MathML and tells the
browser how to style and arrange the individual symbols.
@@ -52,8 +57,7 @@ to achieve this setup using Jekyll, the static site generator I use. In summary:
GitHub](https://github.com/KaTeX/KaTeX/releases) and extract `katex.css` and
`fonts/` to your source repo’s `assets/` directory.
-The last step left me unsatisfied. As I described in a [previous post]({%
-post_url 2024-12-06-self-host-fonts %}), I want to avoid introducing a
+The last step left me unsatisfied. As I described in a [previous post](/posts/2024-12-06-self-host-fonts/), I want to avoid introducing a
third-party code dependency to my source tree—especially one like the KaTeX CSS
that I’d have to keep manually in sync with the version of KaTeX installed by
the Ruby `katex` gem. Instead, I tweaked my `configure.sh` script to
diff --git a/_posts/2025-01-11-newly-excavated-posts.md b/content/posts/2025-01-11-newly-excavated-posts.md
similarity index 67%
rename from _posts/2025-01-11-newly-excavated-posts.md
rename to content/posts/2025-01-11-newly-excavated-posts.md
index b6ab6b08..0605868c 100644
--- a/_posts/2025-01-11-newly-excavated-posts.md
+++ b/content/posts/2025-01-11-newly-excavated-posts.md
@@ -1,19 +1,22 @@
----
-layout: post
-title: Newly excavated posts about teaching
----
++++
+title = "Newly excavated posts about teaching"
+aliases = [ "/2025/01/11/newly-excavated-posts.html",]
+
+[params]
+id = "https://maxkapur.com/2025/01/11/newly-excavated-posts"
++++
When I migrated from Blogger to Jekyll a few years ago, I left a bunch of posts
behind, with the vague resolution of importing them at some point. Today, I
excavated a few more from the archives, all about teaching:
-- The benefits of a
- nonnative teacher (2020): A very long-winded way of saying that language
+- [The benefits of a
+ nonnative teacher](/posts/2020-05-06-nonnative-teacher/) (2020): A very long-winded way of saying that language
learners often benefit from having a teacher who *isn’t* a native speaker of
the target language, because then the teacher has empathy for the student. I
think I stand by this take.
-- Things that are a thing
- here (2018): Just a few days into my Fulbright job, I was enamored with
+- [Things that are a thing
+ here](/posts/2018-08-25-a-thing-here/) (2018): Just a few days into my Fulbright job, I was enamored with
how seriously Korean teachers took their profession—and still am. But knowing
what I do now, I would not write “Art, music, literature, and ‘morality’ class
matter [in Korean schools] just as much as the STEM subjects.” Of course, all
@@ -21,8 +24,8 @@ excavated a few more from the archives, all about teaching:
and parents believe that the best jobs go to STEM graduates. And thus, even if
it’s not their intention, they tend to celebrate students’ success in those
subjects with a bit of extra energy.
-- Centering students as
- creators (2017): Notes from my undergrad internship. I think I see the
+- [Centering students as
+ creators](/posts/2017-08-31-students-as-creators/) (2017): Notes from my undergrad internship. I think I see the
germ here of an idea I’ve grown more certain of since: When you don’t know how
to proceed, the first thing you should do is *start.* You know—open a
document, put your name and the date, choose a font. These first steps shake
diff --git a/_posts/2025-02-12-conda-updated.md b/content/posts/2025-02-12-conda-updated.md
similarity index 83%
rename from _posts/2025-02-12-conda-updated.md
rename to content/posts/2025-02-12-conda-updated.md
index fee15710..8dcdc6fd 100644
--- a/_posts/2025-02-12-conda-updated.md
+++ b/content/posts/2025-02-12-conda-updated.md
@@ -1,7 +1,10 @@
----
-layout: post
-title: Conda updated?
----
++++
+title = "Conda updated?"
+aliases = [ "/2025/02/12/conda-updated.html",]
+
+[params]
+id = "https://maxkapur.com/2025/02/12/conda-updated"
++++
Using a tip from
[Travis Hathaway](https://github.com/conda/conda/issues/14418#issuecomment-2513806325),
diff --git a/_posts/2025-02-14-people-and-blogs-local.md b/content/posts/2025-02-14-people-and-blogs-local.md
similarity index 98%
rename from _posts/2025-02-14-people-and-blogs-local.md
rename to content/posts/2025-02-14-people-and-blogs-local.md
index a1c5bb21..9713de9e 100644
--- a/_posts/2025-02-14-people-and-blogs-local.md
+++ b/content/posts/2025-02-14-people-and-blogs-local.md
@@ -1,9 +1,12 @@
----
-title: People and Blogs interview (local mirror)
-layout: post
-hidden: true
-date: 2025-02-14T12:00:00
----
++++
+title = "People and Blogs interview (local mirror)"
+aliases = [ "/2025/02/14/people-and-blogs-local.html",]
+weight = 10
+
+[params]
+hidden = true
+id = "https://maxkapur.com/2025/02/14/people-and-blogs-local"
++++
Here is a local mirror of my interview on Manuel Moreale's
[People and Blogs](https://manuelmoreale.com/pb-max-kapur) series.
diff --git a/_posts/2025-02-14-people-and-blogs.md b/content/posts/2025-02-14-people-and-blogs.md
similarity index 75%
rename from _posts/2025-02-14-people-and-blogs.md
rename to content/posts/2025-02-14-people-and-blogs.md
index 0207ec8b..24c93499 100644
--- a/_posts/2025-02-14-people-and-blogs.md
+++ b/content/posts/2025-02-14-people-and-blogs.md
@@ -1,14 +1,17 @@
----
-layout: post
-title: People and Blogs interview
-date: 2025-02-14T13:00:00
----
++++
+title = "People and Blogs interview"
+aliases = [ "/2025/02/14/people-and-blogs.html",]
+weight = 20
+
+[params]
+id = "https://maxkapur.com/2025/02/14/people-and-blogs"
++++
Manuel Moreale, a tireless indie web advocate, recently
[had me as a guest on his People and Blogs series](https://manuelmoreale.com/pb-max-kapur)
-([local mirror]({% post_url 2025-02-14-people-and-blogs-local %})).
+([local mirror](/posts/2025-02-14-people-and-blogs-local/)).
Welcome to anyone visiting this site via Manu’s feed! Take a look around and
-[let me know](mailto:{{ site.email }}) what you think.
+[let me know](mailto:max@maxkapur.com) what you think.
I was super excited to be interviewed by Manu, as many of his writings have
sharpened my thinking around why I prefer blogging, email, and group chats to
diff --git a/_posts/2025-02-20-type-em-dash.md b/content/posts/2025-02-20-type-em-dash.md
similarity index 73%
rename from _posts/2025-02-20-type-em-dash.md
rename to content/posts/2025-02-20-type-em-dash.md
index 85b54ec9..968cce5a 100644
--- a/_posts/2025-02-20-type-em-dash.md
+++ b/content/posts/2025-02-20-type-em-dash.md
@@ -1,7 +1,10 @@
----
-layout: post
-title: How to type an em dash
----
++++
+title = "How to type an em dash"
+aliases = [ "/2025/02/20/type-em-dash.html",]
+
+[params]
+id = "https://maxkapur.com/2025/02/20/type-em-dash"
++++
- Phone (Android): Long press the hyphen button
- Desktop (Linux): [Compose key](https://en.wikipedia.org/wiki/Compose_key)
diff --git a/_posts/2025-03-04-check-prove-human.md b/content/posts/2025-03-04-check-prove-human.md
similarity index 61%
rename from _posts/2025-03-04-check-prove-human.md
rename to content/posts/2025-03-04-check-prove-human.md
index fe20d7ea..d8c0d02d 100644
--- a/_posts/2025-03-04-check-prove-human.md
+++ b/content/posts/2025-03-04-check-prove-human.md
@@ -1,7 +1,10 @@
----
-layout: post
-title: Check this box to prove you are a human
----
++++
+title = "Check this box to prove you are a human"
+aliases = [ "/2025/03/04/check-prove-human.html",]
+
+[params]
+id = "https://maxkapur.com/2025/03/04/check-prove-human"
++++
Why is the burden on me to prove that I’m not a robot instead of on the server
to prove I am? This is a vain question if you reflect on the adversarial nature
diff --git a/_posts/2025-03-12-authenticity-ai-policy.md b/content/posts/2025-03-12-authenticity-ai-policy.md
similarity index 89%
rename from _posts/2025-03-12-authenticity-ai-policy.md
rename to content/posts/2025-03-12-authenticity-ai-policy.md
index adfa500a..abda6fe2 100644
--- a/_posts/2025-03-12-authenticity-ai-policy.md
+++ b/content/posts/2025-03-12-authenticity-ai-policy.md
@@ -1,7 +1,10 @@
----
-layout: post
-title: Authenticity and the Illusion Slopes AI policy
----
++++
+title = "Authenticity and the Illusion Slopes AI policy"
+aliases = [ "/2025/03/12/authenticity-ai-policy.html",]
+
+[params]
+id = "https://maxkapur.com/2025/03/12/authenticity-ai-policy"
++++
I attached my real name to this site. That means that above all, this site must
represent me *authentically.*
@@ -28,7 +31,7 @@ marketing. So, I don’t use Illusion Slopes to try to sell anything, nor do I
employ any marketing cookies or user fingerprinting tech to collect statistics
about the people who visit the site. Back when I used Blogger, I used to get
some “dumb” analytics about post views by IP geolocation, but now that I use a
-[static site generator]({% post_url 2022-06-25-migrating-to-jekyll %}),
+[static site generator](/posts/2022-06-25-migrating-to-jekyll/),
I don’t even know which posts are more or less popular. I like it that way; it
ensures I am writing for myself.
@@ -45,6 +48,6 @@ the AI era:
loads quickly (I hope?).
- Everyone who wants to read the site, can. I don’t have to worry about an
aggressive anti-scraping filter that accidentally
- [blocks legitimate users]({% post_url 2025-03-04-check-prove-human %}).
+ [blocks legitimate users](/posts/2025-03-04-check-prove-human/).
- If I write something wrong, then won’t have to issue shallow excuses about
how “the model told me so.” For better or worse, I own my errors.
diff --git a/_posts/2025-03-19-pipx-install-topgrade.md b/content/posts/2025-03-19-pipx-install-topgrade.md
similarity index 87%
rename from _posts/2025-03-19-pipx-install-topgrade.md
rename to content/posts/2025-03-19-pipx-install-topgrade.md
index 227ec362..c260b580 100644
--- a/_posts/2025-03-19-pipx-install-topgrade.md
+++ b/content/posts/2025-03-19-pipx-install-topgrade.md
@@ -1,7 +1,10 @@
----
-layout: post
-title: pipx install topgrade
----
++++
+title = "pipx install topgrade"
+aliases = [ "/2025/03/19/pipx-install-topgrade.html",]
+
+[params]
+id = "https://maxkapur.com/2025/03/19/pipx-install-topgrade"
++++
Many Rust projects, such as [Topgrade](https://github.com/topgrade-rs/topgrade),
also publish themselves on PyPI. This means you can install them using `pip` or
diff --git a/_posts/2025-04-06-circular-regression.md b/content/posts/2025-04-06-circular-regression.md
similarity index 58%
rename from _posts/2025-04-06-circular-regression.md
rename to content/posts/2025-04-06-circular-regression.md
index bede8266..a7b64b76 100644
--- a/_posts/2025-04-06-circular-regression.md
+++ b/content/posts/2025-04-06-circular-regression.md
@@ -1,13 +1,18 @@
----
-layout: post
-title: Circular regression when you already know the period
----
++++
+title = "Circular regression when you already know the period"
+aliases = [ "/2025/04/06/circular-regression.html",]
+
+[params]
+id = "https://maxkapur.com/2025/04/06/circular-regression"
++++
This [Stack Exchange](https://stats.stackexchange.com/a/660199) answer reminded
-me of a useful data science trick. In short, if you try to model $$y$$ as a
-sinusoidal function of $$x$$, you obtain a regression formula that is nonlinear
+me of a useful data science trick. In short, if you try to model {{< math "y" />}} as a
+sinusoidal function of {{< math "x" />}}, you obtain a regression formula that is nonlinear
in the parameters. However, if you know the period, you can use a trig identity
-to linearize the formula and compute an exact least-squares fit.
+to linearize the formula and compute an exact least-squares fit.
+
+
# Background
@@ -18,58 +23,64 @@ this:
with a period of roughly 6.28, amplitude a little less than 1, shifted to the
right by about 6 units.](/assets/images/cyclical-data-before.svg)
-$$y$$ is clearly a sinusoidal function of $$x$$ (plus noise), so there are three
+{{< math "y" />}} is clearly a sinusoidal function of {{< math "x" />}} (plus noise), so there are three
parameters we need to estimate:
-1. The period $$T$$ (distance between peaks)
-2. The amplitude $$A$$ (height of the peaks)
-3. The phase $$\phi$$ (where in the cycle the peaks occur)
+1. The period {{< math "T" />}} (distance between peaks)
+2. The amplitude {{< math "A" />}} (height of the peaks)
+3. The phase {{< math "\phi" />}} (where in the cycle the peaks occur)
For some data, there’s really only one value for the period that makes
-sense. For example, if $$x$$ represents “day of the year” (as in the Stack
+sense. For example, if {{< math "x" />}} represents “day of the year” (as in the Stack
Exchange question), then we are clearly expecting a yearly cycle (or else we
-would have collected exact dates). Thus, $$T = 365.25$$ (or however you want to
+would have collected exact dates). Thus, {{< math "T = 365.25" />}} (or however you want to
handle leap years), and we have only to estimate the amplitude and phase that
best fit the data.
# Problem statement
-Without loss of generality, we can assume that $$T = 2 \pi$$ (if not, multiply
-each $$x_i$$ by $$2 \pi / T$$). Then our objective is to minimize the error in
+Without loss of generality, we can assume that {{< math "T = 2 \pi" />}} (if not, multiply
+each {{< math "x_i" />}} by {{< math "2 \pi / T" />}}). Then our objective is to minimize the error in
the system of equations
-$$y_i = A \sin( x_i + \phi )$$
+{{< math >}}
+y_i = A \sin( x_i + \phi )
+{{< /math >}}
-for each $$(x_i, y_i)$$ in the input data.
+for each {{< math "(x_i, y_i)" />}} in the input data.
-Unfortunately, this model is not linear in the parameters to be estimated ($$A$$
-and $$\phi$$), so we can’t estimate it using ordinary least-squares regression.
+Unfortunately, this model is not linear in the parameters to be estimated ({{< math "A" />}}
+and {{< math "\phi" />}}), so we can’t estimate it using ordinary least-squares regression.
# The trick
The trick is to estimate the following model instead, which *is* linear in the
-parameters $$\beta_0$$ and $$\beta_1$$:
+parameters {{< math "\beta_0" />}} and {{< math "\beta_1" />}}:
-$$y_i = \beta_0 \cos x + \beta_1 \sin x$$
+{{< math >}}
+y_i = \beta_0 \cos x + \beta_1 \sin x
+{{< /math >}}
To see that this model is equivalent, apply a [trig
identity](https://en.wikipedia.org/wiki/List_of_trigonometric_identities#Angle_sum_and_difference_identities)
to find that
-$$A \sin( x_i + \phi ) = A \cos x_i \sin \phi + A \sin x_i \cos \phi.$$
+{{< math >}}
+A \sin( x_i + \phi ) = A \cos x_i \sin \phi + A \sin x_i \cos \phi.
+{{< /math >}}
-Matching coefficients on $$\sin x_i$$ and $$\cos x_i$$, we have
+Matching coefficients on {{< math "\sin x_i" />}} and {{< math "\cos x_i" />}}, we have
-$$
+{{< math >}}
\begin{aligned}
\beta_0 &= A \sin \phi \\
\beta_1 &= A \cos \phi .
\end{aligned}
-$$
+{{< /math >}}
Dividing the first equation by the second, we recover
-$$\phi = \operatorname{atan2}(\beta_0, \beta_1)$$. Then
-$$A = \beta_0 / \sin \phi = \beta_1 / \cos \phi$$.
+{{< math "\phi = \operatorname{atan2}(\beta_0, \beta_1)" />}}. Then
+{{< math "A = \beta_0 / \sin \phi = \beta_1 / \cos \phi" />}}.
# Demo
@@ -122,28 +133,30 @@ well.](/assets/images/cyclical-data-with-fit.svg)
# Notes
**When to retain the linearized parameterization:** In the Python code above, we
-computed $$\phi$$ and $$A$$ just to demonstrate that the technique actually
+computed {{< math "\phi" />}} and {{< math "A" />}} just to demonstrate that the technique actually
works. But the code will throw a zero-division error at the computation of
-`A_pred` if $$\phi$$ is a multiple of $$\pi / 2$$ (which happens when one of the
-$$\beta_j = 0$$). In practice, I recommend discarding the $$A$$ and $$\phi$$
+`A_pred` if {{< math "\phi" />}} is a multiple of {{< math "\pi / 2" />}} (which happens when one of the
+{{< math "\beta_j = 0" />}}). In practice, I recommend discarding the {{< math "A" />}} and {{< math "\phi" />}}
values and running predictions using the
-$$y_i = \beta_0 \cos x + \beta_1 \sin x$$ model instead. Experienced data
+{{< math "y_i = \beta_0 \cos x + \beta_1 \sin x" />}} model instead. Experienced data
scientists will recognize this transformation, but you could add a comment along
-the lines of “this is a linear reparamaterization of $$A \sin( x + \phi )$$.”
+the lines of “this is a linear reparamaterization of {{< math "A \sin( x + \phi )" />}}.”
**Linear combination of sinusoids:** You can also use this technique if your
-$$y$$ variable is a linear combination of sine waves of known periods. For
-example, if $$x$$ is “minute of day” and $$y$$ is “volume of emails,” you might
+{{< math "y" />}} variable is a linear combination of sine waves of known periods. For
+example, if {{< math "x" />}} is “minute of day” and {{< math "y" />}} is “volume of emails,” you might
expect an overall daily trend (period of 24 hours) with smaller variations
within each hour (due to scheduled emails going out at the top of each hour).
-Then you can set $$w_i = 2 \pi x_i / (24 \cdot 60)$$, $$z_i = 2 \pi x_i / 60$$,
+Then you can set {{< math "w_i = 2 \pi x_i / (24 \cdot 60)" />}}, {{< math "z_i = 2 \pi x_i / 60" />}},
and minimize the error in
-$$y_i = A_w \sin( w_i + \phi_w ) + A_z \sin( z_i + \phi_z )$$
+{{< math >}}
+y_i = A_w \sin( w_i + \phi_w ) + A_z \sin( z_i + \phi_z )
+{{< /math >}}
using the transformation above.
-**Fourier transform:** If you *don’t* know the period $$T$$ and your $$x_i$$ are
+**Fourier transform:** If you *don’t* know the period {{< math "T" />}} and your {{< math "x_i" />}} are
evenly spaced, then what you’re looking for is probably not a regression model
but a [discrete Fourier
transform](https://en.wikipedia.org/wiki/Discrete_Fourier_transform).
diff --git a/_posts/2025-04-16-need-smartphone.md b/content/posts/2025-04-16-need-smartphone.md
similarity index 89%
rename from _posts/2025-04-16-need-smartphone.md
rename to content/posts/2025-04-16-need-smartphone.md
index f9296d63..304d0dcc 100644
--- a/_posts/2025-04-16-need-smartphone.md
+++ b/content/posts/2025-04-16-need-smartphone.md
@@ -1,7 +1,10 @@
----
-title: Reasons it’s hard to abandon a smartphone
-layout: post
----
++++
+title = "Reasons it’s hard to abandon a smartphone"
+aliases = [ "/2025/04/16/need-smartphone.html",]
+
+[params]
+id = "https://maxkapur.com/2025/04/16/need-smartphone"
++++
Reasons (excuses?) that I cannot
[downgrade to a dumbphone](https://www.nytimes.com/2025/02/01/opinion/how-quit-smartphone-addiction.html):
@@ -31,6 +34,6 @@ the “prove you own a (smart)phone” tests that most commercial and government
services put you through these days seems to be to combat spam: Acquiring and
maintaining a non-VOIP phone number that can receive SMS messages is too
expensive for most spammers to do at scale. But
-[again]({% post_url 2025-03-04-check-prove-human %}),
+[again](/posts/2025-03-04-check-prove-human/),
I resent that the burden of proof is on me to prove I’m not a spammer instead of
on the service to prove I am.
diff --git a/_posts/2025-04-23-music-videos.md b/content/posts/2025-04-23-music-videos.md
similarity index 91%
rename from _posts/2025-04-23-music-videos.md
rename to content/posts/2025-04-23-music-videos.md
index d04bf10a..76d6e63f 100644
--- a/_posts/2025-04-23-music-videos.md
+++ b/content/posts/2025-04-23-music-videos.md
@@ -1,7 +1,10 @@
----
-title: Music videos
-layout: post
----
++++
+title = "Music videos"
+aliases = [ "/2025/04/23/music-videos.html",]
+
+[params]
+id = "https://maxkapur.com/2025/04/23/music-videos"
++++
A side effect of my refusal to subscribe to Spotify (I just buy albums on
Bandcamp or used CDs) is that when someone sends me a song to listen to, I
diff --git a/_posts/2025-04-30-organize-tabs.md b/content/posts/2025-04-30-organize-tabs.md
similarity index 88%
rename from _posts/2025-04-30-organize-tabs.md
rename to content/posts/2025-04-30-organize-tabs.md
index fef370a4..505fd98b 100644
--- a/_posts/2025-04-30-organize-tabs.md
+++ b/content/posts/2025-04-30-organize-tabs.md
@@ -1,7 +1,10 @@
----
-title: How I organize browser tabs
-layout: post
----
++++
+title = "How I organize browser tabs"
+aliases = [ "/2025/04/30/organize-tabs.html",]
+
+[params]
+id = "https://maxkapur.com/2025/04/30/organize-tabs"
++++
I didn’t realize I had a system for this until I did.
diff --git a/_posts/2025-05-21-tennessee-impressions.md b/content/posts/2025-05-21-tennessee-impressions.md
similarity index 82%
rename from _posts/2025-05-21-tennessee-impressions.md
rename to content/posts/2025-05-21-tennessee-impressions.md
index 4d0461bc..794be455 100644
--- a/_posts/2025-05-21-tennessee-impressions.md
+++ b/content/posts/2025-05-21-tennessee-impressions.md
@@ -1,7 +1,10 @@
----
-layout: post
-title: First impressions after moving to Tennessee
----
++++
+title = "First impressions after moving to Tennessee"
+aliases = [ "/2025/05/21/tennessee-impressions.html",]
+
+[params]
+id = "https://maxkapur.com/2025/05/21/tennessee-impressions"
++++
- *Honey, baby,* etc.
- Even young people have the accent. This surprised me because I thought that
diff --git a/_posts/2025-05-28-guitar-like-piano.md b/content/posts/2025-05-28-guitar-like-piano.md
similarity index 89%
rename from _posts/2025-05-28-guitar-like-piano.md
rename to content/posts/2025-05-28-guitar-like-piano.md
index 5f0b2eda..521a14ef 100644
--- a/_posts/2025-05-28-guitar-like-piano.md
+++ b/content/posts/2025-05-28-guitar-like-piano.md
@@ -1,7 +1,10 @@
----
-title: Still playing guitar like piano
-layout: post
----
++++
+title = "Still playing guitar like piano"
+aliases = [ "/2025/05/28/guitar-like-piano.html",]
+
+[params]
+id = "https://maxkapur.com/2025/05/28/guitar-like-piano"
++++
I still play the guitar like a piano. It’s most obvious when I try to play along
with the lead guitar part in a recording of a two-guitar band. Often, the lead
diff --git a/_posts/2025-06-04-kubuntu-setup.md b/content/posts/2025-06-04-kubuntu-setup.md
similarity index 97%
rename from _posts/2025-06-04-kubuntu-setup.md
rename to content/posts/2025-06-04-kubuntu-setup.md
index 4d3034fb..0592d8e7 100644
--- a/_posts/2025-06-04-kubuntu-setup.md
+++ b/content/posts/2025-06-04-kubuntu-setup.md
@@ -1,13 +1,18 @@
----
-layout: post
-title: My poorly automated Kubuntu setup
----
++++
+title = "My poorly automated Kubuntu setup"
+aliases = [ "/2025/06/04/kubuntu-setup.html",]
+
+[params]
+id = "https://maxkapur.com/2025/06/04/kubuntu-setup"
++++
The target audience for this post is myself. I recently set up a new (old)
laptop and tried to bring it to parity with my main Kubuntu workstation in as
few steps as possible, which turned out to be … a lot of steps, most of them
manual. I wanted to document the full process and see if I can find worthwhile
-opportunities for automation.
+opportunities for automation.
+
+
00. **Install Kubuntu the usual way.** Configure LUKS disk encryption and user
account. `sudo apt update -y && sudo apt upgrade -y`.
@@ -134,7 +139,7 @@ opportunities for automation.
asked to update shell profile. This seems to only affect `bash` so I went
ahead and ran `~/.local/miniforge3/bin/conda init --all`. Make sure I can
configure and build
- [my website]({% post_url 2024-11-29-rbenv-vs-conda %}).
+ [my website](/posts/2024-11-29-rbenv-vs-conda/).
🧪 Hope to automate in `setup.sh`. If
[setup-miniforge](https://github.com/conda-forge/setup-miniforge) can do it,
diff --git a/_posts/2025-06-18-thesaurus-cheating.md b/content/posts/2025-06-18-thesaurus-cheating.md
similarity index 91%
rename from _posts/2025-06-18-thesaurus-cheating.md
rename to content/posts/2025-06-18-thesaurus-cheating.md
index 491d74ad..1d73bcf8 100644
--- a/_posts/2025-06-18-thesaurus-cheating.md
+++ b/content/posts/2025-06-18-thesaurus-cheating.md
@@ -1,7 +1,10 @@
----
-layout: post
-title: Is using a thesaurus cheating?
----
++++
+title = "Is using a thesaurus cheating?"
+aliases = [ "/2025/06/18/thesaurus-cheating.html",]
+
+[params]
+id = "https://maxkapur.com/2025/06/18/thesaurus-cheating"
++++
I remember taking mild offense when my middle school language arts teacher
introduced our class to the thesaurus. I thought, if I let the thesaurus choose
@@ -38,7 +41,7 @@ find highly annoying.)
“We use tools to embody their virtues,” writes
[Fernando Borretti](https://borretti.me/article/you-can-choose-tools-that-make-you-happy).
-I guess I value [authenticity]({% post_url 2025-03-12-authenticity-ai-policy %})
+I guess I value [authenticity](/posts/2025-03-12-authenticity-ai-policy/)
or craftsmanship—my fingerprints on the finished product—more highly than laying
down as much code or text as I can in a fixed amount of time. This value
judgment (and it really is just that) precludes more aggressive uses of LLM tech
diff --git a/_posts/2025-06-26-cls-advice.md b/content/posts/2025-06-26-cls-advice.md
similarity index 84%
rename from _posts/2025-06-26-cls-advice.md
rename to content/posts/2025-06-26-cls-advice.md
index 544d2119..1bd6e117 100644
--- a/_posts/2025-06-26-cls-advice.md
+++ b/content/posts/2025-06-26-cls-advice.md
@@ -1,7 +1,10 @@
----
-layout: post
-title: CLS advice nobody asked for
----
++++
+title = "CLS advice nobody asked for"
+aliases = [ "/2025/06/26/cls-advice.html",]
+
+[params]
+id = "https://maxkapur.com/2025/06/26/cls-advice"
++++
Awesome people occasionally reach out to ask me about the US State Department’s
[Critical Language Scholarship](https://clscholarship.org/) (CLS) program.
@@ -34,8 +37,8 @@ collected and shared over the years.
4. Buy an umbrella in Korea and
[thank me later](https://www.cvs.com/shop/weather-station-folding-automatic-oversize-umbrella-assorted-colors-prodid-457782-reviews).
-And please email me if you are on CLS this
+And please email me if you are on CLS this
year and have any stories to share! I still have a
-[few posts]({% post_url 2016-03-12-cls-korea %}) around
-[from my CLS days]({% post_url 2016-06-25-first-week-class-cnu %}), but nothing
+[few posts](/posts/2016-03-12-cls-korea/) around
+[from my CLS days](/posts/2016-06-25-first-week-class-cnu/), but nothing
profound.
diff --git a/_posts/2025-07-02-tried-recently.md b/content/posts/2025-07-02-tried-recently.md
similarity index 89%
rename from _posts/2025-07-02-tried-recently.md
rename to content/posts/2025-07-02-tried-recently.md
index b80d2eb2..9dc23d00 100644
--- a/_posts/2025-07-02-tried-recently.md
+++ b/content/posts/2025-07-02-tried-recently.md
@@ -1,7 +1,10 @@
----
-layout: post
-title: Some things I tried recently
----
++++
+title = "Some things I tried recently"
+aliases = [ "/2025/07/02/tried-recently.html",]
+
+[params]
+id = "https://maxkapur.com/2025/07/02/tried-recently"
++++
[Kagi Search:](https://kagi.com/) It’s a paid search engine that promises to
give better results than Google and friends. Indeed, the search results are a
@@ -28,7 +31,7 @@ backing tracks are a bit cheesy but well engineered.
you feed it your draft blog post and it complains about vague wording and common
typography problems like curly vs. straight quotes. I like that Proselint uses
regex instead of an LLM, so there’s no
-[creative interference]({% post_url 2025-06-18-thesaurus-cheating %}); it’s more
+[creative interference](/posts/2025-06-18-thesaurus-cheating/); it’s more
like an automated style guide than a chatty editor. But my homegrown
`typography.py` script (I need to upload this to GitHub sometime) enforces a few
lesser irks, such as
diff --git a/_posts/2025-07-09-related-posts.md b/content/posts/2025-07-09-related-posts.md
similarity index 78%
rename from _posts/2025-07-09-related-posts.md
rename to content/posts/2025-07-09-related-posts.md
index 755610e1..999e2287 100644
--- a/_posts/2025-07-09-related-posts.md
+++ b/content/posts/2025-07-09-related-posts.md
@@ -1,7 +1,10 @@
----
-layout: post
-title: "Jekyll plugin to recommend related posts"
----
++++
+title = "Jekyll plugin to recommend related posts"
+aliases = [ "/2025/07/09/related-posts.html",]
+
+[params]
+id = "https://maxkapur.com/2025/07/09/related-posts"
++++
I wrote my first plugin for the Jekyll static website builder: [a tool that
recommends related posts](https://github.com/maxkapur/jekyll-related)
@@ -11,11 +14,13 @@ every site build. You can configure the number of posts to recommend and a
parameter `factor` which determines the algorithm's sensitivity to rare vs.
common words.
-I made a little [demo of the plugin]({{ site.url }}/jekyll-related/) with a fake
+I made a little [demo of the plugin](https://maxkapur.com/jekyll-related/) with a fake
blog whose posts are the articles of the UN Universal Declaration of Human
Rights. You can also see a demo on the current version of this site if you click
the "read more" link below to go to this post's individual page. I think it
-works pretty well!
+works pretty well!
+
+
There are still some tweaks I want to make before I publish this plugin to
[RubyGems.org](https://rubygems.org/). Currently, `jekyll-related` depends on
diff --git a/_posts/2025-08-20-full-of-types.md b/content/posts/2025-08-20-full-of-types.md
similarity index 94%
rename from _posts/2025-08-20-full-of-types.md
rename to content/posts/2025-08-20-full-of-types.md
index 65a4e3bb..b44fe1c2 100644
--- a/_posts/2025-08-20-full-of-types.md
+++ b/content/posts/2025-08-20-full-of-types.md
@@ -1,7 +1,10 @@
----
-title: Full of types
-layout: post
----
++++
+title = "Full of types"
+aliases = [ "/2025/08/20/full-of-types.html",]
+
+[params]
+id = "https://maxkapur.com/2025/08/20/full-of-types"
++++
I’m obsessed with this essay
["Raising a person in a culture full of types"](https://theoutline.com/post/8627/raising-a-teen-personality-types-growth-mindset)
diff --git a/_posts/2025-09-03-abc-smc-evolutionary-algorithm.md b/content/posts/2025-09-03-abc-smc-evolutionary-algorithm.md
similarity index 58%
rename from _posts/2025-09-03-abc-smc-evolutionary-algorithm.md
rename to content/posts/2025-09-03-abc-smc-evolutionary-algorithm.md
index 8c644e79..645627b4 100644
--- a/_posts/2025-09-03-abc-smc-evolutionary-algorithm.md
+++ b/content/posts/2025-09-03-abc-smc-evolutionary-algorithm.md
@@ -1,17 +1,19 @@
----
-title: Is ABC-SMC just an evolutionary algorithm?
-layout: post
-katex: true
----
-
-Suppose we have data $$D$$ and a model that expresses $$D$$ as a noisy function
-of a parameter vector $$\theta$$. We want to determine a value of $$\theta$$
++++
+title = "Is ABC-SMC just an evolutionary algorithm?"
+aliases = [ "/2025/09/03/abc-smc-evolutionary-algorithm.html",]
+
+[params]
+id = "https://maxkapur.com/2025/09/03/abc-smc-evolutionary-algorithm"
++++
+
+Suppose we have data {{< math "D" />}} and a model that expresses {{< math "D" />}} as a noisy function
+of a parameter vector {{< math "\theta" />}}. We want to determine a value of {{< math "\theta" />}}
that fits the data. For the purposes of this post, we're concerned with models
that are "difficult," meaning we cannot write down a simple expression for the
likelihood function and maximize it, whether analytically (as in ordinary least
squares) or numerically (as in nonlinear regression). In fact, all we really
-know how to do is sample data from the model when given an arbitrary $$\theta$$.
-(We'll get a different $$D$$ every time, because the model is nondeterministic.)
+know how to do is sample data from the model when given an arbitrary {{< math "\theta" />}}.
+(We'll get a different {{< math "D" />}} every time, because the model is nondeterministic.)
If you enjoy Bayesian statistics, then you may have already pattern-matched this
problem statement to the ABC-SMC algorithm. But if you are like me and view
@@ -27,22 +29,22 @@ implementation and usage of both evolution and ABC-SMC.
OK, so the basic template for an evolutionary algorithm looks like this:
-1. Guess $$N$$ different parameter vectors $$\theta_i$$ (e.g. by sampling them
+1. Guess {{< math "N" />}} different parameter vectors {{< math "\theta_i" />}} (e.g. by sampling them
from a distribution or applying a heuristic to the data).
-2. For each $$\theta_i$$, simulate the model to generate a bunch of data
- $$D'_i$$.
-3. Compare each instance of simulated data $$D'_i$$ to the real data $$D$$,
- perhaps with a distance measure $$d(D'_i, D)$$ like the 2-norm or
+2. For each {{< math "\theta_i" />}}, simulate the model to generate a bunch of data
+ {{< math "D'_i" />}}.
+3. Compare each instance of simulated data {{< math "D'_i" />}} to the real data {{< math "D" />}},
+ perhaps with a distance measure {{< math "d(D'_i, D)" />}} like the 2-norm or
summary statistics.
-4. Pick a subset of the $$\theta$$ values, perhaps the $$k$$ with the lowest
+4. Pick a subset of the {{< math "\theta" />}} values, perhaps the {{< math "k" />}} with the lowest
distance measure.
-5. Generate a new sample of size $$N$$, perhaps by sampling randomly from the
- $$\theta_i$$s that "survived" the previous step and adding random noise.
+5. Generate a new sample of size {{< math "N" />}}, perhaps by sampling randomly from the
+ {{< math "\theta_i" />}}s that "survived" the previous step and adding random noise.
6. Repeat an arbitrary number of times from step 2.
There are a lot of details to fill in, particularly in the choice of loss
function and resampling method.[^details] But essentially, we try to sample new
-$$\theta$$ values in a way that is *biased* toward the winners identified in
+{{< math "\theta" />}} values in a way that is *biased* toward the winners identified in
step 4 (exploitation), while maintaining enough random noise in step 5 to escape
local optima (exploration). If we strike the right balance between exploitation
and exploration, evolutionary algorithms like this can be quite effective in
@@ -60,22 +62,21 @@ computation
(Depending on the order in which you learned things) an intuitive way to think
about ABC is as an alternative to Markov Chain Monte Carlo (MCMC). Algorithms in
-the MCMC family take a likelihood function $$l(\theta)$$ and produce a sample of
+the MCMC family take a likelihood function {{< math "l(\theta)" />}} and produce a sample of
the probability distribution implied. This is useful because for many
statistical models, we can easily *construct* a likelihood function
([PyMC](https://www.pymc.io/welcome.html) will do it for us), but maximizing
-$$l(\theta)$$ is hard (for example, due to nonconcavity or expensive gradient
-computations). With MCMC, we can just draw samples from the posterior $$P(\theta
-| D)$$, which produces not only a point estimate for each entry of $$\theta$$,
+{{< math "l(\theta)" />}} is hard (for example, due to nonconcavity or expensive gradient
+computations). With MCMC, we can just draw samples from the posterior {{< math "P(\theta | D)" />}}, which produces not only a point estimate for each entry of {{< math "\theta" />}},
but also confidence bounds, their covariance … indeed, an estimate of the entire
distribution.
Unfortunately, MCMC proves inadequate for regression problems (many involving
rank and order statistics) where we cannot write out the likelihood function in
a computationally tractable form. Consider, for example, a model where we draw
-$$X$$ from a multivariate normal distribution (whose mean and covariance matrix
-are part of $$\theta$$), clamp these values to unknown lower and upper bounds
-(more parameters in $$\theta$$), then observe (as $$D$$) the third order
+{{< math "X" />}} from a multivariate normal distribution (whose mean and covariance matrix
+are part of {{< math "\theta" />}}), clamp these values to unknown lower and upper bounds
+(more parameters in {{< math "\theta" />}}), then observe (as {{< math "D" />}}) the third order
statistic. I don't think this will yield a useful likelihood function (although
I admit I haven't tried). Thus, we turn to ABC.
@@ -83,59 +84,58 @@ I admit I haven't tried). Thus, we turn to ABC.
ABC is a sketchy alternative to MCMC which works even when we cannot compute a
likelihood function. All we need is the ability to sample data from the model
-for given values of $$\theta$$, just like our problem. [The ABC rejection
+for given values of {{< math "\theta" />}}, just like our problem. [The ABC rejection
algorithm](https://en.wikipedia.org/wiki/Approximate_Bayesian_computation#The_ABC_rejection_algorithm)
is basically this:
-1. Sample a $$\theta$$ vector from a prior distribution.
-2. Sample data $$D'$$ from the model.
-3. Compare the data $$D'$$ to the sample $$D$$ using a distance measure $$d(D',
- D)$$.
-4. If the distance is less than tolerance $$\varepsilon$$, accept
- $$\theta$$. If not, repeat.
+1. Sample a {{< math "\theta" />}} vector from a prior distribution.
+2. Sample data {{< math "D'" />}} from the model.
+3. Compare the data {{< math "D'" />}} to the sample {{< math "D" />}} using a distance measure {{< math "d(D', D)" />}}.
+4. If the distance is less than tolerance {{< math "\varepsilon" />}}, accept
+ {{< math "\theta" />}}. If not, repeat.
I call this algorithm "sketchy" because before running it, we don't know how
-many times it will loop before accepting $$N$$ posterior samples. To maximize
-the sample's accuracy, step 4 tempts us to set $$\varepsilon$$ close to zero.
+many times it will loop before accepting {{< math "N" />}} posterior samples. To maximize
+the sample's accuracy, step 4 tempts us to set {{< math "\varepsilon" />}} close to zero.
But doing so might drive the acceptance rate down to 0.1%, in which case we must
draw millions of samples from the prior to get thousands from the posterior. (We
might as well just figure out how many iterations we can afford to compute—say,
-100,000—and accept the 1000 samples of $$\theta$$ with the lowest distance
+100,000—and accept the 1000 samples of {{< math "\theta" />}} with the lowest distance
measure. Sound familiar?)
We can tame the ABC rejection algorithm using a modification called sequential
Monte Carlo (SMC); [here is a good tutorial
(PDF)](https://arxiv.org/pdf/0910.4472). Instead of picking a huge sample size
-and tiny $$\varepsilon$$ and crossing our fingers, ABC-SMC has us gradually ease
-the tolerance $$\varepsilon$$ toward zero as the sample converges to the "true"
+and tiny {{< math "\varepsilon" />}} and crossing our fingers, ABC-SMC has us gradually ease
+the tolerance {{< math "\varepsilon" />}} toward zero as the sample converges to the "true"
posterior. The algorithm looks like this:
-1. Sample $$N$$ vectors $$\theta$$ from a prior distribution; call this the
+1. Sample {{< math "N" />}} vectors {{< math "\theta" />}} from a prior distribution; call this the
"population."
2. Construct a new population using ABC rejection sampling, with a tweak:
- 1. Sample a $$\theta$$ vector from the population.
- 2. Tweak: Add random noise to $$\theta$$.
- 3. Sample data $$D'$$ from the model.
- 4. Compare the data $$D'$$ to the sample $$D$$ using a distance measure.
- 5. If the distance is less than tolerance $$\varepsilon$$, accept $$\theta$$
+ 1. Sample a {{< math "\theta" />}} vector from the population.
+ 2. Tweak: Add random noise to {{< math "\theta" />}}.
+ 3. Sample data {{< math "D'" />}} from the model.
+ 4. Compare the data {{< math "D'" />}} to the sample {{< math "D" />}} using a distance measure.
+ 5. If the distance is less than tolerance {{< math "\varepsilon" />}}, accept {{< math "\theta" />}}
and add it to the new population.
- 6. Repeat from step 2.1 until the population has size $$N$$.
+ 6. Repeat from step 2.1 until the population has size {{< math "N" />}}.
-3. Keep repeating step 2 with decreasing values of $$\varepsilon$$.
+3. Keep repeating step 2 with decreasing values of {{< math "\varepsilon" />}}.
This algorithm is more practical than pure ABC because we can log the acceptance
rate at each iteration in order to see how it is doing. If the acceptance rate
is too small, then we restart with a less aggressive tolerance schedule (i.e.
-$$\varepsilon$$ decreasing more gradually).
+{{< math "\varepsilon" />}} decreasing more gradually).
The tweak in step 2.2 (adding extra noise) is necessary to enable ABC-SMC to
-search beyond the $$N$$ samples initially drawn from the prior. The scale of
-this extra noise—call it $$\sigma$$—is an additional free parameter in the
-algorithm design. If $$\sigma$$ is too large, then the algorithm won't converge,
+search beyond the {{< math "N" />}} samples initially drawn from the prior. The scale of
+this extra noise—call it {{< math "\sigma" />}}—is an additional free parameter in the
+algorithm design. If {{< math "\sigma" />}} is too large, then the algorithm won't converge,
because each pass through step 2 resembles a fresh run of the ABC rejection
-sample (poor exploitation). If $$\sigma$$ is too small, then the algorithm gets
-stuck in the neighborhood of whichever $$\theta$$ from the initial sample
+sample (poor exploitation). If {{< math "\sigma" />}} is too small, then the algorithm gets
+stuck in the neighborhood of whichever {{< math "\theta" />}} from the initial sample
happened to be the best (poor exploration). So, like the evolutionary algorithm,
ABC-SMC is no turnkey solution; it requires parameter tuning to balance
exploration and exploitation effectively.
@@ -147,26 +147,25 @@ else these algorithms may have in common. Upon examination, we can recognize
step 2 in the ABC-SMC algorithm as another way of implementing steps 2 through 5
in the evolutionary algorithm. The difference is that in the evolutionary
algorithm (or at least the way I wrote it out), we choose the acceptance rate
-$$k / N$$ at the outset, and the acceptance threshold $$\varepsilon =
-\max\{d(D'_i, D)\}$$ (over the surviving indices $$i$$) is implied. In ABC-SMC,
-it's just the opposite; we commit first to a tolerance $$\varepsilon$$, then
+{{< math "k / N" />}} at the outset, and the acceptance threshold {{< math "\varepsilon = \max\{d(D'_i, D)\}" />}} (over the surviving indices {{< math "i" />}}) is implied. In ABC-SMC,
+it's just the opposite; we commit first to a tolerance {{< math "\varepsilon" />}}, then
keep sampling until we've filled up the population.[^superficial]
Recognizing the similarity between evolution and ABC-SMC might lead us to
discover new algorithms by mixing features from each. For example, in ABC-SMC,
instead of imposing a series of decreasing tolerance, we could choose a fixed,
-large *sample size* from which we always accept the top $$N$$ by distance.
+large *sample size* from which we always accept the top {{< math "N" />}} by distance.
Because the samples converge,[^tuning] the sequence of (implicit) tolerances
still decreases, so this algorithm should still afford the Bayesian
interpretation of producing a *sample from the posterior* (not just a point
-estimate of $$\theta$$). But this modification makes the algorithm's runtime and
+estimate of {{< math "\theta" />}}). But this modification makes the algorithm's runtime and
memory usage deterministic, a desirable property (for example, it lets us
preallocate all the arrays for a performance boost).
Another idea we can transplant from evolution to ABC-SMC is forced exploration.
In evolutionary algorithms, a common practice is to always include a few totally
random samples in each new population. In ABC-SMC, this could look like
-modifying step 2 to construct a new population using $$\lceil 0.9 N \rceil$$ ABC
+modifying step 2 to construct a new population using {{< math "\lceil 0.9 N \rceil" />}} ABC
rejection samples, and filling the rest with fresh draws from the initial prior.
This should reduce the ABC-SMC's propensity to get stuck in a local optimum (a
real problem, in my experience), at the cost of a lower acceptance rate and
@@ -175,24 +174,24 @@ longer runtime.
What about going the other way—incorporating ideas from ABC-SMC into evolution?
In evolutionary algorithms, the custom is to return the global best solution,
but one can argue that the entire final population, as a "sample" of parameter
-values that survived the top-$$k$$ filter repeatedly, deserves more attention.
+values that survived the top-{{< math "k" />}} filter repeatedly, deserves more attention.
Like a Bayesian posterior, this population conveys the *distribution* of good
-$$\theta$$ estimates, which could have properties such as clustering or
+{{< math "\theta" />}} estimates, which could have properties such as clustering or
correlation that warrant further investigation.
[^details]: And we should take care to ensure that the size of each sample
- $$D'_i$$ is sufficiently large to ward off a [winner's
+ {{< math "D'_i" />}} is sufficiently large to ward off a [winner's
curse](https://en.wikipedia.org/wiki/Winner%27s_curse) outcome where the
- $$\theta$$ you return is actually a good estimate, not just a "lucky"
+ {{< math "\theta" />}} you return is actually a good estimate, not just a "lucky"
instance where all the noise happened to point in the right direction.
[^superficial]: Another difference between the algorithms as I laid them out is
- that in my evolutionary algorithm, you upsample the $$k$$ "survivors" into a
- size-$$N$$ population at the end of the inner loop (step 5), whereas in
+ that in my evolutionary algorithm, you upsample the {{< math "k" />}} "survivors" into a
+ size-{{< math "N" />}} population at the end of the inner loop (step 5), whereas in
ABC-SMC the upsampling occurs implicitly in steps 2.1 and 2.2. I think this
difference is superficial—it's a matter of setting up each iteration at the
end of the previous iteration versus at the start of the current one. Note
- that $$k$$ (not $$N$$) in the evolutionary algorithm corresponds to $$N$$ in
+ that {{< math "k" />}} (not {{< math "N" />}}) in the evolutionary algorithm corresponds to {{< math "N" />}} in
ABC-SMC.
-[^tuning]: Well, *if* the samples converge—which depends on tuning $$\sigma$$.
+[^tuning]: Well, *if* the samples converge—which depends on tuning {{< math "\sigma" />}}.
diff --git a/_posts/2025-09-18-tension-condaverse.md b/content/posts/2025-09-18-tension-condaverse.md
similarity index 95%
rename from _posts/2025-09-18-tension-condaverse.md
rename to content/posts/2025-09-18-tension-condaverse.md
index 6ced805b..f4da4045 100644
--- a/_posts/2025-09-18-tension-condaverse.md
+++ b/content/posts/2025-09-18-tension-condaverse.md
@@ -1,7 +1,10 @@
----
-layout: post
-title: Tensions rise in the condaverse
----
++++
+title = "Tensions rise in the condaverse"
+aliases = [ "/2025/09/18/tension-condaverse.html",]
+
+[params]
+id = "https://maxkapur.com/2025/09/18/tension-condaverse"
++++
GitHub has a tool called Dependabot that automatically finds outdated package
versions pinned in project configuration files and issues a pull request to
diff --git a/_posts/2025-09-24-python-scripts.md b/content/posts/2025-09-24-python-scripts.md
similarity index 73%
rename from _posts/2025-09-24-python-scripts.md
rename to content/posts/2025-09-24-python-scripts.md
index 05607ff7..59170069 100644
--- a/_posts/2025-09-24-python-scripts.md
+++ b/content/posts/2025-09-24-python-scripts.md
@@ -1,7 +1,10 @@
----
-title: Python scripts
-layout: post
----
++++
+title = "Python scripts"
+aliases = [ "/2025/09/24/python-scripts.html",]
+
+[params]
+id = "https://maxkapur.com/2025/09/24/python-scripts"
++++
I cleaned up a few utility Python scripts for the GitHub:
@@ -11,10 +14,10 @@ I cleaned up a few utility Python scripts for the GitHub:
a systemd unit file, so you can run the script once, bookmark the local URL,
and forget about it.
- [typography.py](https://github.com/maxkapur/typography.py) (which I mentioned
- [here]({% post_url 2025-07-02-tried-recently %})) checks for ASCII typography
+ [here](/posts/2025-07-02-tried-recently/)) checks for ASCII typography
that can be better rendered as Unicode. For example, it recommends changing
the hyphen in the page range `278-81` to an en dash.
- [dated.py](https://github.com/maxkapur/dated.py) applies my [obnoxious
- filename convention]({% post_url 2024-04-26-iso-dates-filenames %}) to create
+ filename convention](/posts/2024-04-26-iso-dates-filenames/) to create
a dated working copy of a file—useful when collaborating with people who
aren’t comfortable with version control systems.
diff --git a/_posts/2025-10-21-cls-application.md b/content/posts/2025-10-21-cls-application.md
similarity index 72%
rename from _posts/2025-10-21-cls-application.md
rename to content/posts/2025-10-21-cls-application.md
index 86e4fc5a..653ede50 100644
--- a/_posts/2025-10-21-cls-application.md
+++ b/content/posts/2025-10-21-cls-application.md
@@ -1,7 +1,10 @@
----
-title: Apply for CLS
-layout: post
----
++++
+title = "Apply for CLS"
+aliases = [ "/2025/10/21/cls-application.html",]
+
+[params]
+id = "https://maxkapur.com/2025/10/21/cls-application"
++++
This year's application for Critical Language Scholarship from the US State
Department is now [open](https://clscholarship.org/apply). (Thanks, group chat!)
@@ -12,10 +15,10 @@ learning one of the nine
[critical languages](https://clscholarship.org/languages) to apply. Some
languages require prior language study; others accept beginners.
-*Feel free to email me* if you are
+*Feel free to email me* if you are
considering the CLS program or have questions about the experience. I completed
CLS Korean in 2016 and have kept in touch with participants from lots of cohorts
-since then. Here is my [advice post]({%- post_url 2025-06-26-cls-advice -%}).
+since then. Here is my [advice post](/posts/2025-06-26-cls-advice/).
Good CLS blogs:
diff --git a/_posts/2025-11-23-urban-arts.md b/content/posts/2025-11-23-urban-arts.md
similarity index 83%
rename from _posts/2025-11-23-urban-arts.md
rename to content/posts/2025-11-23-urban-arts.md
index df5fdd51..3486304c 100644
--- a/_posts/2025-11-23-urban-arts.md
+++ b/content/posts/2025-11-23-urban-arts.md
@@ -1,7 +1,10 @@
----
-title: Urban Arts Career Pathways Takeover
-layout: post
----
++++
+title = "Urban Arts Career Pathways Takeover"
+aliases = [ "/2025/11/23/urban-arts.html",]
+
+[params]
+id = "https://maxkapur.com/2025/11/23/urban-arts"
++++
My team at work recently got to host a Career Pathways Takeover at
[Urban Arts](https://urbanarts.org/), a nonprofit based in New York that teaches
diff --git a/_posts/2025-11-26-time-person-2006.md b/content/posts/2025-11-26-time-person-2006.md
similarity index 93%
rename from _posts/2025-11-26-time-person-2006.md
rename to content/posts/2025-11-26-time-person-2006.md
index 267480e7..b5d0bca4 100644
--- a/_posts/2025-11-26-time-person-2006.md
+++ b/content/posts/2025-11-26-time-person-2006.md
@@ -1,7 +1,10 @@
----
-title: Person of the Year 2006
-layout: post
----
++++
+title = "Person of the Year 2006"
+aliases = [ "/2025/11/26/time-person-2006.html",]
+
+[params]
+id = "https://maxkapur.com/2025/11/26/time-person-2006"
++++
If you need a fun fact for a corporate icebreaker activity, you can always say,
"I was *Time* magazine’s Person of the Year in 2006," then stand around
diff --git a/_posts/2025-12-10-pytest-ruff-mypy.md b/content/posts/2025-12-10-pytest-ruff-mypy.md
similarity index 91%
rename from _posts/2025-12-10-pytest-ruff-mypy.md
rename to content/posts/2025-12-10-pytest-ruff-mypy.md
index b9f12df1..ebb52e62 100644
--- a/_posts/2025-12-10-pytest-ruff-mypy.md
+++ b/content/posts/2025-12-10-pytest-ruff-mypy.md
@@ -1,7 +1,10 @@
----
-title: Pytest + Ruff + Mypy
-layout: post
----
++++
+title = "Pytest + Ruff + Mypy"
+aliases = [ "/2025/12/10/pytest-ruff-mypy.html",]
+
+[params]
+id = "https://maxkapur.com/2025/12/10/pytest-ruff-mypy"
++++
There is a [pytest-ruff](https://pypi.org/project/pytest-ruff/) plugin for
Pytest (a Python testing framework) that will automatically run
diff --git a/_posts/2025-12-19-perfect-match-integer-programming.md b/content/posts/2025-12-19-perfect-match-integer-programming.md
similarity index 78%
rename from _posts/2025-12-19-perfect-match-integer-programming.md
rename to content/posts/2025-12-19-perfect-match-integer-programming.md
index c8d335c6..e2243df7 100644
--- a/_posts/2025-12-19-perfect-match-integer-programming.md
+++ b/content/posts/2025-12-19-perfect-match-integer-programming.md
@@ -1,14 +1,16 @@
----
-layout: post
-katex: true
-title: Find your perfect match with integer programming
----
++++
+title = "Find your perfect match with integer programming"
+aliases = [ "/2025/12/19/perfect-match-integer-programming.html",]
+
+[params]
+id = "https://maxkapur.com/2025/12/19/perfect-match-integer-programming"
++++
Owen Lacey
[blogged](https://blog.owenlacey.dev/posts/are-you-the-one-is-free-money/) about
a reality game show called *Are You the One?* in which contestants win a
prize by guessing the soulmate ordained for them by the show's producers.
-Specifically, there are $$n$$ men and $$n$$ women, and each one has an unknown
+Specifically, there are {{< math "n" />}} men and {{< math "n" />}} women, and each one has an unknown
"perfect" match; to win the prize, the contestants (as a group) must pair
everyone up correctly.
@@ -20,7 +22,7 @@ During each episode, the contestants get two kinds of clues:
couple) and learn the number (but not the identity) of perfect matches
present in their matching.
-The game ends after Match Up if all $$n$$ couples in the matching are correct.
+The game ends after Match Up if all {{< math "n" />}} couples in the matching are correct.
Definitely check out Owen's post, which has a better (and illustrated!)
explanation of the rules.
@@ -28,7 +30,9 @@ Below, I present an efficient algorithm for playing Are You the One.
It exploits both the informational clues and contestants' intuitions to find
perfect matches quickly. With modest assumptions on the quality of
players' intuitions, my algorithm wins by episode 10 in 100% of simulated
-seasons.
+seasons.
+
+
(I will style the name of the show as *Are You the One?* and the name of the
underlying game as just Are You the One.)
@@ -38,9 +42,9 @@ underlying game as just Are You the One.)
To set the stage, let me summarize Owen's approach. In short, he chooses Truth
Booth and Match Up submissions on the basis of the expected information to be gained.
-Imagine writing down a list of all $$n!$$ possible matchings at the beginning of
+Imagine writing down a list of all {{< math "n!" />}} possible matchings at the beginning of
the season. If, in the episode 1 Truth Booth, we learn that Alice and
-Bob *aren't* a perfect match, we can cross off all $$n! / n$$ matchings that
+Bob *aren't* a perfect match, we can cross off all {{< math "n! / n" />}} matchings that
include this couple. That's progress.
Similarly, after we get our Match Up score
@@ -113,29 +117,29 @@ solutions from the search space.
## Statistical model of contestant intuitions
To generate random instances, my model first assigns each couple a compatibility score
-$$c_{ij}$$ drawn from a standard normal distribution. These values represent
-the "true" compatibility of man $$i$$ with woman $$j$$ (according to the
+{{< math "c_{ij}" />}} drawn from a standard normal distribution. These values represent
+the "true" compatibility of man {{< math "i" />}} with woman {{< math "j" />}} (according to the
show's producers). We then solve the
[maximum-weight bipartite matching](https://en.wikipedia.org/wiki/Assignment_problem)
problem to obtain the ground-truth perfect matching.
This computation sounds fancier than
it is; the idea is to match everyone up in a way that maximizes the overall
-sum of compatibility scores. [More sophisticated matching algorithms]({%- post_url 2021-03-07-stable-matching-planet-money -%})
+sum of compatibility scores. [More sophisticated matching algorithms](/posts/2021-03-07-stable-matching-planet-money/)
exist, but the maximum-weight procedure suits our purposes well.
-It generates each of the possible $$n!$$ matchings with equal probability, and
-it comes with extra data $$c_{ij}$$ that we can use to drive intuitions.
+It generates each of the possible {{< math "n!" />}} matchings with equal probability, and
+it comes with extra data {{< math "c_{ij}" />}} that we can use to drive intuitions.
-Contestants don't get to see $$c_{ij}$$, but instead observe
-$$v_{ij}$$, which is $$c_{ij}$$ plus a normal variate with mean zero and
-standard deviation $$2^{-s}$$. Here $$s$$ represents the strength of
-contestants' intuition. If $$s \ll 0$$, then their intuitions are essentially
-random noise; if $$s \gg 0$$ then their intuitions are spot-on.
+Contestants don't get to see {{< math "c_{ij}" />}}, but instead observe
+{{< math "v_{ij}" />}}, which is {{< math "c_{ij}" />}} plus a normal variate with mean zero and
+standard deviation {{< math "2^{-s}" />}}. Here {{< math "s" />}} represents the strength of
+contestants' intuition. If {{< math "s \ll 0" />}}, then their intuitions are essentially
+random noise; if {{< math "s \gg 0" />}} then their intuitions are spot-on.
-The strength parameter $$s$$ enables us to test how bad players' intuitions
+The strength parameter {{< math "s" />}} enables us to test how bad players' intuitions
can get before a pessimistic algorithm like Owen's has better odds of
-winning than one based on vibes. I chose to scale $$s$$ exponentially on the
-hunch that this would yield a roughly linear relationship between $$s$$ and the
+winning than one based on vibes. I chose to scale {{< math "s" />}} exponentially on the
+hunch that this would yield a roughly linear relationship between {{< math "s" />}} and the
number of turns to win (which turned out to be true).
## Match Up algorithm
@@ -145,24 +149,23 @@ builds on it.
In my algorithm, we pick the guess for Match Up that best resonates with our
vibes without contradicting any of the clues we've gathered. Formally, we compute
-the maximum-weight bipartite matching using the $$v_{ij}$$ values as the weights,
+the maximum-weight bipartite matching using the {{< math "v_{ij}" />}} values as the weights,
while applying logical constraints generated by previous rounds of Truth Booth
and Match Up.
-Let the binary decision variable $$x_{ij}$$ equal one if man
-$$i$$ matches with woman $$j$$ in our Match Up submission, and zero if not.
+Let the binary decision variable {{< math "x_{ij}" />}} equal one if man
+{{< math "i" />}} matches with woman {{< math "j" />}} in our Match Up submission, and zero if not.
-Let $$P$$ denote the set of couples $$(i, j)$$ known (from the Truth Booth) to be
-perfect matches, and $$\cancel{P}$$ the set of couples known *not* to be perfect
+Let {{< math "P" />}} denote the set of couples {{< math "(i, j)" />}} known (from the Truth Booth) to be
+perfect matches, and {{< math "\cancel{P}" />}} the set of couples known *not* to be perfect
matches.
-Let $$\mathcal{M}$$ denote the set of Match Up results. The elements of
-$$\mathcal{M}$$ are tuples $$(M, k)$$, where $$M$$ is the set of couples $$(i,
-j)$$ submitted, and $$k$$ is the score.
+Let {{< math "\mathcal{M}" />}} denote the set of Match Up results. The elements of
+{{< math "\mathcal{M}" />}} are tuples {{< math "(M, k)" />}}, where {{< math "M" />}} is the set of couples {{< math "(i, j)" />}} submitted, and {{< math "k" />}} is the score.
The solution to the following IP is called the *best matching:*
-$$
+{{< math >}}
\begin{aligned}
\text{maximize} \quad & \sum_{i=1}^n \sum_{j=1}^n v_{ij} x_{ij} \\
\text{subject to} \quad & \sum_{j=1}^n x_{ij} = 1 & \forall i \in \{1 \dots n\} \\
@@ -172,12 +175,12 @@ $$
& \sum_{(i, j) \in M} x_{ij} = k & \forall (M, k) \in \mathcal{M} \\
& x_{ij} \text{ binary} & \forall (i, j) \in \{1 \dots n\}^2
\end{aligned}
-$$
+{{< /math >}}
The first two constraints define the
[bipartite matching polytope](https://en.wikipedia.org/wiki/Assignment_problem#Solution_by_linear_programming).
-The following three constraints require that $$x_{ij}$$ agree with our clues.
-The final constraint just says that $$x_{ij}$$ is binary (not a fraction).
+The following three constraints require that {{< math "x_{ij}" />}} agree with our clues.
+The final constraint just says that {{< math "x_{ij}" />}} is binary (not a fraction).
## Truth Booth algorithm
@@ -187,7 +190,7 @@ Booth rules out more matchings than an incorrect guess. So, for the Truth Booth,
algorithm tries to make a guess that is *not certain,* but still *likely* to be
correct.
-First, we compute the best matching using the IP above. Any of the $$(i, j)$$
+First, we compute the best matching using the IP above. Any of the {{< math "(i, j)" />}}
pairs in the best matching is a *valid* guess for truth booth. But some of them
are *silly* guesses, because we already know they are a perfect match; we
should filter these out (hence the "not certain" criterion above).
@@ -197,10 +200,10 @@ perfect match in a past Truth Booth. No sense submitting them again.
Among the remaining couples, even without a Truth Booth result, we may know
*implicitly* that they go together, because all matchings in which they don't
-are contradicted by the sum of our clues. To detect if $$(i, j)$$ from the best
+are contradicted by the sum of our clues. To detect if {{< math "(i, j)" />}} from the best
matching is an implicit perfect match, we can solve another IP, which consists
-of the best matching IP with the additional constraint $$x_{ij} = 0$$. If this
-adjusted IP is *infeasible,* then $$(i, j)$$ is an implicit perfect match, so
+of the best matching IP with the additional constraint {{< math "x_{ij} = 0" />}}. If this
+adjusted IP is *infeasible,* then {{< math "(i, j)" />}} is an implicit perfect match, so
we shouldn't submit that couple to the truth booth.
The couples that survive these filters are *flexible couples,*
@@ -222,7 +225,7 @@ I ran three experiments to verify that my algorithm fulfills the design objectiv
## Experiment 1: Intuition strength vs. turns to win
-For this experiment, we sample $$s$$ uniformly between $$-5$$ and $$5$$. These
+For this experiment, we sample {{< math "s" />}} uniformly between {{< math "-5" />}} and {{< math "5" />}}. These
endpoints represent signal-to-noise ratios of 1:32 and 32:1, respectively, so
the x-axis in the plot below runs the [gamut](https://www.etymonline.com/word/gamut)
from "virtually no intuition" to "spot-on intuition."
@@ -235,7 +238,7 @@ guess in Match Up.
@@ -246,12 +249,12 @@ of the plot.
## Experiment 2: No intuition
To stress-test my algorithm, let's focus on the left edge of the experiment 1
-plot and slide the intuition strength all the way down to $$s = -\infty$$. (What I actually do is
-delete $$c_{ij}$$ entirely from the expression for $$v_{ij}$$ and sample
+plot and slide the intuition strength all the way down to {{< math "s = -\infty" />}}. (What I actually do is
+delete {{< math "c_{ij}" />}} entirely from the expression for {{< math "v_{ij}" />}} and sample
the vibes as pure Gaussian noise.)
This case is similar to Owen's setup. Participants have no intuition about which
-couples are perfect matches, and their vibes $$v_{ij}$$ serve only to give the IP
+couples are perfect matches, and their vibes {{< math "v_{ij}" />}} serve only to give the IP
solver something to optimize.
The histogram below shows how many episodes contestants typically need to win.
@@ -260,7 +263,7 @@ The histogram below shows how many episodes contestants typically need to win.
@@ -274,14 +277,14 @@ reflected in the plot above.
## Experiment 3: Weak intuition
The conditions of experiment 3 cater best to my algorithm's objectives.
-Here, we fix $$s = -1$$ (a signal-to-noise ratio of 1:2),
+Here, we fix {{< math "s = -1" />}} (a signal-to-noise ratio of 1:2),
meaning that contestants have some intuition, but it's not particularly strong.
@@ -321,7 +324,7 @@ my problem setup, even in the special case of experiment 3 (where the 100%
is merely empirical; if you keep running my script, you'll see losses here and there).
The information-theoretic concepts introduced in Owen's blog post can be applied to arbitrary, not just
uniform, distributions over matchings, including the conditional distribution of
-the ground-truth matching on the vibes $$v_{ij}$$. If you can estimate that
+the ground-truth matching on the vibes {{< math "v_{ij}" />}}. If you can estimate that
distribution (more simulations …), then you
can compute the entropies in Owen's algorithm as weighted averages over
matchings. That algorithm would be, you know, *optimal* optimal. And *very* slow.
diff --git a/_posts/2026-05-06-pageranking-pb.md b/content/posts/2026-05-06-pageranking-pb.md
similarity index 90%
rename from _posts/2026-05-06-pageranking-pb.md
rename to content/posts/2026-05-06-pageranking-pb.md
index 317bddbb..ab5eb6bd 100644
--- a/_posts/2026-05-06-pageranking-pb.md
+++ b/content/posts/2026-05-06-pageranking-pb.md
@@ -1,8 +1,10 @@
----
-layout: post
-title: PageRanking People and Blogs interviews
-katex: true
----
++++
+title = "PageRanking People and Blogs interviews"
+aliases = [ "/2026/05/06/pageranking-pb.html",]
+
+[params]
+id = "https://maxkapur.com/2026/05/06/pageranking-pb"
++++
I recently learned about [PageRank](https://en.wikipedia.org/wiki/PageRank), the
original Google algorithm for ranking webpages. It works by constructing a
@@ -24,11 +26,13 @@ yourself. (But to make it clear I'm not covering anything up, let me acknowledge
that [my interview](https://manuelmoreale.com/interview/max-kapur) is in a
61-way tie for last place, with all the other blogs that had no links to them.)
-A few observations from this exercise below.
+A few observations from this exercise below.
+
+
- I computed the stationary distribution explicitly using `ndarray_linalg::eig`
because that's one of the libraries I was fiddling with, but for a graph with
- a large number of nodes, storing the $$O(n^2)$$ Markov array in memory is
+ a large number of nodes, storing the {{< math "O(n^2)" />}} Markov array in memory is
impractical; you'd be better off just simulating the random jumps instead.
- A Monte Carlo simulation would yield cleaner code than my solution, too. Most
of the bugs I had to fix in my implementation had to do with constructing the
diff --git a/_posts/2026-06-03-metasystems.md b/content/posts/2026-06-03-metasystems.md
similarity index 96%
rename from _posts/2026-06-03-metasystems.md
rename to content/posts/2026-06-03-metasystems.md
index 896c4a8e..2551d63e 100644
--- a/_posts/2026-06-03-metasystems.md
+++ b/content/posts/2026-06-03-metasystems.md
@@ -1,7 +1,10 @@
----
-layout: post
-title: Systems for making systems
----
++++
+title = "Systems for making systems"
+aliases = [ "/2026/06/03/metasystems.html",]
+
+[params]
+id = "https://maxkapur.com/2026/06/03/metasystems"
++++
I read a comment stating that git (a tool programmers use to collaborate on
code, sort of like Word's Track Changes) is not a version control system but a
@@ -9,7 +12,9 @@ code, sort of like Word's Track Changes) is not a version control system but a
That got me thinking about other such "metasystems" I use, and the frustration
that arises when one expects a mere system and instead gets the meta
-thing.
+thing.
+
+
# Anki
diff --git a/content/posts/2026-07-24-hugo-migration.md b/content/posts/2026-07-24-hugo-migration.md
new file mode 100644
index 00000000..eb1ef4a3
--- /dev/null
+++ b/content/posts/2026-07-24-hugo-migration.md
@@ -0,0 +1,24 @@
++++
+title = 'Hugo migration'
+
+[params]
+id = 'tag:max@maxkapur.com,2026-05-27:posts/2026-07-17-hugo-migration'
++++
+
+If you're reading this, I finally updated the backend of the site to use a new
+framework—from Jekyll to Hugo. I also took the opportunity to redo the homepage
+a bit. I don't have as many blog-post-shaped thoughts as I used to, so I reduced
+the post previews to just links to create room for more freeform content.
+
+The [Hugo migration](https://github.com/maxkapur/maxkapur.github.io/pull/98) was
+a bit of work because I took my time and did it the fun way. The old version of
+the site used Jekyll and had accrued a lot of Jekyll's "isms," along with weird
+conventions of my own. I wrote a custom [content migration
+script](https://github.com/maxkapur/maxkapur.github.io/blob/d2ad5ba9b81e473a32c48b44e3975ea22b6e4380/migrate.py)
+to transform Jekyll tags automatically where possible and flag for manual
+attention where needed.
+
+I think/hope it worked. Old post links will redirect you to the new URLs. For
+RSS subscribers, depending on how your reader is configured, you may pick up a
+new copy of every post; you can just mark as read everything before today's
+date.
diff --git a/content/posts/_index.md b/content/posts/_index.md
new file mode 100644
index 00000000..e6ba5a15
--- /dev/null
+++ b/content/posts/_index.md
@@ -0,0 +1,4 @@
++++
+title = "Posts"
+# Suppress default lorem ipsum content from theme
++++
diff --git a/devshell.sh b/devshell.sh
new file mode 100755
index 00000000..f2675b6d
--- /dev/null
+++ b/devshell.sh
@@ -0,0 +1,7 @@
+#!/usr/bin/env sh
+if [ $# -eq 0 ]
+then
+ nix --extra-experimental-features nix-command --extra-experimental-features flakes develop
+else
+ nix --extra-experimental-features nix-command --extra-experimental-features flakes develop --command "$@"
+fi
diff --git a/feed.xml b/feed.xml
deleted file mode 100644
index 086a95f3..00000000
--- a/feed.xml
+++ /dev/null
@@ -1,92 +0,0 @@
----
----
-
-{%- comment -%}
-
- This custom XML file replaces that produced by the built-in jekyll-feed
- plugin. You may still see the jekyll-feed plugin do its thing during site
- build because it is part of the minima theme, but the feed rendered by this
- file is the one that gets included in the end.
-
- References:
-
- - https://gist.github.com/andrewstiefel/57a0a400aa2deb6c9fe18c6da4e16e0f
- - https://www.roboleary.net/webdev/2022/06/14/how-i-fixed-the-atom-web-feed-on-my-website.html
-
-{%- endcomment -%}
-
-{%- comment -%}
- Date format used in the field below
-{%- endcomment -%}
-{%- assign date_format = site.minima.date_format | default: "%b %-d, %Y" -%}
-
-
-
-
-
-
-
-
-
- {{- site.title | strip_html | normalize_whitespace | xml_escape -}}
-
-
-
-
-
- {{- site.description | strip_html | normalize_whitespace | xml_escape -}}
-
-
- {{- site.time | date_to_xmlschema -}}
-
- {{ site.url }}/
-
-
- {{- site.author | strip_html | normalize_whitespace | xml_escape -}}
-
-
- {{- site.email | strip_html | normalize_whitespace | xml_escape -}}
-
-
- Creative Commons CC BY-SA 4.0 license
-
- {% for post in site.posts %}
- {%- if post.hidden -%}
- {%- continue -%}
- {%- endif -%}
-
- {{ post.title | strip_html | normalize_whitespace }}
-
-
-
- {{ post.date | date_to_xmlschema }}
- {{ post.date | date_to_xmlschema }}
-
-
-
- {{- post.date | date: date_format | xml_escape -}}
-
-
- {{ site.url }}{{ site.baseurl }}{{ post.id }}
-
- {{- post.excerpt | strip | normalize_whitespace | xml_escape -}}
-
-
- {{- post.content | strip | normalize_whitespace | xml_escape -}}
-
-
-
- {% endfor %}
-
-
diff --git a/feed.xsl b/feed.xsl
deleted file mode 100644
index 11adc6ff..00000000
--- a/feed.xsl
+++ /dev/null
@@ -1,104 +0,0 @@
----
----
-
-
-
-
-
-
-
-
-
-
-
-
- | RSS
-
- {%- include styling.html %}
-
-
-
- {%- include header.html -%}
-
-
-
-
-
-
-
-
-
- {%- comment -%}
- TODO: Cannot include footer because it uses XML namespaces internally
- that cause the styled XML feed to fail to render.
- {%- include footer.html -%}
- {%- endcomment %}
-
-
-
-
-
-
-
-
-
Web feed preview
-
-
-
This is a web feed, also known as an RSS feed, for the
-
-
-
-
- blog. You can subscribe by copying the URL from the
- address bar into your newsreader app.
-
Visit About
- Feeds to learn more about newsreaders.
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {%- comment -%} Intentional whitespace {%- endcomment %}
-
-
-
-
-
- {%- comment -%}
- Uncomment below to incorporate post excerpts into the styled preview.
- Note that the excerpts are included in the *feed itself* in either
- case, as defined in feed.xml.
-
-
- {%- endcomment %}
-
-
-
-
-
diff --git a/flake.lock b/flake.lock
new file mode 100644
index 00000000..8c2b1507
--- /dev/null
+++ b/flake.lock
@@ -0,0 +1,27 @@
+{
+ "nodes": {
+ "nixpkgs": {
+ "locked": {
+ "lastModified": 1784707089,
+ "narHash": "sha256-2V/6imsUgB7mPZlHY54oeVBRDoZbPKnvzwkAHUSSufk=",
+ "owner": "nixos",
+ "repo": "nixpkgs",
+ "rev": "b3fe9581c9061c749abef42b6d4ee7b7c05c33fa",
+ "type": "github"
+ },
+ "original": {
+ "owner": "nixos",
+ "ref": "nixos-26.05",
+ "repo": "nixpkgs",
+ "type": "github"
+ }
+ },
+ "root": {
+ "inputs": {
+ "nixpkgs": "nixpkgs"
+ }
+ }
+ },
+ "root": "root",
+ "version": 7
+}
diff --git a/flake.nix b/flake.nix
new file mode 100644
index 00000000..3e8f540c
--- /dev/null
+++ b/flake.nix
@@ -0,0 +1,54 @@
+{
+ description = "Dev shell";
+
+ inputs = {
+ nixpkgs.url = "github:nixos/nixpkgs?ref=nixos-26.05";
+ };
+
+ outputs =
+ { self, nixpkgs }:
+ let
+ system = "x86_64-linux";
+ pkgs = import nixpkgs { inherit system; };
+ in
+ {
+ devShells.x86_64-linux.default = pkgs.mkShell {
+ buildInputs = with pkgs; [
+ htmltest
+ hugo
+ nixfmt
+ prettier
+ prettier-plugin-go-template
+ python314
+ python314Packages.mdformat
+ python314Packages.requests
+ python314Packages.ruff
+ python314Packages.saneyaml
+ python314Packages.tinycss2
+ python314Packages.toml
+ shellcheck
+ ];
+ shellHook = ''
+ cat > .prettierrc << EOF
+ {
+ "plugins": ["${pkgs.prettier-plugin-go-template}/lib/node_modules/prettier-plugin-go-template/lib/index.js"],
+ "overrides": [
+ {
+ "files": ["*.html", "*.xml"],
+ "options": {
+ "parser": "go-template"
+ }
+ }
+ ]
+ }
+ EOF
+ cat > pyproject.toml << EOF
+ [project]
+ requires-python = "== 3.14"
+ EOF
+ ./themes/illusion-slopes/download_assets.py >/dev/null
+ echo "Dev shell initialized from flake.nix"
+ '';
+ };
+ };
+}
diff --git a/format.sh b/format.sh
new file mode 100755
index 00000000..3d6b472b
--- /dev/null
+++ b/format.sh
@@ -0,0 +1,11 @@
+#!/usr/bin/env bash
+
+set -e
+shopt -s globstar
+
+mdformat ./**/README.md --number --wrap 80
+nixfmt flake.nix
+prettier . --write
+ruff format
+ruff check --fix
+shellcheck ./*.sh
diff --git a/hugo.toml b/hugo.toml
new file mode 100644
index 00000000..2e261b51
--- /dev/null
+++ b/hugo.toml
@@ -0,0 +1,48 @@
+# Site metadata
+baseURL = "https://maxkapur.com"
+locale = "en-us"
+title = "Illusion Slopes"
+theme = "illusion-slopes"
+
+# Summary is shown instead of full content on homepage and used in feeds. Hugo
+# generates an automatic summary from first n words if a manual summary isn't
+# defined using a slice. But if I don't supply the slice manually,
+# it's because I want the whole post to show on the homepage (i.e. it is its own
+# summary). Hence
+summaryLength = 99999
+
+# Custom
+params.subtitle = "Max’s personal homepage"
+params.description = "I blog about culture, numerical optimization, and software. I work at a consulting firm as an operations research and numerical modeling specialist. All opinions are my own."
+params.author.name = "Max Kapur"
+params.author.email = "max@maxkapur.com"
+
+# Build settings
+cleanDestinationDir = true
+
+[outputs]
+home = ["html", "atom"]
+section = ["html", "atom"]
+taxonomy = ["html", "atom"]
+term = ["html", "atom"]
+
+[frontmatter]
+# Extract dates from filenames if possible
+date = ["date", ":filename", ":default"]
+lastmod = ["lastmod", "date", ":filename", ":default"]
+publishDate = ["publishdate", "date", ":filename", ":default"]
+
+[permalinks]
+# For posts (only), include the date in the URL to disambiguate recurring posts
+# with the same title (like "recent reading")
+posts = "/posts/:year-:month-:day-:slug"
+
+# Shadow the [menus] key from the theme's hugo.toml to suppress "Tags" link, as
+# I don't use tags
+[[menus.main]]
+name = 'Posts'
+pageRef = '/posts'
+weight = 999
+
+[markup.goldmark]
+renderer.unsafe = true
diff --git a/index.html b/index.html
deleted file mode 100644
index 07980f48..00000000
--- a/index.html
+++ /dev/null
@@ -1,63 +0,0 @@
----
-layout: default
-katex: true
-pagination:
- enabled: true
- per_page: 5
- sort_field: date
- sort_reverse: true
- title: ':title (page :num)'
- permalink: '/page/:num/'
----
-
-
- {%- if site.posts.size > 0 -%}
- {%- if page.list_title -%}
-
{{ page.list_title }}
- {%- endif %}
-
- {%- for post in paginator.posts -%}
- {%- if post.hidden -%}
- {%- continue -%}
- {%- endif -%}
-
- {%- if paginator.next_page -%}
- Next page
- {%- endif -%}
-
-
- {%- endif %}
- {%- endif %}
-
diff --git a/layouts/_partials/copyright.html b/layouts/_partials/copyright.html
new file mode 100644
index 00000000..6bec1e01
--- /dev/null
+++ b/layouts/_partials/copyright.html
@@ -0,0 +1,6 @@
+
+ {{ site.Title }}. 2016–2026 Max Kapur. Licenses: CC BY-SA 4.0
+ (content) and Apache (theme). See
+ GitHub for source
+ code and license text.
+
diff --git a/lint.sh b/lint.sh
new file mode 100755
index 00000000..d5006b05
--- /dev/null
+++ b/lint.sh
@@ -0,0 +1,18 @@
+#!/usr/bin/env bash
+
+set -ex
+shopt -s globstar
+
+# These checks have mutating counterparts in format.sh
+mdformat ./**/README.md --number --wrap 80 --check
+nixfmt flake.nix --check
+prettier . --check
+ruff format --check
+ruff check
+shellcheck ./*.sh
+
+# These checks don't
+TEMP_DIR="$(mktemp --directory)"
+hugo build --destination "$TEMP_DIR"
+htmltest --conf .htmltest.yml --skip-external "$TEMP_DIR"
+./check_reserved_paths.py --build-dir "$TEMP_DIR"
diff --git a/migrate.py b/migrate.py
new file mode 100755
index 00000000..56695810
--- /dev/null
+++ b/migrate.py
@@ -0,0 +1,321 @@
+#!/usr/bin/env python
+"""Jekyll migration script.
+
+This script tries to automate the migration of posts from the Jekyll version
+of the site, flagging areas that need manual attention manually.
+
+Workflow is to manually copy all the top level pages (`about.md` etc.) from
+Jekyll into `content/pages/`, and all the posts (`_posts/*.md`) into
+`content/posts/`, commit, then run this script and examine the diff.
+"""
+
+import re
+import warnings
+from datetime import date
+from pathlib import Path
+from urllib.parse import quote
+
+import saneyaml
+import toml
+import tomllib
+
+# Eager indicators that a page contains unmigrated Jekyll syntax. Exclude "{{%"
+# and "{{<", which are Hugo markdown and standard shortcodes, respectively:
+# https://gohugo.io/content-management/shortcodes/
+jekyll_object_start = re.compile(r"{{[^%<]", re.MULTILINE)
+jekyll_tag_start = re.compile(r"[^{]{%", re.MULTILINE)
+
+isodate = re.compile(r"\d{4}\-\d{2}\-\d{2}")
+maybe_inline_summary_separator = re.compile(r"<\!--\s*more\s*-->", re.MULTILINE)
+
+
+def process(path: Path) -> tuple[bool, bool]:
+ """Migrate a page or post.
+
+ Return whether anything changed and whether manual attention needed.
+ """
+ attention_needed = False
+ if path.parts[1] == "pages":
+ page_type = "page"
+ elif path.parts[1] == "posts":
+ page_type = "post"
+ else:
+ raise ValueError
+
+ if path.name == "_index.md":
+ return False, attention_needed
+
+ original_text = path.read_text()
+
+ d, body = extract_parts(original_text)
+ migrate_frontmatter_keys(path, page_type, d)
+
+ frontmatter = toml.dumps(d)
+ body = body.strip()
+
+ if "$$" in body:
+ body = migrate_katex(body)
+ assert "$$" not in body
+
+ if isodate.match(path.name) and "date" in d:
+ warnings.warn(
+ f"{path} has date in both filename and frontmatter; see https://github.com/gohugoio/hugo/issues/14971"
+ )
+ attention_needed = True
+
+ if (
+ (("post_url" in body) or ("relative_url" in body))
+ # False positive: meta post with relative_url in a code block/example
+ and (path.name != "2024-12-26-server-side-katex.md")
+ ):
+ warnings.warn(f"{path} contains unmigrated Jekyll URL references")
+ body = migrate_hrefs(body)
+ assert ("post_url" not in body) and ("relative_url" not in body)
+
+ if (
+ (jekyll_object_start.search(body) or jekyll_tag_start.search(body))
+ # False positive: meta post with Jekyll syntax examples
+ and path.name != "2024-12-26-server-side-katex.md"
+ ):
+ warnings.warn(f"{path} contains unmigrated Jekyll syntax")
+ body = migrate_jekyll_syntax(body)
+ attention_needed = True
+
+ if m := maybe_inline_summary_separator.search(body):
+ left, right = m.span()
+ # Jekyll lets you put the separator inline, but Hugo docs say it must be
+ # on own line. (Hugo itself seems to process inline separators just fine
+ # but we should conform to what's officially supported.) These require
+ # manual attention because I often used the separator to avoid footnotes
+ # on the homepage.
+ check = body[left - 2 : right + 2]
+ if check != "\n\n\n\n":
+ warnings.warn(f"{path} contains inline separator: {check!r}")
+ attention_needed = True
+
+ output = f"+++\n{frontmatter}+++\n\n{body}\n"
+ if output != original_text:
+ path.write_text(output)
+ return True, attention_needed
+ return False, attention_needed
+
+
+def extract_parts(original_text: str) -> tuple[dict, str]:
+ """Extract the dictionary of frontmatter data and the body."""
+
+ if original_text.strip().startswith("---"):
+ _, frontmatter, body = original_text.split("---", maxsplit=2)
+ d = saneyaml.load(frontmatter)
+ elif original_text.strip().startswith("+++"):
+ _, frontmatter, body = original_text.split("+++", maxsplit=2)
+ d = tomllib.loads(frontmatter)
+ else:
+ body = original_text
+ d = {}
+
+ return d, body
+
+
+def migrate_frontmatter_keys(path: Path, page_type: str, d: dict):
+ """Migrate the frontmatter keys dictionary `d`, in place."""
+ if "params" not in d:
+ # Every post should have a params key to provide the id field
+ d["params"] = {}
+
+ date_str = None
+
+ # Construct aliases to maintain paths from Jekyll site
+ if page_type == "page":
+ # Every page in the Jekyll site had a permalink so there's no default
+ # path to alias
+ default_aliases = []
+ # https://www.taguri.org/, note same form used in archetype
+ id = f"tag:max@maxkapur.com,2026-05-27:pages/{quote(d['title'])}"
+ elif page_type == "post":
+ date_str = path.name[:10]
+ slug = path.name[11:-3] # strip date and ".md"
+ assert date.fromisoformat(date_str) # check format
+ old_relpath = f"/{date_str.replace('-', '/')}/{slug}.html"
+ default_aliases = [old_relpath]
+ # This is the exact ID used in the old Atom feed: Absolute URL minus the
+ # trailing .html. Bad, but we have to keep it
+ id = f"https://maxkapur.com{old_relpath[:-5]}"
+ else:
+ raise ValueError
+
+ d["aliases"] = d.get("aliases", default_aliases)
+ if permalink := d.get("permalink"):
+ # Jekyll permalink
+ d["aliases"].append(permalink)
+ del d["permalink"]
+ if redirects := d.get("redirect_from"):
+ d["aliases"].extend(redirects)
+ del d["redirect_from"]
+
+ d["params"]["id"] = id
+
+ if "layout" in d:
+ # Tech debt in Jekyll where I manually had to say every post was a post
+ del d["layout"]
+
+ # Handle custom keys
+
+ # Used to have a KaTeX indicator to indicate whether the page has math on
+ # it. Idea was that you could get faster page loads by skipping the KaTeX
+ # CSS on pages that don't need it. But the *homepage* needs it, so there is
+ # a good chance it's cached. Moreover, the key wasn't consistently, so just
+ # drop it.
+ if "katex" in d:
+ del d["katex"]
+ if "katex" in d.get("params", {}):
+ del d["params"]["katex"]
+
+ # Jekyll side used hidden key in a sort of overloaded way. For *posts,*
+ # hidden suppressed inclusion on the homepage (but the post would still land
+ # in Browse). Haven't decide how to implement this but for now, just move it
+ # into params.
+ if page_type == "post" and "hidden" in d:
+ # Sanity check assumption that I only included this key if it was true
+ assert d["hidden"]
+ d["params"] = d.get("params", {}) | {"hidden": True}
+ del d["hidden"]
+ # For pages, hidden would suppress them from the top navigation. Hugo works
+ # a little differently here: Inclusion is the marked case, not exclusion.
+ # New site.Menus.main is autopopulated with a link to browse posts (and a
+ # few others, see hugo.toml in the theme), then you add "main" to the menu
+ # list for any content item that should be added to this menu.
+ if page_type == "page" and (
+ "menus" not in d # Ensures idempotency
+ ):
+ was_hidden = False
+ if "hidden" in d:
+ assert d["hidden"]
+ was_hidden = True
+ del d["hidden"]
+ if d.get("params", {}).get("hidden"):
+ # Previous migration moved hidden key into params, before I understood hugo menus
+ was_hidden = True
+ del d["params"]["hidden"]
+
+ if was_hidden:
+ d["menus"] = []
+ else:
+ d["menus"] = ["main"]
+
+ if sort_order := d.get("sort_order"):
+ d["weight"] = int(sort_order)
+ del d["sort_order"]
+
+
+display_math = re.compile(
+ r"(\r?\n)+\r?\n\$\$(?P(?:(?!\r?\n\r?\n).)*?)\$\$(\r?\n)+\r?\n",
+ re.MULTILINE | re.DOTALL,
+)
+inline_math = re.compile(
+ r"\$\$(?P(?:(?!\r?\n\r?\n).)*?)\$\$",
+ re.MULTILINE | re.DOTALL, # Even inline needs DOTALL due to line wrapping
+)
+
+
+def display_shortcode(expr: str) -> str:
+ return "\n\n{{< math >}}\n" + expr.strip() + "\n{{< /math >}}\n\n"
+
+
+def inline_shortcode(expr: str) -> str:
+ # For inline math (only), collapse whitespace since Hugo shortcodes
+ # don't support multiline strings
+ return '{{< math "' + re.subn(r"\n+", " ", expr.strip())[0] + '" />}}'
+
+
+def migrate_katex(body: str) -> str:
+ """Replace old KaTeX delimiters with new shortcode."""
+
+ after = body
+ while True:
+ # Make replacements one at a time to prevent overlap since delims are
+ # symmetric
+ before = after
+
+ # Eagerly match display math since its pattern is a superset of inline
+ after = display_math.sub(lambda m: display_shortcode(m.group("expr")), before)
+ if after != before:
+ continue
+
+ after = inline_math.sub(lambda m: inline_shortcode(m.group("expr")), before)
+ if after != before:
+ continue
+
+ return after
+
+
+# example:
+#
+# Things that are a thing
+# here
+jekyll_href = re.compile(
+ r'[0-9a-z\-]+?)\s+\-?%\}">(?P.*?)',
+ re.MULTILINE | re.DOTALL,
+)
+
+# example:
+#
+# [More sophisticated matching algorithms]({%- post_url 2021-03-07-stable-matching-planet-money -%})
+jekyll_mdref = re.compile(
+ # r"\[\s+(?P.*?)\s+\]\(\s+\{%\-?\s+post_url\s+(?P[0-9a-z\-]+?)\s+\-?%\}\s+\)",
+ r"\[\s*(?P.*?)\s*\]\(\s*\{%\-?\s+post_url\s+(?P[0-9a-z\-]+?)\s+\-?%\}\s*\)",
+ re.MULTILINE | re.DOTALL,
+)
+
+
+def hugo_post_href(slug: str, disp: str) -> str:
+ # All post_urls point to posts as opposed to pages
+ return f"[{disp.strip()}](/posts/{slug.strip()}/)"
+
+
+def migrate_hrefs(body: str) -> str:
+ body, _ = jekyll_href.subn(
+ lambda m: hugo_post_href(m.group("slug"), m.group("disp")), body
+ )
+ body, _ = jekyll_mdref.subn(
+ lambda m: hugo_post_href(m.group("slug"), m.group("disp")), body
+ )
+ return body
+
+
+jekyll_site_url = re.compile(r"\{\{\s*?site\.url\s*?\}\}", re.MULTILINE)
+jekyll_site_email = re.compile(r"\{\{\s*?site\.email\s*?\}\}", re.MULTILINE)
+jekyll_site_github_username = re.compile(
+ r"\{\{\s*?site\.github_username\s*?\}\}", re.MULTILINE
+)
+jekyll_site_linkedin_username = re.compile(
+ r"\{\{\s*?site\.linkedin_username\s*?\}\}", re.MULTILINE
+)
+
+
+def migrate_jekyll_syntax(body: str) -> str:
+ # Hardcode these for now. TODO: Develop shortcodes
+ body, _ = jekyll_site_url.subn(
+ "https://maxkapur.com", # Lack of trailing slash intentional
+ body,
+ )
+ body, _ = jekyll_site_email.subn("max@maxkapur.com", body)
+ body, _ = jekyll_site_github_username.subn("maxkapur", body)
+ body, _ = jekyll_site_linkedin_username.subn("maxkapur", body)
+
+ return body
+
+
+if __name__ == "__main__":
+ change_detected = False
+ attention_needed = False
+ pages = list(Path("./content/pages/").glob("**.md")) + list(
+ Path("./content/posts/").glob("**.md")
+ )
+ for page in pages:
+ change_detected_page, attention_needed_page = process(page)
+ change_detected |= change_detected_page
+ attention_needed |= attention_needed_page
+
+ if change_detected or attention_needed:
+ warnings.warn("Migrations needed; see diff")
+ exit(1)
diff --git a/static/LICENSE.txt b/static/LICENSE.txt
new file mode 100644
index 00000000..795087c2
--- /dev/null
+++ b/static/LICENSE.txt
@@ -0,0 +1,427 @@
+Attribution-ShareAlike 4.0 International
+
+=======================================================================
+
+Creative Commons Corporation ("Creative Commons") is not a law firm and
+does not provide legal services or legal advice. Distribution of
+Creative Commons public licenses does not create a lawyer-client or
+other relationship. Creative Commons makes its licenses and related
+information available on an "as-is" basis. Creative Commons gives no
+warranties regarding its licenses, any material licensed under their
+terms and conditions, or any related information. Creative Commons
+disclaims all liability for damages resulting from their use to the
+fullest extent possible.
+
+Using Creative Commons Public Licenses
+
+Creative Commons public licenses provide a standard set of terms and
+conditions that creators and other rights holders may use to share
+original works of authorship and other material subject to copyright
+and certain other rights specified in the public license below. The
+following considerations are for informational purposes only, are not
+exhaustive, and do not form part of our licenses.
+
+ Considerations for licensors: Our public licenses are
+ intended for use by those authorized to give the public
+ permission to use material in ways otherwise restricted by
+ copyright and certain other rights. Our licenses are
+ irrevocable. Licensors should read and understand the terms
+ and conditions of the license they choose before applying it.
+ Licensors should also secure all rights necessary before
+ applying our licenses so that the public can reuse the
+ material as expected. Licensors should clearly mark any
+ material not subject to the license. This includes other CC-
+ licensed material, or material used under an exception or
+ limitation to copyright. More considerations for licensors:
+ wiki.creativecommons.org/Considerations_for_licensors
+
+ Considerations for the public: By using one of our public
+ licenses, a licensor grants the public permission to use the
+ licensed material under specified terms and conditions. If
+ the licensor's permission is not necessary for any reason--for
+ example, because of any applicable exception or limitation to
+ copyright--then that use is not regulated by the license. Our
+ licenses grant only permissions under copyright and certain
+ other rights that a licensor has authority to grant. Use of
+ the licensed material may still be restricted for other
+ reasons, including because others have copyright or other
+ rights in the material. A licensor may make special requests,
+ such as asking that all changes be marked or described.
+ Although not required by our licenses, you are encouraged to
+ respect those requests where reasonable. More considerations
+ for the public:
+ wiki.creativecommons.org/Considerations_for_licensees
+
+=======================================================================
+
+Creative Commons Attribution-ShareAlike 4.0 International Public
+License
+
+By exercising the Licensed Rights (defined below), You accept and agree
+to be bound by the terms and conditions of this Creative Commons
+Attribution-ShareAlike 4.0 International Public License ("Public
+License"). To the extent this Public License may be interpreted as a
+contract, You are granted the Licensed Rights in consideration of Your
+acceptance of these terms and conditions, and the Licensor grants You
+such rights in consideration of benefits the Licensor receives from
+making the Licensed Material available under these terms and
+conditions.
+
+
+Section 1 -- Definitions.
+
+ a. Adapted Material means material subject to Copyright and Similar
+ Rights that is derived from or based upon the Licensed Material
+ and in which the Licensed Material is translated, altered,
+ arranged, transformed, or otherwise modified in a manner requiring
+ permission under the Copyright and Similar Rights held by the
+ Licensor. For purposes of this Public License, where the Licensed
+ Material is a musical work, performance, or sound recording,
+ Adapted Material is always produced where the Licensed Material is
+ synched in timed relation with a moving image.
+
+ b. Adapter's License means the license You apply to Your Copyright
+ and Similar Rights in Your contributions to Adapted Material in
+ accordance with the terms and conditions of this Public License.
+
+ c. BY-SA Compatible License means a license listed at
+ creativecommons.org/compatiblelicenses, approved by Creative
+ Commons as essentially the equivalent of this Public License.
+
+ d. Copyright and Similar Rights means copyright and/or similar rights
+ closely related to copyright including, without limitation,
+ performance, broadcast, sound recording, and Sui Generis Database
+ Rights, without regard to how the rights are labeled or
+ categorized. For purposes of this Public License, the rights
+ specified in Section 2(b)(1)-(2) are not Copyright and Similar
+ Rights.
+
+ e. Effective Technological Measures means those measures that, in the
+ absence of proper authority, may not be circumvented under laws
+ fulfilling obligations under Article 11 of the WIPO Copyright
+ Treaty adopted on December 20, 1996, and/or similar international
+ agreements.
+
+ f. Exceptions and Limitations means fair use, fair dealing, and/or
+ any other exception or limitation to Copyright and Similar Rights
+ that applies to Your use of the Licensed Material.
+
+ g. License Elements means the license attributes listed in the name
+ of a Creative Commons Public License. The License Elements of this
+ Public License are Attribution and ShareAlike.
+
+ h. Licensed Material means the artistic or literary work, database,
+ or other material to which the Licensor applied this Public
+ License.
+
+ i. Licensed Rights means the rights granted to You subject to the
+ terms and conditions of this Public License, which are limited to
+ all Copyright and Similar Rights that apply to Your use of the
+ Licensed Material and that the Licensor has authority to license.
+
+ j. Licensor means the individual(s) or entity(ies) granting rights
+ under this Public License.
+
+ k. Share means to provide material to the public by any means or
+ process that requires permission under the Licensed Rights, such
+ as reproduction, public display, public performance, distribution,
+ dissemination, communication, or importation, and to make material
+ available to the public including in ways that members of the
+ public may access the material from a place and at a time
+ individually chosen by them.
+
+ l. Sui Generis Database Rights means rights other than copyright
+ resulting from Directive 96/9/EC of the European Parliament and of
+ the Council of 11 March 1996 on the legal protection of databases,
+ as amended and/or succeeded, as well as other essentially
+ equivalent rights anywhere in the world.
+
+ m. You means the individual or entity exercising the Licensed Rights
+ under this Public License. Your has a corresponding meaning.
+
+
+Section 2 -- Scope.
+
+ a. License grant.
+
+ 1. Subject to the terms and conditions of this Public License,
+ the Licensor hereby grants You a worldwide, royalty-free,
+ non-sublicensable, non-exclusive, irrevocable license to
+ exercise the Licensed Rights in the Licensed Material to:
+
+ a. reproduce and Share the Licensed Material, in whole or
+ in part; and
+
+ b. produce, reproduce, and Share Adapted Material.
+
+ 2. Exceptions and Limitations. For the avoidance of doubt, where
+ Exceptions and Limitations apply to Your use, this Public
+ License does not apply, and You do not need to comply with
+ its terms and conditions.
+
+ 3. Term. The term of this Public License is specified in Section
+ 6(a).
+
+ 4. Media and formats; technical modifications allowed. The
+ Licensor authorizes You to exercise the Licensed Rights in
+ all media and formats whether now known or hereafter created,
+ and to make technical modifications necessary to do so. The
+ Licensor waives and/or agrees not to assert any right or
+ authority to forbid You from making technical modifications
+ necessary to exercise the Licensed Rights, including
+ technical modifications necessary to circumvent Effective
+ Technological Measures. For purposes of this Public License,
+ simply making modifications authorized by this Section 2(a)
+ (4) never produces Adapted Material.
+
+ 5. Downstream recipients.
+
+ a. Offer from the Licensor -- Licensed Material. Every
+ recipient of the Licensed Material automatically
+ receives an offer from the Licensor to exercise the
+ Licensed Rights under the terms and conditions of this
+ Public License.
+
+ b. Additional offer from the Licensor -- Adapted Material.
+ Every recipient of Adapted Material from You
+ automatically receives an offer from the Licensor to
+ exercise the Licensed Rights in the Adapted Material
+ under the conditions of the Adapter's License You apply.
+
+ c. No downstream restrictions. You may not offer or impose
+ any additional or different terms or conditions on, or
+ apply any Effective Technological Measures to, the
+ Licensed Material if doing so restricts exercise of the
+ Licensed Rights by any recipient of the Licensed
+ Material.
+
+ 6. No endorsement. Nothing in this Public License constitutes or
+ may be construed as permission to assert or imply that You
+ are, or that Your use of the Licensed Material is, connected
+ with, or sponsored, endorsed, or granted official status by,
+ the Licensor or others designated to receive attribution as
+ provided in Section 3(a)(1)(A)(i).
+
+ b. Other rights.
+
+ 1. Moral rights, such as the right of integrity, are not
+ licensed under this Public License, nor are publicity,
+ privacy, and/or other similar personality rights; however, to
+ the extent possible, the Licensor waives and/or agrees not to
+ assert any such rights held by the Licensor to the limited
+ extent necessary to allow You to exercise the Licensed
+ Rights, but not otherwise.
+
+ 2. Patent and trademark rights are not licensed under this
+ Public License.
+
+ 3. To the extent possible, the Licensor waives any right to
+ collect royalties from You for the exercise of the Licensed
+ Rights, whether directly or through a collecting society
+ under any voluntary or waivable statutory or compulsory
+ licensing scheme. In all other cases the Licensor expressly
+ reserves any right to collect such royalties.
+
+
+Section 3 -- License Conditions.
+
+Your exercise of the Licensed Rights is expressly made subject to the
+following conditions.
+
+ a. Attribution.
+
+ 1. If You Share the Licensed Material (including in modified
+ form), You must:
+
+ a. retain the following if it is supplied by the Licensor
+ with the Licensed Material:
+
+ i. identification of the creator(s) of the Licensed
+ Material and any others designated to receive
+ attribution, in any reasonable manner requested by
+ the Licensor (including by pseudonym if
+ designated);
+
+ ii. a copyright notice;
+
+ iii. a notice that refers to this Public License;
+
+ iv. a notice that refers to the disclaimer of
+ warranties;
+
+ v. a URI or hyperlink to the Licensed Material to the
+ extent reasonably practicable;
+
+ b. indicate if You modified the Licensed Material and
+ retain an indication of any previous modifications; and
+
+ c. indicate the Licensed Material is licensed under this
+ Public License, and include the text of, or the URI or
+ hyperlink to, this Public License.
+
+ 2. You may satisfy the conditions in Section 3(a)(1) in any
+ reasonable manner based on the medium, means, and context in
+ which You Share the Licensed Material. For example, it may be
+ reasonable to satisfy the conditions by providing a URI or
+ hyperlink to a resource that includes the required
+ information.
+
+ 3. If requested by the Licensor, You must remove any of the
+ information required by Section 3(a)(1)(A) to the extent
+ reasonably practicable.
+
+ b. ShareAlike.
+
+ In addition to the conditions in Section 3(a), if You Share
+ Adapted Material You produce, the following conditions also apply.
+
+ 1. The Adapter's License You apply must be a Creative Commons
+ license with the same License Elements, this version or
+ later, or a BY-SA Compatible License.
+
+ 2. You must include the text of, or the URI or hyperlink to, the
+ Adapter's License You apply. You may satisfy this condition
+ in any reasonable manner based on the medium, means, and
+ context in which You Share Adapted Material.
+
+ 3. You may not offer or impose any additional or different terms
+ or conditions on, or apply any Effective Technological
+ Measures to, Adapted Material that restrict exercise of the
+ rights granted under the Adapter's License You apply.
+
+
+Section 4 -- Sui Generis Database Rights.
+
+Where the Licensed Rights include Sui Generis Database Rights that
+apply to Your use of the Licensed Material:
+
+ a. for the avoidance of doubt, Section 2(a)(1) grants You the right
+ to extract, reuse, reproduce, and Share all or a substantial
+ portion of the contents of the database;
+
+ b. if You include all or a substantial portion of the database
+ contents in a database in which You have Sui Generis Database
+ Rights, then the database in which You have Sui Generis Database
+ Rights (but not its individual contents) is Adapted Material,
+ including for purposes of Section 3(b); and
+
+ c. You must comply with the conditions in Section 3(a) if You Share
+ all or a substantial portion of the contents of the database.
+
+For the avoidance of doubt, this Section 4 supplements and does not
+replace Your obligations under this Public License where the Licensed
+Rights include other Copyright and Similar Rights.
+
+
+Section 5 -- Disclaimer of Warranties and Limitation of Liability.
+
+ a. UNLESS OTHERWISE SEPARATELY UNDERTAKEN BY THE LICENSOR, TO THE
+ EXTENT POSSIBLE, THE LICENSOR OFFERS THE LICENSED MATERIAL AS-IS
+ AND AS-AVAILABLE, AND MAKES NO REPRESENTATIONS OR WARRANTIES OF
+ ANY KIND CONCERNING THE LICENSED MATERIAL, WHETHER EXPRESS,
+ IMPLIED, STATUTORY, OR OTHER. THIS INCLUDES, WITHOUT LIMITATION,
+ WARRANTIES OF TITLE, MERCHANTABILITY, FITNESS FOR A PARTICULAR
+ PURPOSE, NON-INFRINGEMENT, ABSENCE OF LATENT OR OTHER DEFECTS,
+ ACCURACY, OR THE PRESENCE OR ABSENCE OF ERRORS, WHETHER OR NOT
+ KNOWN OR DISCOVERABLE. WHERE DISCLAIMERS OF WARRANTIES ARE NOT
+ ALLOWED IN FULL OR IN PART, THIS DISCLAIMER MAY NOT APPLY TO YOU.
+
+ b. TO THE EXTENT POSSIBLE, IN NO EVENT WILL THE LICENSOR BE LIABLE
+ TO YOU ON ANY LEGAL THEORY (INCLUDING, WITHOUT LIMITATION,
+ NEGLIGENCE) OR OTHERWISE FOR ANY DIRECT, SPECIAL, INDIRECT,
+ INCIDENTAL, CONSEQUENTIAL, PUNITIVE, EXEMPLARY, OR OTHER LOSSES,
+ COSTS, EXPENSES, OR DAMAGES ARISING OUT OF THIS PUBLIC LICENSE OR
+ USE OF THE LICENSED MATERIAL, EVEN IF THE LICENSOR HAS BEEN
+ ADVISED OF THE POSSIBILITY OF SUCH LOSSES, COSTS, EXPENSES, OR
+ DAMAGES. WHERE A LIMITATION OF LIABILITY IS NOT ALLOWED IN FULL OR
+ IN PART, THIS LIMITATION MAY NOT APPLY TO YOU.
+
+ c. The disclaimer of warranties and limitation of liability provided
+ above shall be interpreted in a manner that, to the extent
+ possible, most closely approximates an absolute disclaimer and
+ waiver of all liability.
+
+
+Section 6 -- Term and Termination.
+
+ a. This Public License applies for the term of the Copyright and
+ Similar Rights licensed here. However, if You fail to comply with
+ this Public License, then Your rights under this Public License
+ terminate automatically.
+
+ b. Where Your right to use the Licensed Material has terminated under
+ Section 6(a), it reinstates:
+
+ 1. automatically as of the date the violation is cured, provided
+ it is cured within 30 days of Your discovery of the
+ violation; or
+
+ 2. upon express reinstatement by the Licensor.
+
+ For the avoidance of doubt, this Section 6(b) does not affect any
+ right the Licensor may have to seek remedies for Your violations
+ of this Public License.
+
+ c. For the avoidance of doubt, the Licensor may also offer the
+ Licensed Material under separate terms or conditions or stop
+ distributing the Licensed Material at any time; however, doing so
+ will not terminate this Public License.
+
+ d. Sections 1, 5, 6, 7, and 8 survive termination of this Public
+ License.
+
+
+Section 7 -- Other Terms and Conditions.
+
+ a. The Licensor shall not be bound by any additional or different
+ terms or conditions communicated by You unless expressly agreed.
+
+ b. Any arrangements, understandings, or agreements regarding the
+ Licensed Material not stated herein are separate from and
+ independent of the terms and conditions of this Public License.
+
+
+Section 8 -- Interpretation.
+
+ a. For the avoidance of doubt, this Public License does not, and
+ shall not be interpreted to, reduce, limit, restrict, or impose
+ conditions on any use of the Licensed Material that could lawfully
+ be made without permission under this Public License.
+
+ b. To the extent possible, if any provision of this Public License is
+ deemed unenforceable, it shall be automatically reformed to the
+ minimum extent necessary to make it enforceable. If the provision
+ cannot be reformed, it shall be severed from this Public License
+ without affecting the enforceability of the remaining terms and
+ conditions.
+
+ c. No term or condition of this Public License will be waived and no
+ failure to comply consented to unless expressly agreed to by the
+ Licensor.
+
+ d. Nothing in this Public License constitutes or may be interpreted
+ as a limitation upon, or waiver of, any privileges and immunities
+ that apply to the Licensor or You, including from the legal
+ processes of any jurisdiction or authority.
+
+
+=======================================================================
+
+Creative Commons is not a party to its public licenses.
+Notwithstanding, Creative Commons may elect to apply one of its public
+licenses to material it publishes and in those instances will be
+considered the “Licensor.” The text of the Creative Commons public
+licenses is dedicated to the public domain under the CC0 Public Domain
+Dedication. Except for the limited purpose of indicating that material
+is shared under a Creative Commons public license or as otherwise
+permitted by the Creative Commons policies published at
+creativecommons.org/policies, Creative Commons does not authorize the
+use of the trademark "Creative Commons" or any other trademark or logo
+of Creative Commons without its prior written consent including,
+without limitation, in connection with any unauthorized modifications
+to any of its public licenses or any other arrangements,
+understandings, or agreements concerning use of licensed material. For
+the avoidance of doubt, this paragraph does not form part of the public
+licenses.
+
+Creative Commons may be contacted at creativecommons.org.
diff --git a/static/assets/README.md b/static/assets/README.md
new file mode 100644
index 00000000..2530e5d1
--- /dev/null
+++ b/static/assets/README.md
@@ -0,0 +1,9 @@
+# static/assets/
+
+This replicates the `assets/` directory from the Jekyll version of the site in
+order to support image tags from old posts as-is.
+
+TODO: A future migration might move these images next to the posts that
+reference them. `2025-12-13-post.md` and `2025-12-13-post/` with `index.md` and
+images inside should result in equivalent paths in Hugo, but I haven't tested
+this yet.
diff --git a/assets/images/app-count-utility.svg b/static/assets/images/app-count-utility.svg
similarity index 100%
rename from assets/images/app-count-utility.svg
rename to static/assets/images/app-count-utility.svg
diff --git a/assets/images/are-you-the-one/no-intuition.svg b/static/assets/images/are-you-the-one/no-intuition.svg
similarity index 100%
rename from assets/images/are-you-the-one/no-intuition.svg
rename to static/assets/images/are-you-the-one/no-intuition.svg
diff --git a/assets/images/are-you-the-one/strength-vs-nturns.svg b/static/assets/images/are-you-the-one/strength-vs-nturns.svg
similarity index 100%
rename from assets/images/are-you-the-one/strength-vs-nturns.svg
rename to static/assets/images/are-you-the-one/strength-vs-nturns.svg
diff --git a/assets/images/are-you-the-one/weak-intuition.svg b/static/assets/images/are-you-the-one/weak-intuition.svg
similarity index 100%
rename from assets/images/are-you-the-one/weak-intuition.svg
rename to static/assets/images/are-you-the-one/weak-intuition.svg
diff --git a/assets/images/cyclical-data-before.svg b/static/assets/images/cyclical-data-before.svg
similarity index 100%
rename from assets/images/cyclical-data-before.svg
rename to static/assets/images/cyclical-data-before.svg
diff --git a/assets/images/cyclical-data-with-fit.svg b/static/assets/images/cyclical-data-with-fit.svg
similarity index 100%
rename from assets/images/cyclical-data-with-fit.svg
rename to static/assets/images/cyclical-data-with-fit.svg
diff --git a/assets/images/fish-fortune-greeter.png b/static/assets/images/fish-fortune-greeter.png
similarity index 100%
rename from assets/images/fish-fortune-greeter.png
rename to static/assets/images/fish-fortune-greeter.png
diff --git a/assets/images/fish-fzl-example.png b/static/assets/images/fish-fzl-example.png
similarity index 100%
rename from assets/images/fish-fzl-example.png
rename to static/assets/images/fish-fzl-example.png
diff --git a/assets/images/gsms-farewell.jpg b/static/assets/images/gsms-farewell.jpg
similarity index 100%
rename from assets/images/gsms-farewell.jpg
rename to static/assets/images/gsms-farewell.jpg
diff --git a/assets/images/houseplants-1-length-width.png b/static/assets/images/houseplants-1-length-width.png
similarity index 100%
rename from assets/images/houseplants-1-length-width.png
rename to static/assets/images/houseplants-1-length-width.png
diff --git a/assets/images/houseplants-2-area-qq.png b/static/assets/images/houseplants-2-area-qq.png
similarity index 100%
rename from assets/images/houseplants-2-area-qq.png
rename to static/assets/images/houseplants-2-area-qq.png
diff --git a/assets/images/houseplants-3-ratio-qq.png b/static/assets/images/houseplants-3-ratio-qq.png
similarity index 100%
rename from assets/images/houseplants-3-ratio-qq.png
rename to static/assets/images/houseplants-3-ratio-qq.png
diff --git a/assets/images/jeju-conference.jpg b/static/assets/images/jeju-conference.jpg
similarity index 100%
rename from assets/images/jeju-conference.jpg
rename to static/assets/images/jeju-conference.jpg
diff --git a/assets/images/logo-new.svg b/static/assets/images/logo-new.svg
similarity index 100%
rename from assets/images/logo-new.svg
rename to static/assets/images/logo-new.svg
diff --git a/assets/images/logo-old.svg b/static/assets/images/logo-old.svg
similarity index 100%
rename from assets/images/logo-old.svg
rename to static/assets/images/logo-old.svg
diff --git a/assets/images/ms-graduation.jpg b/static/assets/images/ms-graduation.jpg
similarity index 100%
rename from assets/images/ms-graduation.jpg
rename to static/assets/images/ms-graduation.jpg
diff --git a/assets/images/mulberry-logo.png b/static/assets/images/mulberry-logo.png
similarity index 100%
rename from assets/images/mulberry-logo.png
rename to static/assets/images/mulberry-logo.png
diff --git a/assets/images/naju-gomtang-samaegi-garden.jpg b/static/assets/images/naju-gomtang-samaegi-garden.jpg
similarity index 100%
rename from assets/images/naju-gomtang-samaegi-garden.jpg
rename to static/assets/images/naju-gomtang-samaegi-garden.jpg
diff --git a/assets/images/naju-gomtang-samaegi-soup.jpg b/static/assets/images/naju-gomtang-samaegi-soup.jpg
similarity index 100%
rename from assets/images/naju-gomtang-samaegi-soup.jpg
rename to static/assets/images/naju-gomtang-samaegi-soup.jpg
diff --git a/assets/images/number-of-degrees.png b/static/assets/images/number-of-degrees.png
similarity index 100%
rename from assets/images/number-of-degrees.png
rename to static/assets/images/number-of-degrees.png
diff --git a/assets/images/profile.jpg b/static/assets/images/profile.jpg
similarity index 100%
rename from assets/images/profile.jpg
rename to static/assets/images/profile.jpg
diff --git a/assets/images/rockbox-sansa-clip.jpg b/static/assets/images/rockbox-sansa-clip.jpg
similarity index 100%
rename from assets/images/rockbox-sansa-clip.jpg
rename to static/assets/images/rockbox-sansa-clip.jpg
diff --git a/assets/images/what-if-theories.png b/static/assets/images/what-if-theories.png
similarity index 100%
rename from assets/images/what-if-theories.png
rename to static/assets/images/what-if-theories.png
diff --git a/themes/illusion-slopes/.gitignore b/themes/illusion-slopes/.gitignore
new file mode 100644
index 00000000..1922c734
--- /dev/null
+++ b/themes/illusion-slopes/.gitignore
@@ -0,0 +1 @@
+assets/fonts/ibm*
diff --git a/themes/illusion-slopes/LICENSE.txt b/themes/illusion-slopes/LICENSE.txt
new file mode 100644
index 00000000..ba6c670a
--- /dev/null
+++ b/themes/illusion-slopes/LICENSE.txt
@@ -0,0 +1,199 @@
+Copyright 2026 Max Kapur
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+or implied. See the License for the specific language governing
+permissions and limitations under the License.
+
+-------------------------------------------------------------------
+
+Apache License
+Version 2.0, January 2004
+http://www.apache.org/licenses/
+
+TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction, and
+ distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by the
+ copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all other
+ entities that control, are controlled by, or are under common control with
+ that entity. For the purposes of this definition, "control" means (i) the
+ power, direct or indirect, to cause the direction or management of such
+ entity, whether by contract or otherwise, or (ii) ownership of
+ fifty percent (50%) or more of the outstanding shares, or (iii) beneficial
+ ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity exercising
+ permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation source,
+ and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical transformation
+ or translation of a Source form, including but not limited to compiled
+ object code, generated documentation, and conversions to
+ other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or Object
+ form, made available under the License, as indicated by a copyright notice
+ that is included in or attached to the work (an example is provided in the
+ Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object form,
+ that is based on (or derived from) the Work and for which the editorial
+ revisions, annotations, elaborations, or other modifications represent,
+ as a whole, an original work of authorship. For the purposes of this
+ License, Derivative Works shall not include works that remain separable
+ from, or merely link (or bind by name) to the interfaces of, the Work and
+ Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including the original
+ version of the Work and any modifications or additions to that Work or
+ Derivative Works thereof, that is intentionally submitted to Licensor for
+ inclusion in the Work by the copyright owner or by an individual or
+ Legal Entity authorized to submit on behalf of the copyright owner.
+ For the purposes of this definition, "submitted" means any form of
+ electronic, verbal, or written communication sent to the Licensor or its
+ representatives, including but not limited to communication on electronic
+ mailing lists, source code control systems, and issue tracking systems
+ that are managed by, or on behalf of, the Licensor for the purpose of
+ discussing and improving the Work, but excluding communication that is
+ conspicuously marked or otherwise designated in writing by the copyright
+ owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity on
+ behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+2. Grant of Copyright License.
+
+ Subject to the terms and conditions of this License, each Contributor
+ hereby grants to You a perpetual, worldwide, non-exclusive, no-charge,
+ royalty-free, irrevocable copyright license to reproduce, prepare
+ Derivative Works of, publicly display, publicly perform, sublicense,
+ and distribute the Work and such Derivative Works in
+ Source or Object form.
+
+3. Grant of Patent License.
+
+ Subject to the terms and conditions of this License, each Contributor
+ hereby grants to You a perpetual, worldwide, non-exclusive, no-charge,
+ royalty-free, irrevocable (except as stated in this section) patent
+ license to make, have made, use, offer to sell, sell, import, and
+ otherwise transfer the Work, where such license applies only to those
+ patent claims licensable by such Contributor that are necessarily
+ infringed by their Contribution(s) alone or by combination of their
+ Contribution(s) with the Work to which such Contribution(s) was submitted.
+ If You institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work or a
+ Contribution incorporated within the Work constitutes direct or
+ contributory patent infringement, then any patent licenses granted to
+ You under this License for that Work shall terminate as of the date such
+ litigation is filed.
+
+4. Redistribution.
+
+ You may reproduce and distribute copies of the Work or Derivative Works
+ thereof in any medium, with or without modifications, and in Source or
+ Object form, provided that You meet the following conditions:
+
+ 1. You must give any other recipients of the Work or Derivative Works a
+ copy of this License; and
+
+ 2. You must cause any modified files to carry prominent notices stating
+ that You changed the files; and
+
+ 3. You must retain, in the Source form of any Derivative Works that You
+ distribute, all copyright, patent, trademark, and attribution notices from
+ the Source form of the Work, excluding those notices that do not pertain
+ to any part of the Derivative Works; and
+
+ 4. If the Work includes a "NOTICE" text file as part of its distribution,
+ then any Derivative Works that You distribute must include a readable copy
+ of the attribution notices contained within such NOTICE file, excluding
+ those notices that do not pertain to any part of the Derivative Works,
+ in at least one of the following places: within a NOTICE text file
+ distributed as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or, within a
+ display generated by the Derivative Works, if and wherever such
+ third-party notices normally appear. The contents of the NOTICE file are
+ for informational purposes only and do not modify the License.
+ You may add Your own attribution notices within Derivative Works that You
+ distribute, alongside or as an addendum to the NOTICE text from the Work,
+ provided that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and may
+ provide additional or different license terms and conditions for use,
+ reproduction, or distribution of Your modifications, or for any such
+ Derivative Works as a whole, provided Your use, reproduction, and
+ distribution of the Work otherwise complies with the conditions
+ stated in this License.
+
+5. Submission of Contributions.
+
+ Unless You explicitly state otherwise, any Contribution intentionally
+ submitted for inclusion in the Work by You to the Licensor shall be under
+ the terms and conditions of this License, without any additional
+ terms or conditions. Notwithstanding the above, nothing herein shall
+ supersede or modify the terms of any separate license agreement you may
+ have executed with Licensor regarding such Contributions.
+
+6. Trademarks.
+
+ This License does not grant permission to use the trade names, trademarks,
+ service marks, or product names of the Licensor, except as required for
+ reasonable and customary use in describing the origin of the Work and
+ reproducing the content of the NOTICE file.
+
+7. Disclaimer of Warranty.
+
+ Unless required by applicable law or agreed to in writing, Licensor
+ provides the Work (and each Contributor provides its Contributions)
+ on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
+ either express or implied, including, without limitation, any warranties
+ or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS
+ FOR A PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any risks
+ associated with Your exercise of permissions under this License.
+
+8. Limitation of Liability.
+
+ In no event and under no legal theory, whether in tort
+ (including negligence), contract, or otherwise, unless required by
+ applicable law (such as deliberate and grossly negligent acts) or agreed
+ to in writing, shall any Contributor be liable to You for damages,
+ including any direct, indirect, special, incidental, or consequential
+ damages of any character arising as a result of this License or out of
+ the use or inability to use the Work (including but not limited to damages
+ for loss of goodwill, work stoppage, computer failure or malfunction,
+ or any and all other commercial damages or losses), even if such
+ Contributor has been advised of the possibility of such damages.
+
+9. Accepting Warranty or Additional Liability.
+
+ While redistributing the Work or Derivative Works thereof, You may choose
+ to offer, and charge a fee for, acceptance of support, warranty,
+ indemnity, or other liability obligations and/or rights consistent with
+ this License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf of any
+ other Contributor, and only if You agree to indemnify, defend, and hold
+ each Contributor harmless for any liability incurred by, or claims
+ asserted against, such Contributor by reason of your accepting any such
+ warranty or additional liability.
+
+END OF TERMS AND CONDITIONS
diff --git a/themes/illusion-slopes/archetypes/default.md b/themes/illusion-slopes/archetypes/default.md
new file mode 100644
index 00000000..25b67521
--- /dev/null
+++ b/themes/illusion-slopes/archetypes/default.md
@@ -0,0 +1,5 @@
++++
+date = '{{ .Date }}'
+draft = true
+title = '{{ replace .File.ContentBaseName "-" " " | title }}'
++++
diff --git a/themes/illusion-slopes/assets/css/imports/elements.css b/themes/illusion-slopes/assets/css/imports/elements.css
new file mode 100644
index 00000000..d0ca9361
--- /dev/null
+++ b/themes/illusion-slopes/assets/css/imports/elements.css
@@ -0,0 +1,156 @@
+body {
+ color: var(--text-primary);
+ background-color: var(--background-primary);
+ font-family: var(--base-font-family);
+}
+
+/* Global header and footer */
+.body-header,
+.body-footer {
+ background-color: var(--background-highlight);
+ padding-top: var(--layout-spacing-unit);
+ padding-bottom: var(--layout-spacing-unit);
+
+ h1 {
+ font-size: var(--font-size-1);
+ }
+
+ &.body-footer {
+ border-top: var(--line-a);
+ }
+ &.body-header {
+ border-bottom: var(--line-a);
+ }
+}
+
+.footer-menu-wrapper {
+ display: flex;
+ justify-content: space-between;
+ gap: var(--horizontal-spacing-unit);
+ flex-wrap: wrap;
+}
+
+ul.menu {
+ list-style-type: none;
+ margin: 0;
+ padding: 0;
+ display: flex;
+ gap: calc(2 * var(--horizontal-spacing-unit));
+}
+
+h1,
+h2,
+h3,
+h4 {
+ &:not(&:first-child) {
+ margin-top: calc(2 * var(--vertical-spacing-unit));
+ }
+ &:not(&:last-child) {
+ margin-bottom: var(--vertical-spacing-unit);
+ }
+ font-weight: var(--strongest-font-weight);
+}
+
+p {
+ &:not(&:first-child) {
+ margin-top: var(--vertical-spacing-unit);
+ }
+ &:not(&:last-child) {
+ margin-bottom: var(--vertical-spacing-unit);
+ }
+}
+
+/*
+* Paragraph-like spacing between elements of a list if any element has internal
+* paragraphs
+*/
+ul,
+ol {
+ &:has(li p) li {
+ &:not(&:first-child) {
+ margin-top: var(--vertical-spacing-unit);
+ }
+ &:not(&:last-child) {
+ margin-bottom: var(--vertical-spacing-unit);
+ }
+ }
+}
+
+/* Headers within page/post content slide a notch down the font-size scale. */
+.article-content h1 {
+ font-size: var(--font-size-2);
+}
+.article-content h2 {
+ font-size: var(--font-size-3);
+}
+.article-content h3 {
+ font-size: var(--font-size-4);
+}
+
+blockquote {
+ margin-left: var(--horizontal-spacing-unit);
+ padding-left: var(--horizontal-spacing-unit);
+ border-left: var(--line-a);
+}
+
+a {
+ &,
+ &:active,
+ &:hover {
+ color: var(--brand-color-3);
+ }
+ &:visited {
+ color: var(--brand-color-2);
+ }
+ /* without href used for paginator button for current page */
+ &:disabled,
+ &:not([href]) {
+ color: inherit;
+ }
+}
+
+table {
+ background-color: var(--background-highlight);
+ border: var(--line-b1);
+ thead {
+ border-bottom: var(--line-b2);
+ background-color: var(--background-secondary);
+ }
+ th,
+ td {
+ border: var(--line-b1);
+ padding: var(--horizontal-spacing-unit);
+ }
+}
+
+hr {
+ border: none;
+ border-top: var(--line-a);
+}
+
+.katex math {
+ font-family: var(--math-font-family);
+}
+
+ul.pagination {
+ display: flex;
+ list-style-type: none;
+ gap: calc(2 * var(--horizontal-spacing-unit));
+ justify-content: center;
+}
+
+/* Inline code */
+code:not(pre code) {
+ font-size: var(--font-size-5);
+}
+
+/* Syntax-highlighted code blocks */
+pre {
+ overflow-x: scroll;
+ padding: var(--horizontal-spacing-unit);
+
+ code {
+ font-size: var(--font-size-5);
+ display: block;
+ }
+}
diff --git a/themes/illusion-slopes/assets/css/imports/layout.css b/themes/illusion-slopes/assets/css/imports/layout.css
new file mode 100644
index 00000000..60692f25
--- /dev/null
+++ b/themes/illusion-slopes/assets/css/imports/layout.css
@@ -0,0 +1,66 @@
+body {
+ display: flex;
+ flex-direction: column;
+ align-items: stretch;
+ gap: var(--layout-spacing-unit);
+
+ /* Float footer to bottom when viewport is taller than content */
+ min-height: 100%;
+ > *:last-child {
+ margin-top: auto;
+ }
+}
+
+.max-width-1 {
+ max-width: min(100%, var(--content-width-1));
+ padding-inline: var(--horizontal-spacing-unit);
+ margin-inline: auto;
+}
+
+.max-width-2 {
+ max-width: min(100%, var(--content-width-2));
+ padding-inline: var(--horizontal-spacing-unit);
+ margin-inline: auto;
+}
+
+.max-width-3 {
+ max-width: min(100%, var(--content-width-3));
+ padding-inline: var(--horizontal-spacing-unit);
+ margin-inline: auto;
+}
+
+.layout-spacing {
+ display: flex;
+ flex-direction: column;
+ gap: var(--layout-spacing-unit);
+}
+
+.vertical-spacing {
+ display: flex;
+ flex-direction: column;
+ gap: var(--vertical-spacing-unit);
+}
+
+.centered {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ text-align: center;
+}
+
+p:has(> img),
+figure {
+ display: flex;
+ flex-direction: column;
+
+ /*
+ TODO: Would be nice if images could exceed the max-width on the article
+ wrapper when viewport is wide
+ */
+
+ figcaption {
+ text-align: center;
+ font-size: var(--font-size-5);
+ color: var(--text-secondary);
+ }
+}
diff --git a/themes/illusion-slopes/assets/css/imports/preflight.css b/themes/illusion-slopes/assets/css/imports/preflight.css
new file mode 100644
index 00000000..7a03d3e4
--- /dev/null
+++ b/themes/illusion-slopes/assets/css/imports/preflight.css
@@ -0,0 +1,79 @@
+table {
+ border-collapse: collapse;
+}
+
+/* Below adapted from https://piccalil.li/blog/a-more-modern-css-reset/ */
+
+/* Box sizing rules */
+*,
+*::before,
+*::after {
+ box-sizing: border-box;
+}
+
+/* Prevent font size inflation */
+html {
+ -moz-text-size-adjust: none;
+ -webkit-text-size-adjust: none;
+ text-size-adjust: none;
+}
+
+body,
+h1,
+h2,
+h3,
+h4,
+p,
+figure,
+blockquote,
+dl,
+dd,
+ul,
+ol,
+li {
+ /* Remove default margin in favour of better control in authored CSS */
+ margin: 0;
+ /* Normalize line height */
+ line-height: 1.5em;
+}
+
+/* Set core body defaults */
+html,
+body {
+ height: 100%;
+}
+
+/* Balance text wrapping on headings */
+h1,
+h2,
+h3,
+h4 {
+ text-wrap: balance;
+}
+
+/* Make images easier to work with */
+img,
+picture {
+ max-width: 100%;
+ display: block;
+}
+
+/* Inherit fonts for inputs and buttons */
+input,
+button,
+textarea,
+select {
+ font-family: inherit;
+ font-size: inherit;
+}
+
+/* Anything that has been anchored to should have extra scroll margin */
+:target {
+ scroll-margin-block: 5ex;
+}
+
+/* Prevent footnote references from increasing line height */
+sup,
+sub {
+ line-height: 0;
+}
diff --git a/themes/illusion-slopes/assets/css/imports/states.css b/themes/illusion-slopes/assets/css/imports/states.css
new file mode 100644
index 00000000..e69de29b
diff --git a/themes/illusion-slopes/assets/css/imports/tokens.css b/themes/illusion-slopes/assets/css/imports/tokens.css
new file mode 100644
index 00000000..2f321d63
--- /dev/null
+++ b/themes/illusion-slopes/assets/css/imports/tokens.css
@@ -0,0 +1,41 @@
+:root {
+ --base-font-family: "IBM Plex Sans", "IBM Plex Sans KR", sans-serif;
+ --math-font-family: "IBM Plex Math", math, sans-serif;
+ --monospace-font-family: "IBM Plex Mono", monospace;
+
+ --content-width-1: 512px;
+ --content-width-2: 768px;
+ --content-width-3: 1024px;
+
+ --base-font-weight: 400;
+ --stronger-font-weight: 550;
+ --strongest-font-weight: 700;
+
+ /* Font scale */
+ --font-size-1: 2rem; /* Standalone h1 */
+ --font-size-2: 1.5rem; /* Article h1 */
+ --font-size-3: 1.2rem; /* Article h2 */
+ --font-size-4: 1rem; /* Article h3 */
+ --font-size-5: 0.9rem; /* Smaller/caption text */
+
+ --vertical-spacing-unit: 0.5em; /* Determines spacing of paragraphs, headers in prose */
+ --horizontal-spacing-unit: 0.5rem; /* E.g. blockquote padding-left */
+ --layout-spacing-unit: 32px; /* For layout of structural elements of the page as a whole*/
+
+ --text-primary: #000000;
+ --text-secondary: #546270;
+
+ --background-primary: #ffffeb;
+ --background-secondary: #b7ccb7;
+ --background-highlight: #fffff5;
+
+ --brand-color-2: #206020;
+ --brand-color-3: #008000;
+
+ /* E.g. outline of header and footer */
+ --line-a: 2px solid var(--background-secondary);
+
+ /* E.g. table boundaries (thick one below header row) */
+ --line-b1: 1px solid var(--text-secondary);
+ --line-b2: 2px solid var(--text-secondary);
+}
diff --git a/themes/illusion-slopes/assets/css/imports/variants.css b/themes/illusion-slopes/assets/css/imports/variants.css
new file mode 100644
index 00000000..153e9daa
--- /dev/null
+++ b/themes/illusion-slopes/assets/css/imports/variants.css
@@ -0,0 +1,36 @@
+/* Used for the site header, clickable post headers, etc. */
+a.subtle {
+ color: inherit;
+ text-decoration: none;
+
+ &:hover {
+ text-decoration: underline;
+ }
+}
+
+/* Used in page.html: a standalone page or post */
+article.page-item {
+ .article-header h1 {
+ font-size: var(--font-size-1);
+ }
+}
+
+/* Used in section.html: a page/post (preview) shown in section listing */
+article.section-item {
+ .article-header h1 {
+ font-size: var(--font-size-2);
+ }
+}
+
+/* Collapse margin between header and subtitle */
+h1:has(+ p.subtitle) {
+ margin-bottom: 0;
+}
+
+p.subtitle {
+ color: var(--text-secondary);
+}
+
+.smaller {
+ font-size: var(--font-size-5);
+}
diff --git a/themes/illusion-slopes/assets/css/style.css b/themes/illusion-slopes/assets/css/style.css
new file mode 100644
index 00000000..0d8fdd44
--- /dev/null
+++ b/themes/illusion-slopes/assets/css/style.css
@@ -0,0 +1,22 @@
+/*
+ These imports point to CSS files from IBM installed by download_assets.py.
+ To help regenerate this file after an IBM Plex update, run:
+
+ find static/ -iname '*default.min.css'
+
+ This excludes the '*all.min.css' variants, which have more glyphs.
+*/
+
+@import "../fonts/ibm-plex-mono/css/ibm-plex-mono-default.css";
+@import "../fonts/ibm-plex-math/css/ibm-plex-math-default.css";
+@import "../fonts/ibm-plex-sans-kr/css/ibm-plex-sans-kr-default.css";
+@import "../fonts/ibm-plex-sans/css/ibm-plex-sans-default.css";
+
+/* https://jacobb.nyc/writing/how-i-write-css-in-2024 */
+
+@import "imports/preflight.css";
+@import "imports/tokens.css";
+@import "imports/elements.css";
+@import "imports/variants.css";
+@import "imports/states.css";
+@import "imports/layout.css";
diff --git a/themes/illusion-slopes/assets/favicon/README.md b/themes/illusion-slopes/assets/favicon/README.md
new file mode 100644
index 00000000..dec40a0c
--- /dev/null
+++ b/themes/illusion-slopes/assets/favicon/README.md
@@ -0,0 +1,7 @@
+# Favicon development area
+
+`favicon.svg` is the source file from which the rasterized variant is generated:
+
+```shell
+magick -background none favicon.svg -resize 32x32 favicon.png
+```
diff --git a/themes/illusion-slopes/assets/favicon/favicon.png b/themes/illusion-slopes/assets/favicon/favicon.png
new file mode 100644
index 00000000..b9844059
Binary files /dev/null and b/themes/illusion-slopes/assets/favicon/favicon.png differ
diff --git a/_favicon/favicon.svg b/themes/illusion-slopes/assets/favicon/favicon.svg
similarity index 100%
rename from _favicon/favicon.svg
rename to themes/illusion-slopes/assets/favicon/favicon.svg
diff --git a/themes/illusion-slopes/assets/js/main.js b/themes/illusion-slopes/assets/js/main.js
new file mode 100644
index 00000000..2f45f613
--- /dev/null
+++ b/themes/illusion-slopes/assets/js/main.js
@@ -0,0 +1 @@
+console.log("This site was generated by Hugo.");
diff --git a/themes/illusion-slopes/content/_index.md b/themes/illusion-slopes/content/_index.md
new file mode 100644
index 00000000..652623b5
--- /dev/null
+++ b/themes/illusion-slopes/content/_index.md
@@ -0,0 +1,9 @@
++++
+title = 'Home'
+date = 2023-01-01T08:00:00-07:00
+draft = false
++++
+
+Laborum voluptate pariatur ex culpa magna nostrud est incididunt fugiat
+pariatur do dolor ipsum enim. Consequat tempor do dolor eu. Non id id anim anim
+excepteur excepteur pariatur nostrud qui irure ullamco.
diff --git a/themes/illusion-slopes/content/pages/_index.md b/themes/illusion-slopes/content/pages/_index.md
new file mode 100644
index 00000000..0d411dda
--- /dev/null
+++ b/themes/illusion-slopes/content/pages/_index.md
@@ -0,0 +1,7 @@
++++
+title = 'Pages'
+date = 2023-01-01T08:30:00-07:00
+draft = false
++++
+
+Tempor est exercitation ad qui pariatur quis adipisicing aliquip nisi ea consequat ipsum occaecat. Nostrud consequat ullamco laboris fugiat esse esse adipisicing velit laborum ipsum incididunt ut enim. Dolor pariatur nulla quis fugiat dolore excepteur. Aliquip ad quis aliqua enim do consequat.
diff --git a/themes/illusion-slopes/content/posts/_index.md b/themes/illusion-slopes/content/posts/_index.md
new file mode 100644
index 00000000..e7066c09
--- /dev/null
+++ b/themes/illusion-slopes/content/posts/_index.md
@@ -0,0 +1,7 @@
++++
+title = 'Posts'
+date = 2023-01-01T08:30:00-07:00
+draft = false
++++
+
+Tempor est exercitation ad qui pariatur quis adipisicing aliquip nisi ea consequat ipsum occaecat. Nostrud consequat ullamco laboris fugiat esse esse adipisicing velit laborum ipsum incididunt ut enim. Dolor pariatur nulla quis fugiat dolore excepteur. Aliquip ad quis aliqua enim do consequat.
diff --git a/themes/illusion-slopes/download_assets.py b/themes/illusion-slopes/download_assets.py
new file mode 100755
index 00000000..34f27919
--- /dev/null
+++ b/themes/illusion-slopes/download_assets.py
@@ -0,0 +1,56 @@
+#!/usr/bin/env python
+from io import BytesIO
+from pathlib import Path
+from zipfile import ZipFile
+
+import requests
+
+assets_dir = Path(__file__).parent / "assets"
+
+
+def main():
+ ibm_plex_fonts()
+
+
+def ibm_plex_fonts():
+ """Download/extract IBM Plex fonts from GitHub releases."""
+
+ output_dir = assets_dir / "fonts"
+
+ urls = {
+ "ibm-plex-mono": "https://github.com/IBM/plex/releases/download/%40ibm%2Fplex-mono%401.1.0/ibm-plex-mono.zip",
+ "ibm-plex-math": "https://github.com/IBM/plex/releases/download/%40ibm%2Fplex-math%401.1.0/ibm-plex-math.zip",
+ "ibm-plex-sans": "https://github.com/IBM/plex/releases/download/%40ibm%2Fplex-sans%401.1.0/ibm-plex-sans.zip",
+ "ibm-plex-sans-kr": "https://github.com/IBM/plex/releases/download/%40ibm%2Fplex-sans-kr%401.1.0/ibm-plex-sans-kr.zip",
+ }
+
+ for font_name, url in urls.items():
+ # Plex .zips have a top-level directory; we extract into just output_dir
+ # to prevent double nesting but need the font name here to make sure we check
+ # against the right font
+ expected_outdir = output_dir / font_name
+
+ if list(expected_outdir.glob("**/*.css")) and list(
+ expected_outdir.glob("**/*.woff2")
+ ):
+ relpath = expected_outdir.relative_to(Path().absolute())
+ print(f"{relpath} already looks good, skipping download")
+ continue
+ del expected_outdir # Avoid misuse
+
+ print(f"Downloading {font_name}.zip ...", end="")
+ response = requests.get(url)
+ assert response.ok
+ print("OK")
+
+ with BytesIO() as buffer:
+ buffer.write(response.content)
+ with ZipFile(buffer) as zipfile:
+ output_dir.mkdir(exist_ok=True, parents=True)
+ zipfile.extractall(output_dir)
+
+ print(f"Extracted {font_name}.zip")
+
+
+if __name__ == "__main__":
+ main()
diff --git a/themes/illusion-slopes/hugo.toml b/themes/illusion-slopes/hugo.toml
new file mode 100644
index 00000000..3efa87ff
--- /dev/null
+++ b/themes/illusion-slopes/hugo.toml
@@ -0,0 +1,44 @@
+baseURL = 'https://example.org/'
+locale = 'en-US'
+title = 'My New Hugo Project'
+
+[mediaTypes]
+ [mediaTypes.'application/atom+xml']
+ suffixes = ['xml']
+
+[outputFormats]
+ [outputFormats.atom]
+ baseName = 'feed'
+ isHTML = false
+ isPlainText = false
+ mediaType = 'application/atom+xml'
+ noUgly = true
+ notAlternative = false
+ path = ''
+ permalinkable = false
+ protocol = ''
+ rel = 'alternate'
+ root = false
+ ugly = false
+ weight = 0
+
+[menus]
+ # [[menus.main]]
+ # name = 'Home'
+ # pageRef = '/'
+ # weight = 10
+
+ [[menus.main]]
+ name = 'Posts'
+ pageRef = '/posts'
+ weight = 20
+
+ [[menus.main]]
+ name = 'Tags'
+ pageRef = '/tags'
+ weight = 30
+
+[module]
+ [module.hugoVersion]
+ extended = false
+ min = '0.146.0'
diff --git a/themes/illusion-slopes/layouts/_partials/copyright.html b/themes/illusion-slopes/layouts/_partials/copyright.html
new file mode 100644
index 00000000..61a1cb92
--- /dev/null
+++ b/themes/illusion-slopes/layouts/_partials/copyright.html
@@ -0,0 +1,4 @@
+
+ {{- end }}
+{{- end }}
diff --git a/themes/illusion-slopes/layouts/_partials/pagination.html b/themes/illusion-slopes/layouts/_partials/pagination.html
new file mode 100644
index 00000000..a196df2f
--- /dev/null
+++ b/themes/illusion-slopes/layouts/_partials/pagination.html
@@ -0,0 +1,242 @@
+{{/* Adapted from https://github.com/gohugoio/hugo/blob/45c00b7c162b55ca9bcdd9a664bcf1294aa5d266/tpl/tplimpl/embedded/templates/_partials/pagination.html */}}
+
+{{- $validFormats := slice "default" "terse" }}
+
+{{- $msg1 := "When passing a map to the internal pagination template, one of the elements must be named 'page', and it must be set to the context of the current page." }}
+{{- $msg2 := "The 'format' specified in the map passed to the internal pagination template is invalid. Valid choices are: %s." }}
+
+{{- $page := . }}
+{{- $format := "terse" }}
+
+{{- if reflect.IsMap . }}
+ {{- with .page }}
+ {{- $page = . }}
+ {{- else }}
+ {{- errorf $msg1 }}
+ {{- end }}
+ {{- with .format }}
+ {{- $format = lower . }}
+ {{- end }}
+{{- end }}
+
+{{- if in $validFormats $format }}
+ {{- if gt $page.Paginator.TotalPages 1 }}
+
+
+ {{/* Insert a "read more" link if summary differs from content. One way
+ this happens is via truncation (inserting the line).
+ Another way is by manually defining a summary in frontmatter. We need
+ to handle the latter case separately because frontmatter summaries
+ actually set .Truncated to false:
+
+ https://gohugo.io/methods/page/truncated/
+ */}}
+ {{ $canReadMore := or .Truncated (.Params.summary) }}
+
+ {{ if $canReadMore }}
+
+ {{ end }}
+
+ {{ end }}
+
+
+
+
+{{ end }}
diff --git a/themes/illusion-slopes/layouts/taxonomy.html b/themes/illusion-slopes/layouts/taxonomy.html
new file mode 100644
index 00000000..c2e78751
--- /dev/null
+++ b/themes/illusion-slopes/layouts/taxonomy.html
@@ -0,0 +1,7 @@
+{{ define "main" }}
+