<?xml version="1.0" encoding="UTF-8"?><rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>John D. Cook</title>
	<atom:link href="http://www.johndcook.com/blog/feed/" rel="self" type="application/rss+xml" />
	<link>https://www.johndcook.com/blog</link>
	<description>Applied Mathematics Consulting</description>
	<lastBuildDate>Mon, 03 Aug 2026 15:07:41 +0000</lastBuildDate>
	<language>en-US</language>
	<sy:updatePeriod>
	hourly	</sy:updatePeriod>
	<sy:updateFrequency>
	1	</sy:updateFrequency>
	

<image>
	<url>https://www.johndcook.com/wp-content/uploads/2020/01/cropped-favicon_512-32x32.png</url>
	<title>John D. Cook</title>
	<link>https://www.johndcook.com/blog</link>
	<width>32</width>
	<height>32</height>
</image> 
	<item>
		<title>Holonomic functions</title>
		<link>https://www.johndcook.com/blog/2026/08/02/holonomic-functions/</link>
					<comments>https://www.johndcook.com/blog/2026/08/02/holonomic-functions/#respond</comments>
		
		<dc:creator><![CDATA[John]]></dc:creator>
		<pubDate>Sun, 02 Aug 2026 20:47:53 +0000</pubDate>
				<category><![CDATA[Math]]></category>
		<category><![CDATA[Differential equations]]></category>
		<category><![CDATA[Special functions]]></category>
		<guid isPermaLink="false">https://www.johndcook.com/blog/?p=247524</guid>

					<description><![CDATA[<p>Yesterday I wrote that a lot of the special functions that pop up in mathematical physics are solutions to second order linear differential equations with polynomial coefficients. More generally, holonomic functions are defined to be those functions that are the solutions to linear differential equations, of any order, with polynomial coefficients. Most special functions are [&#8230;]</p>
The post <a href="https://www.johndcook.com/blog/2026/08/02/holonomic-functions/">Holonomic functions</a> first appeared on <a href="https://www.johndcook.com/blog">John D. Cook</a>.]]></description>
										<content:encoded><![CDATA[<p><a href="https://www.johndcook.com/blog/2026/08/01/why-polynomial-coefficients/">Yesterday</a> I wrote that a lot of the special functions that pop up in mathematical physics are solutions to second order linear differential equations with polynomial coefficients. More generally,<strong> holonomic functions</strong> are defined to be those functions that are the solutions to linear differential equations, of any order, with polynomial coefficients.</p>
<p>Most special functions are holonomic. To quantify that statement, I went through the special functions covered in Abramowitz and Stegun. The large majority are holonomic, thought some common functions like the gamma function are not holonomic.</p>
<p><a href="https://www.johndcook.com/holonomic_odes_abramowitz_stegun.pdf">This report</a> goes through the functions in A&amp;S. For those that are holonomic, it gives the differential equation that the function solves. The large majority of these equations are second order, but not all. And the coefficients are nearly always first or second order polynomials, rarely higher order.</p>The post <a href="https://www.johndcook.com/blog/2026/08/02/holonomic-functions/">Holonomic functions</a> first appeared on <a href="https://www.johndcook.com/blog">John D. Cook</a>.]]></content:encoded>
					
					<wfw:commentRss>https://www.johndcook.com/blog/2026/08/02/holonomic-functions/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Estimating a cumulative sum</title>
		<link>https://www.johndcook.com/blog/2026/08/02/estimating-a-cumulative-sum/</link>
					<comments>https://www.johndcook.com/blog/2026/08/02/estimating-a-cumulative-sum/#respond</comments>
		
		<dc:creator><![CDATA[John]]></dc:creator>
		<pubDate>Sun, 02 Aug 2026 17:57:31 +0000</pubDate>
				<category><![CDATA[Math]]></category>
		<guid isPermaLink="false">https://www.johndcook.com/blog/?p=247518</guid>

					<description><![CDATA[<p>In this post I mentioned two series which I denoted t(n) and c(n). The former is the number of unlabeled rooted trees with n nodes. The latter is the cumulative sum of the former, i.e. The sequence c(n) is also the number of constraints on an n-step Runge-Kutta method; that&#8217;s how I became interested in it. Now the t(n) sequence [&#8230;]</p>
The post <a href="https://www.johndcook.com/blog/2026/08/02/estimating-a-cumulative-sum/">Estimating a cumulative sum</a> first appeared on <a href="https://www.johndcook.com/blog">John D. Cook</a>.]]></description>
										<content:encoded><![CDATA[<p>In <a href="https://www.johndcook.com/blog/2026/08/01/counting-rooted-trees/">this post</a> I mentioned two series which I denoted <em>t</em>(<em>n</em>) and <em>c</em>(<em>n</em>). The former is the number of unlabeled rooted trees with <em>n</em> nodes. The latter is the cumulative sum of the former, i.e.</p>
<p><img decoding="async" class="aligncenter" style="background-color: white;" src="https://www.johndcook.com/cumsum_asymp1.svg" alt="c(n) = t(1) + t(2) + t(3) + \cdots + t(n)" width="320" height="18" /></p>
<p>The sequence <em>c</em>(<em>n</em>) is also the number of constraints on an <em>n</em>-step Runge-Kutta method; that&#8217;s how I became interested in it.</p>
<p>Now the <em>t</em>(<em>n</em>) sequence has been cataloged as OEIS <a href="https://oeis.org/A000081">A000081</a> and OEIS gives the asymptotic estimate of <em>t</em>(<em>n</em>) for large <em>n</em> as</p>
<p><img decoding="async" class="aligncenter" style="background-color: white;" src="https://www.johndcook.com/cumsum_asymp2.svg" alt="t(n) \sim C \frac{a^n}{n^{3/2}}" width="105" height="42" /></p>
<p>where <em>C</em> = 0.4399… and α = 2.9557….</p>
<p>The cumulative sum of <em>t</em>(<em>n</em>), what I&#8217;ve called <em>c</em>(<em>n</em>), is also cataloged in OEIS, sequence number <a href="https://oeis.org/A087803">A087803</a>. However, OEIS does not give an asymptotic estimate for this sequence. I&#8217;ll give one here.</p>
<p>(Update: After looking closer at the page for A087803 I see that there is an asymptotic formula, the same one derived here.)</p>
<p>The basis for my derivation is to assume the cumulative sum of the asymptotic estimates gives an asymptotic estimate of the cumulative sum. This is justified by the fact that the sequence is increasing rapidly and only the last few terms contribute much relatively to the sum.</p>
<p>The technique illustrated here would be applicable to the cumulative sum of other series whose asymptotic form is known.</p>
<p><img fetchpriority="high" decoding="async" class="aligncenter" style="background-color: white;" src="https://www.johndcook.com/cumsum_asymp.svg" alt="\begin{align*} c(n) &amp;= \sum_{n=1}^N t(n) \\ &amp;\sim \sum_{n=1}^N C \frac{a^n}{n^{3/2}}\\ &amp;= C \frac{a^N}{N^{3/2}} \sum_{k=0}^{N-1} a^{-k}\left(1 - \frac{k}{N} \right)^{-3/2} \\ &amp;\sim C \frac{a^N}{N^{3/2}} \sum_{k=0}^\infty a^{-k} \\ &amp;= C \frac{a^N}{N^{3/2}} \frac{a}{a-1} \\ &amp;= C \frac{a^{N+1}}{(a-1)N^{3/2}} \end{align*} " width="280" height="371" /></p>
<p>Here&#8217;s code to visualize the rate of convergence.</p>
<pre>import numpy as np
import matplotlib.pyplot as plt

# from https://oeis.org/A000081/b000081.txt
A000081 = [
    0,
    1,
    1,
    2,
    4,
    ...
    51384328351659326880337136395054298255277970,
]  
A087803 = np.cumsum(A000081)

def approx(n):
    C = 0.43992401257102530
    a = 2.95576528565199497
    return C*a**(n+1)*n**(-3/2)/(a - 1)

n = np.arange(len(A087803))
ratio = A087803/approx(n)

plt.plot(n[1:], ratio[1:])
plt.plot(n, 0*n + 1, '--')
plt.xlabel("$n$")
plt.ylabel("exact/approx")
plt.show()
</pre>
<p>Here&#8217;s the plot:</p>
<p><img loading="lazy" decoding="async" class="aligncenter size-medium" src="https://www.johndcook.com/cumsum_asymp_plot.png" width="640" height="480" /></p>The post <a href="https://www.johndcook.com/blog/2026/08/02/estimating-a-cumulative-sum/">Estimating a cumulative sum</a> first appeared on <a href="https://www.johndcook.com/blog">John D. Cook</a>.]]></content:encoded>
					
					<wfw:commentRss>https://www.johndcook.com/blog/2026/08/02/estimating-a-cumulative-sum/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Why polynomial coefficients?</title>
		<link>https://www.johndcook.com/blog/2026/08/01/why-polynomial-coefficients/</link>
					<comments>https://www.johndcook.com/blog/2026/08/01/why-polynomial-coefficients/#respond</comments>
		
		<dc:creator><![CDATA[John]]></dc:creator>
		<pubDate>Sat, 01 Aug 2026 20:18:41 +0000</pubDate>
				<category><![CDATA[Math]]></category>
		<category><![CDATA[Differential equations]]></category>
		<category><![CDATA[Science]]></category>
		<guid isPermaLink="false">https://www.johndcook.com/blog/?p=247512</guid>

					<description><![CDATA[<p>Second order linear differential equations with polynomial coefficients form their own area of study. This seems like a narrow class of equations, but it&#8217;s very important in applications. This class of equations seems like a mathematically natural topic, but why is it so important in applications? I did a PhD in differential equations without ever [&#8230;]</p>
The post <a href="https://www.johndcook.com/blog/2026/08/01/why-polynomial-coefficients/">Why polynomial coefficients?</a> first appeared on <a href="https://www.johndcook.com/blog">John D. Cook</a>.]]></description>
										<content:encoded><![CDATA[<p>Second order linear differential equations with polynomial coefficients form their own area of study. This seems like a narrow class of equations, but it&#8217;s very important in applications.</p>
<p>This class of equations seems like a mathematically natural topic, but why is it so important in applications? I did a PhD in differential equations without ever learning why. The theory of second order linear equations with polynomial coefficients is too complicated for undergraduate courses [0] and too well-established for graduate courses [1]. </p>
<p>The explanation that I was missing can be found in the first chapter of [2]. The PDEs that are common in physics are separable in various coordinate systems, meaning that in these coordinate systems the PDEs reduce to ODEs. These ODEs either have polynomial coefficients, or there is a change of variables which makes the ODEs have polynomial coefficients.</p>
<p>See this <a href="https://www.johndcook.com/separable_helmholtz.pdf">writeup</a> that looks at the Helmholtz and Laplace equations in 11 coordinate systems.</p>
<p>[0] You may see the simplest parts of the theory in a section on solving ODEs with power series. But textbooks don&#8217;t go very far for good reasons.</p>
<p>[1] Unfortunately, a lot of really useful topics are left out of the graduate curriculum because they&#8217;re too well understood to provide thesis topics. Or the problems that are still open have been open for so long that they&#8217;re likely too hard to be cracked by a graduate student.</p>
<p>[2] Gerhard Kristensson. Second Order Differential Equations: Special Functions and their Classification. Springer, 2010.</p>The post <a href="https://www.johndcook.com/blog/2026/08/01/why-polynomial-coefficients/">Why polynomial coefficients?</a> first appeared on <a href="https://www.johndcook.com/blog">John D. Cook</a>.]]></content:encoded>
					
					<wfw:commentRss>https://www.johndcook.com/blog/2026/08/01/why-polynomial-coefficients/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Counting rooted trees</title>
		<link>https://www.johndcook.com/blog/2026/08/01/counting-rooted-trees/</link>
					<comments>https://www.johndcook.com/blog/2026/08/01/counting-rooted-trees/#respond</comments>
		
		<dc:creator><![CDATA[John]]></dc:creator>
		<pubDate>Sat, 01 Aug 2026 16:15:53 +0000</pubDate>
				<category><![CDATA[Math]]></category>
		<category><![CDATA[Combinatorics]]></category>
		<category><![CDATA[Differential equations]]></category>
		<guid isPermaLink="false">https://www.johndcook.com/blog/?p=247502</guid>

					<description><![CDATA[<p>Combinatorial problems can be interesting for their own sake, but they are more interesting when there is a connection to a problem outside combinatorics, and the more unexpected the connection the better. Counting the number of unlabeled rooted trees [1] with n nodes is a pure mathematics problem. Designing numerical methods for solving differential equations [&#8230;]</p>
The post <a href="https://www.johndcook.com/blog/2026/08/01/counting-rooted-trees/">Counting rooted trees</a> first appeared on <a href="https://www.johndcook.com/blog">John D. Cook</a>.]]></description>
										<content:encoded><![CDATA[<p>Combinatorial problems can be interesting for their own sake, but they are more interesting when there is a connection to a problem outside combinatorics, and the more unexpected the connection the better.</p>
<p>Counting the number of unlabeled rooted trees [1] with <em>n</em> nodes is a pure mathematics problem. Designing numerical methods for solving differential equations is an applied mathematics problem. And yet the two are closely linked.</p>
<p>Let <em>t</em>(<em>n</em>) be the number of distinct unlabeled rooted trees with <em>n</em> nodes. The diagram below shows that the first few terms of this sequence are 1, 1, 2, and 4.</p>
<p><img loading="lazy" decoding="async" class="aligncenter size-medium" style="background-color: white;" src="https://www.johndcook.com/rooted_trees_order_1_to_4.svg" width="680" height="600" /></p>
<p>In an <a href="https://www.johndcook.com/blog/2026/07/31/runge-kutta-design/">earlier post</a> I showed that designing a 4-stage explicit Runge-Kutta method required solving a system of 8 equations in 10 unknowns, leaving two degrees of freedom in the solutions.</p>
<p>The number of constraints <em>c</em>(<em>s</em>) needed to design an <em>s</em>-stage explicit RK method is equal to the number of rooted trees with up to <em>s</em> nodes:</p>
<p style="padding-left: 40px;"><em>c</em>(<em>s</em>) = <em>t</em>(1) + <em>t</em>(2) + <em>t</em>(3) + … + <em>t</em>(<em>s</em>)</p>
<p>This is because there is a one-to-one correspondence between constraints on the <em>n</em>th derivative of an RK formula and rooted trees, and an <em>s</em> stage method has to satisfy the constraints of all stages up to <em>s</em>. In the example of the 4th order RK method, we have</p>
<p style="padding-left: 40px;"><em>c</em>(4) = <em>t</em>(1) + <em>t</em>(2)  + <em>t</em>(3) + <em>t</em>(4) = 1 + 1 + 2 + 4 = 8.</p>
<p>The first few values [2] of <em>t</em>(<em>n</em>) are</p>
<p style="padding-left: 40px;">1, 1, 2, 4, 9, 20, 48, 115, 286, 719, 1842, 4766, 12486, 32973, …</p>
<p>and so you can see that <em>t</em>(<em>n</em>) grows quickly. In fact, it grows exponentially [3].</p>
<p>However, the number of parameters in an <em>s</em> stage RK method is <em>s</em>(<em>s</em> + 1)/2. The number of equations grows exponentially and the number of variables grows only quadratically, so at some point you have more equations than variables. That&#8217;s already the case for <em>s</em> = 5 because you have 17 constraints on 15 variables. The system has a solution because symmetry considerations render some of the equations redundant.</p>
<p>A 10th order RK method requires 17 stages. (See the <a href="https://www.johndcook.com/blog/2026/08/01/butcher-barrier/">previous post</a> for why the number of stages exceeds the order when the order is greater than 4.) Designing such a method would require solving over a million equations in 153 variables, and yet it can be done. [4]</p>
<h2>Related posts</h2>
<ul>
<li class="link"><a href="https://www.johndcook.com/blog/2026/07/31/runge-kutta-design/">Solving the RK4 equations</a></li>
<li class="link"><a href="https://www.johndcook.com/blog/2026/07/27/counting-permutations-with-roots/">Counting permutations with roots</a></li>
<li class="link"><a href="https://www.johndcook.com/blog/2026/06/30/dna-sequence-alignment-and-kings/">DNA alignment and Kings</a></li>
</ul>
<p>[1] This is a slightly contradictory term. Unlabeled means the we don&#8217;t distinguish the nodes. But we do distinguish one node, namely the root.</p>
<p>[2] See OEIS <a href="https://oeis.org/A000081">A000081</a>.</p>
<p>[2] Richard Otter proved in 1948 that the number of unlabeled rooted trees with <em>n</em> nodes is asymptotically <em>C</em> α<sup><em>n</em></sup> / <em>n</em><sup>−3/2</sup> where <em>C</em> = 0.4399… and α = 2.9557…. The cumulative sum is at least this large since Otter&#8217;s estimate gives the size of the last term in the sum.</p>
<p>[3] E. Hairer. A Runge-Kutta Method of Order 10. J. Inst. Maths Applics (1978) 21, 47-59</p>The post <a href="https://www.johndcook.com/blog/2026/08/01/counting-rooted-trees/">Counting rooted trees</a> first appeared on <a href="https://www.johndcook.com/blog">John D. Cook</a>.]]></content:encoded>
					
					<wfw:commentRss>https://www.johndcook.com/blog/2026/08/01/counting-rooted-trees/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Runge-Kutta order versus stages</title>
		<link>https://www.johndcook.com/blog/2026/08/01/butcher-barrier/</link>
					<comments>https://www.johndcook.com/blog/2026/08/01/butcher-barrier/#comments</comments>
		
		<dc:creator><![CDATA[John]]></dc:creator>
		<pubDate>Sat, 01 Aug 2026 15:58:15 +0000</pubDate>
				<category><![CDATA[Math]]></category>
		<category><![CDATA[Differential equations]]></category>
		<guid isPermaLink="false">https://www.johndcook.com/blog/?p=247499</guid>

					<description><![CDATA[<p>The textbook version of the Runge-Kutta method for solving differential equations has 4 stages and has 4th order error. For lower order versions of RK the number of stages s also matches the order of the error p. But in order to achieve error on the order of p ≥ 5, you need more than p [&#8230;]</p>
The post <a href="https://www.johndcook.com/blog/2026/08/01/butcher-barrier/">Runge-Kutta order versus stages</a> first appeared on <a href="https://www.johndcook.com/blog">John D. Cook</a>.]]></description>
										<content:encoded><![CDATA[<p>The textbook version of the Runge-Kutta method for solving differential equations has 4 stages and has 4th order error. For lower order versions of RK the number of stages <em>s</em> also matches the order of the error <em>p</em>. But in order to achieve error on the order of <em>p</em> ≥ 5, you need more than <em>p</em> stages. This is known as the Butcher barrier.</p>
<p>Before going any further, let&#8217;s back up and say what we mean by stages and by order.</p>
<h2>Stages</h2>
<p>The number of stages in an RK method to solve the equation</p>
<p><img decoding="async" class="aligncenter" src="https://www.johndcook.com/first_order_ode.svg" alt="y' = f(t, y)" width="82" /></p>
<p>is the number of evaluations of the function <em>f</em> on the right-hand side. For example, the textbook RK4 method estimates the solution at each step by</p>
<p><img decoding="async" class="aligncenter" src="https://www.johndcook.com/rk4.svg" alt="y_{n+1} = y_n + \frac{h}{6}\left( k_{n1} + 2k_{n2} + 2k_{n3} + k_{n4}\right)" width="305" /></p>
<p>where</p>
<p><img decoding="async" class="aligncenter" src="https://www.johndcook.com/rk4k.svg" alt="k_{n1} &amp;=&amp; f(t_n, y_n) \\ k_{n2} &amp;=&amp; f(t_n + 0.5h, y_n + 0.5hk_{n1}) \\ k_{n3} &amp;=&amp; f(t_n + 0.5h, y_n + 0.5hk_{n2}) \\ k_{n4} &amp;=&amp; f(t_n + h, y_n + hk_{n3}) \\" width="280" /></p>
<p>which requires four stages, i.e. four evaluations of <em>f</em>.</p>
<h2>Order</h2>
<p>A differential equation solver is said to have order <em>p</em> if the local error, the error after one step of size <em>h</em>, is <em>O</em>(<em>h</em><sup><em>p</em> + 1</sup>). Then after solving an ODE over a period of time <em>T</em> with <em>N</em> = <em>T</em>/<em>h</em> steps, the global error is <em>O</em>(<em>h</em><sup><em>p</em></sup>). So, for example, if <em>p</em> = 4, you would expect that cutting your step size <em>h</em> in half would cut your error at <em>T</em> by a factor of 16.</p>
<h2>More stages than the order</h2>
<p>John C. Butcher proved that an explicit RK method of order <em>p</em> requires <em>s</em> stages where <em>s</em> &gt; <em>p</em> if <em>p</em> &gt; 4.</p>
<p>An important example is the Dormand-Prince method. It is a version of RK that has order 5 and 7 stages. The clever thing about this method is that you can make a 4th order solver out of a subset of its function evaluations.</p>
<p>That means that after you&#8217;ve evaluated one step of the 5th order method, you can also evaluate a 4th order method essentially for free. And by comparing them, you can get a sense of the error. If the solutions given by the two methods are substantially different, you have probably taken too big a step and need to back up. If the two solutions essentially agree, you&#8217;re probably good to take the next step.</p>
<p>For an explict RK method to have order 5, 6, or 7 you need at least 6, 7, or 9 stages respectively.</p>The post <a href="https://www.johndcook.com/blog/2026/08/01/butcher-barrier/">Runge-Kutta order versus stages</a> first appeared on <a href="https://www.johndcook.com/blog">John D. Cook</a>.]]></content:encoded>
					
					<wfw:commentRss>https://www.johndcook.com/blog/2026/08/01/butcher-barrier/feed/</wfw:commentRss>
			<slash:comments>1</slash:comments>
		
		
			</item>
		<item>
		<title>Solving the RK4 design equations</title>
		<link>https://www.johndcook.com/blog/2026/07/31/runge-kutta-design/</link>
					<comments>https://www.johndcook.com/blog/2026/07/31/runge-kutta-design/#respond</comments>
		
		<dc:creator><![CDATA[John]]></dc:creator>
		<pubDate>Fri, 31 Jul 2026 18:59:02 +0000</pubDate>
				<category><![CDATA[Math]]></category>
		<category><![CDATA[Differential equations]]></category>
		<category><![CDATA[Mathematica]]></category>
		<guid isPermaLink="false">https://www.johndcook.com/blog/?p=247492</guid>

					<description><![CDATA[<p>I was digging into the Runge-Kutta method for solving differential equations and a line from [1] piqued my curiosity. These calculations, which are not reproduced in Kutta&#8217;s paper (they are however in Huen (1900)), are very tedious. The calculations are a set of eight constraints that the parameters of a fourth-order Runge-Kutta method must satisfy. [&#8230;]</p>
The post <a href="https://www.johndcook.com/blog/2026/07/31/runge-kutta-design/">Solving the RK4 design equations</a> first appeared on <a href="https://www.johndcook.com/blog">John D. Cook</a>.]]></description>
										<content:encoded><![CDATA[<p>I was digging into the Runge-Kutta method for solving differential equations and a line from [1] piqued my curiosity.</p>
<p style="padding-left: 40px;">These calculations, which are not reproduced in Kutta&#8217;s paper (they are however in Huen (1900)), are very tedious.</p>
<p>The calculations are a set of eight constraints that the parameters of a fourth-order Runge-Kutta method must satisfy. I wondered how well Mathematica might have done at assisting Mr. Huen in his &#8220;very tedious&#8221; calculations if it had been available in 1900.</p>
<p>I go into Runge-Kutta methods in <a href="https://www.johndcook.com/blog/2020/02/13/runge-kutta-methods/">this post</a>. Here I&#8217;d like to concentrate on a step in the design of the methods, namely solving the set of equations alluded in the quote above.</p>
<p><img loading="lazy" decoding="async" class="aligncenter" style="background-color: white;" src="https://www.johndcook.com/RK4.svg" alt="\begin{align*} b_1 + b_2 + b_3 + b_4 &amp;= 1 \\ b_2 c_2 + b_3 c_3 + b_4 c_4 &amp;= \frac{1}{2} \\ b_2 c_2^2 + b_3 c_3^2 + b_4 c_4^2 &amp;= \frac{1}{3} \\ b_3 a_{32} c_2 + b_4(a_{42} c_2 + a_{43} c_3) &amp;= \frac{1}{6} \\ b_2 c_2^3 + b_3 c_3^3 + b_4 c_4^3 &amp;= \frac{1}{4} \\ b_3 c_3 a_{32} c_2 + b_4 c_4(a_{42} c_2 + a_{43} c_3) &amp;= \frac{1}{8} \\ b_3 a_{32} c_2^2 + b_4(a_{42} c_2^2 + a_{43} c_3^2) &amp;= \frac{1}{12} \\ b_4 a_{43} a_{32} c_2 &amp;= \frac{1}{24} \end{align*} " width="297" height="353" /></p>
<p>The first thing to note is that there are 10 variables and only 8 equations, and so the solution is not fully determined. What we think of as <em>the</em> fourth order Runge-Kutta method is in fact <em>a</em> fourth order Runge-Kutta method.</p>
<p>One could argue that we should have <em>b</em><sub>2</sub> = <em>b</em><sub>3</sub> and <em>c</em><sub>2</sub> = <em>c</em><sub>3</sub>. With these additional equations, the system of equations has a unique solution, and Mathematic finds it easily.</p>
<pre>eqs = {
    b1 + b2 + b3 + b4 == 1,
    b2*c2 + b3*c3 + b4*c4 == 1/2,
    b2*c2^2 + b3*c3^2 + b4*c4^2 == 1/3,
    b3*a32*c2 + b4*(a42*c2 + a43*c3) == 1/6,
    b2*c2^3 + b3*c3^3 + b4*c4^3 == 1/4,
    b3*c3*a32*c2 + b4*c4*(a42*c2 + a43*c3) == 1/8,
    b3*a32*c2^2 + b4*(a42*c2^2 + a43*c3^2) == 1/12,
    b4*a43*a32*c2 == 1/24,
    b2 == b3,
    c2 == c3
};

vars = {b1, b2, b3, b4, c2, c3, c4, a32, a42, a43};

solution = Solve[eqs, vars]
</pre>
<p>This returns the parameters used for the version of Runge-Kutta presented in every textbook.</p>
<p>If you keep the requirement <em>b</em><sub>2</sub> = <em>b</em><sub>3</sub> but substitute the requirement 2<em>c</em><sub>2</sub> = <em>c</em><sub>3</sub> for <em>c</em>&#8216;s Mathematica will return the coefficients for the so-called Runge-Kutta 3/8 rule. This method has some slight advantages by some criteria.</p>
<p>In 1951 Gill [2] discovered a fourth order Runge-Kutta rule optimized for running in extremely constrained computer hardware. It&#8217;s a strange method, with irrational parameters, but one that was a very clever response to the limitations of its time.</p>
<p><strong>Update</strong>: See <a href="https://www.johndcook.com/blog/2026/08/01/counting-rooted-trees/">this post</a> for a discussion of the parameters and constriants for higher-ordered RK methods.</p>
<h2>Related posts</h2>
<ul>
<li class="link"><a href="https://www.johndcook.com/blog/2020/02/19/dormand-prince/">Dormand and Prince</a></li>
<li class="link"><a href="https://www.johndcook.com/blog/2016/06/02/ode-solver-as-a-functional-fold/">RK4 as a fold</a></li>
<li class="link"><a href="https://www.johndcook.com/blog/2020/02/02/stiff-differential-equations/">Stiff differential equations</a></li>
</ul>
<p>[1] Hairer, Nørsett, and Wanner. Solving Ordinary Differential Equations I: Nonstiff Problems. Springer-Verlag 1987.</p>
<p>[2] A. Gill. A process for the step-by-step integration of differential equations in an automatic digital computing machine. Proc. Cambridge Philos. Soc., vol 27, pp 95–108.</p>The post <a href="https://www.johndcook.com/blog/2026/07/31/runge-kutta-design/">Solving the RK4 design equations</a> first appeared on <a href="https://www.johndcook.com/blog">John D. Cook</a>.]]></content:encoded>
					
					<wfw:commentRss>https://www.johndcook.com/blog/2026/07/31/runge-kutta-design/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Inverse factorial improved</title>
		<link>https://www.johndcook.com/blog/2026/07/28/inverse-factorial-improved/</link>
					<comments>https://www.johndcook.com/blog/2026/07/28/inverse-factorial-improved/#comments</comments>
		
		<dc:creator><![CDATA[John]]></dc:creator>
		<pubDate>Tue, 28 Jul 2026 14:11:30 +0000</pubDate>
				<category><![CDATA[Math]]></category>
		<category><![CDATA[Special functions]]></category>
		<guid isPermaLink="false">https://www.johndcook.com/blog/?p=247470</guid>

					<description><![CDATA[<p>A couple years ago I wrote about how to compute the inverse of factorial. I used that code in writing the previous post because the post required solving the equation ⌊log2(n!)⌋ ≥ b given b. That is, given a number of bits b, find the smallest value of n such that n! ≥ 2b. What the [&#8230;]</p>
The post <a href="https://www.johndcook.com/blog/2026/07/28/inverse-factorial-improved/">Inverse factorial improved</a> first appeared on <a href="https://www.johndcook.com/blog">John D. Cook</a>.]]></description>
										<content:encoded><![CDATA[<p><a href="https://www.johndcook.com/blog/2024/01/01/inverse-factorial/">A couple years ago</a> I wrote about how to compute the inverse of factorial. I used that code in writing the previous post because the post required solving the equation</p>
<p style="padding-left: 40px;">⌊log<sub>2</sub>(<em>n</em>!)⌋ ≥ <em>b</em></p>
<p>given <em>b</em>. That is, given a number of bits <em>b</em>, find the smallest value of <em>n</em> such that <em>n</em>! ≥ 2<sup><em>b</em></sup>.</p>
<h2>What the code got right</h2>
<p>Looking back on the code in that post, there are a few changes I&#8217;d like to make. But first of all, I&#8217;d like to point out something the post does right: instead of trying to solve</p>
<p style="padding-left: 40px;">Γ(<em>y</em>) = <em>x</em></p>
<p>it solves</p>
<p style="padding-left: 40px;">log Γ(<em>y</em>) = log <em>x</em>.</p>
<p>That&#8217;s why the argument to <code>inverse_log_gamma</code> is <code>logarg</code>. That makes the code useful for values of <em>x</em> that would far exceed the maximum floating point value, such as in the calculations for the previous post.</p>
<h2>What I&#8217;d change</h2>
<h3>Rounding</h3>
<p>The function <code>inverse_factorial</code> from the old post solves finds the closest integer solution. It would be better for it to return the solution without rounding and then let the user round result if they want to. In my calculations in the previous post, I wanted to take the floor, not round.</p>
<h3>Newton&#8217;s method</h3>
<p>The code in the previous post uses the bisection method. This method is very safe, and fast enough for my purposes, but it could be made faster. Newton&#8217;s method is faster, but it can be ill-behaved if you don&#8217;t start close enough to the solution.</p>
<p>It&#8217;s safe to use Newton&#8217;s method to invert log Γ for two reasons. First, you can get a good starting point based on Stirling&#8217;s approximation. Second, and more importantly, log Γ is convex. Newton&#8217;s method will converge from <em>any</em> starting point when applied to a convex function. A little caution is necessary because log Γ is not convex everywhere, but it is convex on the positive real axis.</p>
<p>Another difficulty with Newton&#8217;s method is that you need to supply the derivative of the function whose root you&#8217;re trying to find. But this isn&#8217;t an issue here because the derivative of log Γ is the digamma function, which is implemented in SciPy.</p>
<h3>Tolerance</h3>
<p>Finally, the previous code used the default tolerance for deciding when to stop refining the solution. The revised method lets the user specify tolerance. It provides a default value, but that default is visible in the function call, not hidden down in SciPy.</p>
<h2>Revised code</h2>
<p>Here&#8217;s the revised code.</p>
<pre>from scipy.special import gammaln, digamma
from scipy.optimize import newton

def inverse_log_gamma(logarg, tol=1e-12):
    assert(logarg &gt; 0)    
    x0 = logarg / log(logarg + 1) + 1 if logarg &gt; 1 else 2.0
    def f(z): return gammaln(z) - logarg
    return newton(f, x0, fprime=digamma, tol=tol)

def inverse_factorial(logarg):
    g = inverse_log_gamma(logarg)
    return g - 1 
</pre>The post <a href="https://www.johndcook.com/blog/2026/07/28/inverse-factorial-improved/">Inverse factorial improved</a> first appeared on <a href="https://www.johndcook.com/blog">John D. Cook</a>.]]></content:encoded>
					
					<wfw:commentRss>https://www.johndcook.com/blog/2026/07/28/inverse-factorial-improved/feed/</wfw:commentRss>
			<slash:comments>1</slash:comments>
		
		
			</item>
		<item>
		<title>Cryptographic Keys and Decks of Cards</title>
		<link>https://www.johndcook.com/blog/2026/07/28/keys-and-cards/</link>
					<comments>https://www.johndcook.com/blog/2026/07/28/keys-and-cards/#comments</comments>
		
		<dc:creator><![CDATA[John]]></dc:creator>
		<pubDate>Tue, 28 Jul 2026 12:13:17 +0000</pubDate>
				<category><![CDATA[Math]]></category>
		<category><![CDATA[Combinatorics]]></category>
		<category><![CDATA[Cryptography]]></category>
		<guid isPermaLink="false">https://www.johndcook.com/blog/?p=247468</guid>

					<description><![CDATA[<p>The previous post looked at the idea of storing a cryptographic key in the order of a deck of cards. A deck of 52 cards can store 225 bits of data because ⌊log2(52!)⌋ = 225. Here ⌊x⌋ is x rounded down to the nearest integer. If we want to store bigger keys, we&#8217;re going to need [&#8230;]</p>
The post <a href="https://www.johndcook.com/blog/2026/07/28/keys-and-cards/">Cryptographic Keys and Decks of Cards</a> first appeared on <a href="https://www.johndcook.com/blog">John D. Cook</a>.]]></description>
										<content:encoded><![CDATA[<p>The <a href="https://www.johndcook.com/blog/2026/07/27/hiding-data-in-permutations/">previous post</a> looked at the idea of storing a cryptographic key in the order of a deck of cards. A deck of 52 cards can store 225 bits of data because</p>
<p style="padding-left: 40px;">⌊log<sub>2</sub>(52!)⌋ = 225.</p>
<p>Here ⌊<em>x</em>⌋ is <em>x</em> rounded down to the nearest integer.</p>
<p>If we want to store bigger keys, we&#8217;re going to need a bigger deck of cards.</p>
<h2>Bitcoin</h2>
<p>A Bitcoin key has 256 bits, which would require a deck of 58 cards. There is a card game called Zwicker that uses a deck of 58 cards, the usual 52 cards plus six jokers. So you could store a Bitcoin key in the permutation of a Zwicker deck.</p>
<p>You could also use a deck of 52 cards, plus 2 jokers, if you also consider orientation. 30 cards are rotationally symmetric, 22 are not, and neither are jokers. So, including two asymmetric jokers, you could add 24 additional bits. Permutations of a 54 card deck can encode 237 bits, and with 24 orientation bits, this is a total of 261 bits.</p>
<h2>RSA</h2>
<p>RSA key sizes vary, but 2048 and 3072 are common. A 2048-bit key would require a deck of 301 cards. Casinos often use a shoe of 312 cards, combining six decks of 52 cards, to deal Baccarat or Blackjack. However, casinos combine identical decks. If you were to combine six unique decks, you could store a 2048-bit key.</p>
<p>Storing a 3072-bit key would require a deck of 422 cards. You could make a deck of 432 cards by combining 8 distinguishable packs of 54 cards (52 + 2 jokers).</p>
<h2>ML-KEM</h2>
<p>ML-KEM is a proposed quantum-resistant replacement for RSA. As with RSA, key sizes for ML-KEM vary, the smallest being ML-<strong>KEM-512</strong> with a key size of 1632 bytes, which equals 13056 bits. This would require a deck of 1442 cards. You could combine 28 distinct packs of 52 cards, but that&#8217;s unwieldy.</p>
<p>This illustrates one of the difficult trade-offs with post-quantum cryptography: key sizes are much bigger. If you wanted to create a deck of 1442 cards, you&#8217;d probably want to make your &#8220;cards&#8221; something other than standard playing cards. You&#8217;d want to use permutations of something else.</p>
<h2>Verification</h2>
<p>The following Python code verifies the calculations above.</p>
<pre>from math import log2, factorial, floor

def capacity(cards):
    return floor(log2(factorial(cards)))

def verify(bits, cards):
    return capacity(cards) &gt;= bits and capacity(cards-1) &lt; bits

print(verify(237, 54))
print(verify(256, 58))
print(verify(2048, 301))
print(verify(3072, 422))
print(verify(1632*8, 1442))
</pre>
<p>For more on how I came up with the deck sizes, see the <a href="https://www.johndcook.com/blog/2026/07/28/inverse-factorial-improved/">next post</a> on computing the inverse factorial.</p>The post <a href="https://www.johndcook.com/blog/2026/07/28/keys-and-cards/">Cryptographic Keys and Decks of Cards</a> first appeared on <a href="https://www.johndcook.com/blog">John D. Cook</a>.]]></content:encoded>
					
					<wfw:commentRss>https://www.johndcook.com/blog/2026/07/28/keys-and-cards/feed/</wfw:commentRss>
			<slash:comments>2</slash:comments>
		
		
			</item>
		<item>
		<title>Hiding data in permutations</title>
		<link>https://www.johndcook.com/blog/2026/07/27/hiding-data-in-permutations/</link>
		
		<dc:creator><![CDATA[John]]></dc:creator>
		<pubDate>Mon, 27 Jul 2026 23:08:59 +0000</pubDate>
				<category><![CDATA[Computing]]></category>
		<category><![CDATA[Combinatorics]]></category>
		<category><![CDATA[Cryptography]]></category>
		<category><![CDATA[SymPy]]></category>
		<guid isPermaLink="false">https://www.johndcook.com/blog/?p=247462</guid>

					<description><![CDATA[<p>The latest issue of Paged Out! has an article by Stephen Hewitt &#8220;An off-line backup of your cryptographic key using playing cards.&#8221; The idea is to use a deck of 52 to store a 128-bit cryptographic key. To erase the key, shuffle the deck. Hewitt gives his algorithm for embedding a key, one that can [&#8230;]</p>
The post <a href="https://www.johndcook.com/blog/2026/07/27/hiding-data-in-permutations/">Hiding data in permutations</a> first appeared on <a href="https://www.johndcook.com/blog">John D. Cook</a>.]]></description>
										<content:encoded><![CDATA[<p>The <a href="https://pagedout.institute/download/PagedOut_009.pdf">latest issue</a> of Paged Out! has an article by Stephen Hewitt &#8220;An off-line backup of your cryptographic key using playing cards.&#8221; The idea is to use a deck of 52 to store a 128-bit cryptographic key. To erase the key, shuffle the deck. Hewitt gives his algorithm for embedding a key, one that can be carried out manually but isn&#8217;t maximally efficient.</p>
<p>You could store a 225-bit key as a permutation of 52 cards because</p>
<p style="padding-left: 40px;">log<sub>2</sub>(52!) = 225.581.</p>
<p>But then how would you number permutations so you could go from a number to a particular permutation and later decode the permutation to a number? Is this even practical? For a small number <em>n</em>, you could encode a number <em>k</em> &lt; <em>n</em> by enumerating the first <em>k</em> permutations of a set of <em>n</em> items, and you could decode by enumerating permutations until you find the one you have. But this is completely impractical for large <em>n</em>, such as <em>n</em> = 52.</p>
<p>The process of mapping permutation to an integer is called <b>ranking</b>, and the mapping from an integer to a permutation is called <b>unranking</b>. How efficiently can rankings and unrankings be calculated?</p>
<p>Let <em>n</em> be the number of symbols being permuted. Then there are simple algorithms for ranking and unranking with respect to lexicographical order that have complexity <em>O</em>(<em>n</em>²) and more sophisticated algorithms that have complexity <em>O</em>(<em>n</em> log <em>n</em>). There are also <em>O</em>(<em>n</em>) algorithms that do not preserve lexicographical order.</p>
<p>The <code>Permutations</code> class in SymPy has methods <code>unrank_lex</code> and <code>rank</code> to unrank and rank permutations according to lexicographical order.</p>
<p>The notation the <code>Permutations</code> class uses requires a little explanation. For example, suppose we unrank 2026.</p>
<pre>&gt;&gt;&gt; from sympy.combinatorics import Permutation
&gt;&gt;&gt; Permutation.unrank_lex(52, 2026)
Permutation(45, 47, 51, 48, 46, 50)
</pre>
<p>The output is not a full list of 52 numbers in permuted order; it is only a cycle. The notation refers to the permutation that sends 45 to 47, 47 to 51, …, 50 to 45 and leaves everything else fixed.</p>
<p>If we rank the permutation given above, we get 2026 back.</p>
<pre>&gt;&gt;&gt; Permutation.rank(Permutation(45, 47, 51, 48, 46, 50))
2026
</pre>
<p>Note that we didn&#8217;t say how many elements (45, 47, 51, 48, 46, 50) is a permutation of. Because of lexicographical order, the rank would be the same whether we viewed this as a permutation of 52 objects or of more objects.</p>
<p>Now let&#8217;s do something larger. Let&#8217;s generate a 220-bit number and encode it as a permutation.</p>
<pre>&gt;&gt;&gt; n = random.getrandbits(225)
&gt;&gt;&gt; a = Permutation.unrank_lex(52, n)
&gt;&gt;&gt; n
40234719030664563684489051530416964877785781669439875437823431388841
&gt;&gt;&gt; a
Permutation(0, 25, 32, 15, 8, 28)(1, 48, 34, 14, 10, 51, 38, 31, 21, 5, 42, 47, 29, 26, 46, 30, 50, 49, 37, 22, 18, 23)(2, 45, 17, 20, 36, 40, 11, 4, 7, 41, 33, 3, 43, 44, 19, 16, 35, 39, 12, 6, 9)
&gt;&gt;&gt; Permutation.rank(a) == n
True
</pre>
<p>Now just for fun, let&#8217;s display the permutation above applied to a standard (French) deck of 52 cards. As explained <a href="https://www.johndcook.com/blog/2024/04/30/a-deck-of-cards/">here</a>, symbols associated with these cards have a range of Unicode values. By printing these values, we can visualize the permuted deck.</p>
<p><img loading="lazy" decoding="async" class="aligncenter size-medium" src="https://www.johndcook.com/shuffled_deck.png" width="720" height="366" /></p>
<p>Here&#8217;s the code that made the image above.</p>
<pre>spades = list(range(0x1F0A1, 0x1F0AF))
spades.remove(0x1F0AC) # take out the knight
cards = [s + 16*i for s in spades for i in range(4)]

a = Permutation.unrank_lex(52, n)
p = a(cards)

for i in range(4):
    for j in range(13):
        print(chr(p[13*i + j]), end="")
    print()
</pre>
<p>The code above is plenty fast, but Permutation has methods <code>rank_nonlex</code> and <code>unrank_nonlex</code> that run in <em>O</em>(<em>n</em>) time, which could be useful for <em>n</em> much larger than 52.</p>The post <a href="https://www.johndcook.com/blog/2026/07/27/hiding-data-in-permutations/">Hiding data in permutations</a> first appeared on <a href="https://www.johndcook.com/blog">John D. Cook</a>.]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>Counting permutations with roots</title>
		<link>https://www.johndcook.com/blog/2026/07/27/counting-permutations-with-roots/</link>
		
		<dc:creator><![CDATA[John]]></dc:creator>
		<pubDate>Mon, 27 Jul 2026 16:03:44 +0000</pubDate>
				<category><![CDATA[Math]]></category>
		<category><![CDATA[Combinatorics]]></category>
		<category><![CDATA[Mathematica]]></category>
		<category><![CDATA[SymPy]]></category>
		<guid isPermaLink="false">https://www.johndcook.com/blog/?p=247457</guid>

					<description><![CDATA[<p>My post from yesterday on permutation roots ends with a Mathematica code for finding the probability that a permutation of n elements has a kth root. This is done by finding the coefficient of xn in the generating function I wanted to say more about this, and look at implementing the same code in SymPy. [&#8230;]</p>
The post <a href="https://www.johndcook.com/blog/2026/07/27/counting-permutations-with-roots/">Counting permutations with roots</a> first appeared on <a href="https://www.johndcook.com/blog">John D. Cook</a>.]]></description>
										<content:encoded><![CDATA[<p>My post from <a href="https://www.johndcook.com/blog/2026/07/26/permutation-roots/">yesterday</a> on permutation roots ends with a Mathematica code for finding the probability that a permutation of <em>n</em> elements has a <em>k</em>th root. This is done by finding the coefficient of <em>x</em><sup><em>n</em></sup> in the generating function</p>
<p><img loading="lazy" decoding="async" class="aligncenter size-medium" src="https://www.johndcook.com/expq11.svg" alt="\prod_{m=1}^\infty \exp_{\text{gcd}(m, k)} \left\frac{x^m}{m}\right)" width="162" height="54" /></p>
<p>I wanted to say more about this, and look at implementing the same code in SymPy. I was curious how well SymPy would do because I&#8217;ve noticed that LLMs often generate SymPy code since it&#8217;s an open source <abbr title="computer algebra system">CAS</abbr>.</p>
<p>Wilf [1] describes the infinite product above as the exponential generating function (egf) of <em>f</em>(<em>n</em>, <em>k</em>), the number of permutations of <em>n</em> objects that have a <em>k</em>th root. Since egfs have a <em>n</em>! term in the denominator, this is also the ordinary generating function (ogf) of the <em>probability</em> that a randomly chosen permutation on <em>n</em> objects has a <em>k</em>th root.</p>
<p>My first attempt at using Mathematica to probe the generating function was</p>
<pre>expq[x_, q_] := MittagLefflerE[q, x^q]	 
p[n_, k_] :=  SeriesCoefficient[	 
    Product[expq[x^m/m, GCD[m, k]], {m, 1, Infinity}], {x, 0, n}]
</pre>
<p>This hung forever when I tried to use it on a small example. I realized, but apparently Mathematica did not, that <code>Infinity</code> could be replaced by <code>n</code> since terms higher than <em>n</em> do not contribute to the coefficient of <em>x</em><sup><em>n</em></sup>. With that change, the code ran quickly.</p>
<p>This morning I tried converting the Mathematica code to Sympy; Claude did this in one shot. I also reproduced the table of <em>f</em>(<em>n</em>, <em>k</em>) values on page 150 of [1] to test the code. Since Wilf tabulated <em>f</em>(<em>n</em>, <em>k</em>), not <em>f</em>(<em>n</em>, <em>k</em>)/<em>n</em>!, I multiplied the results by <em>n</em>!.</p>
<p>Here is the output:</p>
<pre>k = 2 [1, 1, 3, 12, 60, 270, 1890, 14280, 128520, 1096200]
k = 3 [1, 2, 4, 16, 80, 400, 2800, 22400, 181440, 1814400]
k = 4 [1, 1, 3, 12, 60, 270, 1890, 13020, 117180, 1039500]
k = 5 [1, 2, 6, 24, 96, 576, 4032, 32256, 290304, 2612736]
k = 6 [1, 1, 1, 4, 40, 190, 1330, 8680, 52920, 340200]
k = 7 [1, 2, 6, 24, 120, 720, 4320, 34560, 311040, 3110400]
</pre>
<p>and here is the SymPy code. I edited the main but the rest is verbatim from Claude.</p>
<pre>from sympy import symbols, gcd, factorial, Rational, S

x = symbols('x')

def expq_coeffs(m, q, n):
    """
    Truncated (degree &lt;= n) series coefficients of
        expq(x**m/m, q) = MittagLefflerE(q, (x**m/m)**q)
    Since q is a positive integer:
        E_q(y^q) = sum_j y^(q*j) / (q*j)!
    with y = x**m/m, so the term of degree m*q*j has coefficient
        1 / ( m**(q*j) * (q*j)! ).
    Returns a list c[0..n] of coefficients.
    """
    c = [S.Zero] * (n + 1)
    j = 0
    while m * q * j &lt;= n:
        deg = m * q * j
        c[deg] += Rational(1, m**(q * j) * factorial(q * j))
        j += 1
    return c

def poly_mult_trunc(a, b, n):
    """Multiply two series (lists of coeffs, index = degree) truncated to degree n."""
    c = [S.Zero] * (n + 1)
    for i, ai in enumerate(a):
        if ai == 0:
            continue
        max_j = n - i
        for j2 in range(max_j + 1):
            bj = b[j2]
            if bj != 0:
                c[i + j2] += ai * bj
    return c

def p(n, k):
    """
    SymPy equivalent of:
        expq[x_, q_] := MittagLefflerE[q, x^q]
        p[n_, k_] := SeriesCoefficient[
            Product[expq[x^m/m, GCD[m, k]], {m, 1, n}], {x, 0, n}]
    """
    result = [S.Zero] * (n + 1)
    result[0] = S.One
    for m in range(1, n + 1):
        q = gcd(m, k)
        factor = expq_coeffs(m, q, n)
        result = poly_mult_trunc(result, factor, n)
    return result[n]

# example
if __name__ == "__main__":
    for k in range(2, 8):
        print("k =", k, [factorial(n)*p(n, k) for n in range(1,11)])
</pre>
<p>[1] Herbert Wilf. Generatingfunctionology. Available online <a href="https://www2.math.upenn.edu/~wilf/DownldGF.html">here</a>.</p>The post <a href="https://www.johndcook.com/blog/2026/07/27/counting-permutations-with-roots/">Counting permutations with roots</a> first appeared on <a href="https://www.johndcook.com/blog">John D. Cook</a>.]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>Printing floating point numbers in binary</title>
		<link>https://www.johndcook.com/blog/2026/07/27/float-binary/</link>
					<comments>https://www.johndcook.com/blog/2026/07/27/float-binary/#comments</comments>
		
		<dc:creator><![CDATA[John]]></dc:creator>
		<pubDate>Mon, 27 Jul 2026 15:34:05 +0000</pubDate>
				<category><![CDATA[Computing]]></category>
		<category><![CDATA[Math]]></category>
		<category><![CDATA[Number systems]]></category>
		<guid isPermaLink="false">https://www.johndcook.com/blog/?p=247449</guid>

					<description><![CDATA[<p>It&#8217;s well known that you can convert the base 16 (hex) representation of an integer to the base 2 (binary) representation by simply converting each digit from hex to binary. For example, CAFEhex = 1100 1010 1111 1110two I imagine it&#8217;s less well known that you can do the same thing with floating point numbers. [&#8230;]</p>
The post <a href="https://www.johndcook.com/blog/2026/07/27/float-binary/">Printing floating point numbers in binary</a> first appeared on <a href="https://www.johndcook.com/blog">John D. Cook</a>.]]></description>
										<content:encoded><![CDATA[<p>It&#8217;s well known that you can convert the base 16 (hex) representation of an integer to the base 2 (binary) representation by simply converting each digit from hex to binary. For example,</p>
<p style="padding-left: 40px;">CAFE<sub>hex</sub> = 1100 1010 1111 1110<sub>two</sub></p>
<p>I imagine it&#8217;s less well known that you can do the same thing with floating point numbers.</p>
<p>I wanted to find the binary representation of a floating point number using Python, and discovered that it has no function to do this. However, there is a method on floats to show a hex representation. For example, here&#8217;s the hex representation of π.</p>
<pre>&gt;&gt;&gt; import math
&gt;&gt;&gt; (math.pi).hex()
'0x1.921fb54442d18p+1'
</pre>
<p>Curiously, the p+<em>k</em> part at the end is an exponent of 2, not an exponent of 16. So after we convert 1.921fb54442d18 to binary, we&#8217;ll need to multiply by 2, i.e. move the fractional point one space to the right.</p>
<p>So first we convert 1.921fb54442d18<sub>hex</sub> to binary by converting 1, 9, 2, etc. each to binary.</p>
<p style="padding-left: 40px;">1.1001 0010 0001 1111 1011 0101 0100 0100 0100 0010 1101 0001 1000<sub>two</sub></p>
<p>Then after shifting the fraction point to account for the <code>p+1</code> part we have</p>
<p style="padding-left: 40px;">π = 11.001001000011111101101010100010001000010110100011000<sub>two</sub></p>
<p>You could use Python&#8217;s bin() function to convert the fractional part, interpreted as an integer, to hex, though you may need to pad with 0 bits. For example,</p>
<pre>&gt;&gt;&gt; (1.03).hex()
'0x1.07ae147ae147bp+0

&gt;&gt;&gt; bin(0x7ae147ae147)
'0b1111010111000010100011110101110000101000111'
</pre>
<p>The binary representation of 1.03<sub>ten</sub> is</p>
<p style="padding-left: 40px;">1.000001111010111000010100011110101110000101000111<sub>two</sub></p>
<p>We added a total of five zero bits, four for the 0 after the fractional point and one for converting 7 to 0111<sub>two</sub>.</p>The post <a href="https://www.johndcook.com/blog/2026/07/27/float-binary/">Printing floating point numbers in binary</a> first appeared on <a href="https://www.johndcook.com/blog">John D. Cook</a>.]]></content:encoded>
					
					<wfw:commentRss>https://www.johndcook.com/blog/2026/07/27/float-binary/feed/</wfw:commentRss>
			<slash:comments>1</slash:comments>
		
		
			</item>
		<item>
		<title>Permutation roots</title>
		<link>https://www.johndcook.com/blog/2026/07/26/permutation-roots/</link>
		
		<dc:creator><![CDATA[John]]></dc:creator>
		<pubDate>Sun, 26 Jul 2026 20:32:02 +0000</pubDate>
				<category><![CDATA[Uncategorized]]></category>
		<guid isPermaLink="false">https://www.johndcook.com/blog/?p=247440</guid>

					<description><![CDATA[<p>Let σ be a permutation on n elements. If there is a permutation τ such that applying τ twice has the same effect on the list of elements as applying σ once, we say σ = τ² and τ is a square root of σ. If we let our n elements be the integers 0 [&#8230;]</p>
The post <a href="https://www.johndcook.com/blog/2026/07/26/permutation-roots/">Permutation roots</a> first appeared on <a href="https://www.johndcook.com/blog">John D. Cook</a>.]]></description>
										<content:encoded><![CDATA[<p>Let σ be a permutation on <em>n</em> elements. If there is a permutation τ such that applying τ twice has the same effect on the list of elements as applying σ once, we say σ = τ² and τ is a square root of σ.</p>
<p>If we let our <em>n</em> elements be the integers 0 through <em>n</em> − 1, then we can represent permutations by what they do to this list of numbers. In Python as a tuple of length <em>n</em> and compose permutations with the following function:</p>
<pre>import itertools

def compose(sigma, tau):
    "Return the composition σ ∘ τ (apply τ first, then σ)."
    return tuple(sigma[j] for j in tau)
</pre>
<p>We can always construct permutations that have square roots by squaring a permutation. If we run the following code</p>
<pre>tau = (3, 1, 4, 5, 2, 0)
sigma = compose(tau, tau)
</pre>
<p>we find σ = (5, 1, 2, 0, 4, 3), and by construction (3, 1, 4, 5, 2, 0) is a square root of &amp;sigma, though it&#8217;s not the only one.</p>
<p>The following code shows that σ has four roots.</p>
<pre>import itertools

def numroots(sigma):
    n = len(sigma)
    c = 0
    for tau in itertools.permutations(range(n)):
        if sigma == compose(tau, tau):
            c += 1
    return c

print( numroots(sigma) )
print( numroots( (1, 2, 3, 4, 5, 0) ) )
</pre>
<p>It also shows that the rotation (1, 2, 3, 4, 5. 0) has no roots.</p>
<p>The function <code>numroots</code> has runtime proportional to <em>n</em>! and so it&#8217;s not practical for large permutations. There is a theorem that says a permutation σ has a square root if and only if the number of cycles it has of every even length is even. See [1].</p>
<p>We can also define cubes and cube roots of permutations, and higher powers and roots.</p>
<p>How common is it for permutations to have square roots, or cube roots, etc.? If you pick a random permutation on <em>n</em> elements, what is the probability that it has a <em>k</em>th root?</p>
<p>This is a hard question in general, but it is equivalent to finding the coefficient of <em>x</em><sup><em>k</em></sup> in the infinite product</p>
<p><img loading="lazy" decoding="async" class="aligncenter size-medium" src="https://www.johndcook.com/expq11.svg" alt="\prod_{m=1}^\infty \exp_{\text{gcd}(m, k)} \left(\frac{x^m}{m}\right)" width="162" height="54" /></p>
<p>This is theorem 4.8.3 in [1]. This theorem was the motivation for writing about exp<sub><em>q</em></sub> in the <a href="https://www.johndcook.com/blog/2026/07/26/exp-q/">previous post</a>.</p>
<p>Although the product is infinite, there&#8217;s no need to compute terms in the product that only contribute powers of <em>x</em> higher than you&#8217;re interested in. The following Mathematica code will compute the probability that a permutation on <em>n</em> elements has a <em>k</em>th root.</p>
<pre>expq[x_, q_] := MittagLefflerE[q, x^q]	 
p[n_, k_] :=  SeriesCoefficient[	 
    Product[expq[x^m/m, GCD[m, k]], {m, 1, n}], {x, 0, n}]
</pre>
<p>So, for example, the probability that a permutation of 10 elements has a square root is 29/96.</p>
<p>[1] Herbert Wilf. Generatingfunctionology. Available online <a href="https://www2.math.upenn.edu/~wilf/DownldGF.html">here</a>.</p>The post <a href="https://www.johndcook.com/blog/2026/07/26/permutation-roots/">Permutation roots</a> first appeared on <a href="https://www.johndcook.com/blog">John D. Cook</a>.]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>exp_q</title>
		<link>https://www.johndcook.com/blog/2026/07/26/exp-q/</link>
		
		<dc:creator><![CDATA[John]]></dc:creator>
		<pubDate>Sun, 26 Jul 2026 18:20:36 +0000</pubDate>
				<category><![CDATA[Math]]></category>
		<category><![CDATA[Differential equations]]></category>
		<category><![CDATA[Special functions]]></category>
		<guid isPermaLink="false">https://www.johndcook.com/blog/?p=247432</guid>

					<description><![CDATA[<p>The function expq(x) is defined by taking the power series for exp(x) and keeping only the terms whose index is a multiple of q. For example, exp2(x) keeps only the even-numbered terms in the exponential power series and so equals cosh(x). In general, The first sum uses Iverson&#8217;s bracket notation: a Boolean expression in brackets [&#8230;]</p>
The post <a href="https://www.johndcook.com/blog/2026/07/26/exp-q/">exp_q</a> first appeared on <a href="https://www.johndcook.com/blog">John D. Cook</a>.]]></description>
										<content:encoded><![CDATA[<p>The function exp<sub><em>q</em></sub>(<em>x</em>) is defined by taking the power series for exp(<em>x</em>) and keeping only the terms whose index is a multiple of <em>q</em>. For example, exp<sub>2</sub>(<em>x</em>) keeps only the even-numbered terms in the exponential power series and so equals cosh(<em>x</em>).</p>
<p><img loading="lazy" decoding="async" class="aligncenter" style="background-color: white;" src="https://www.johndcook.com/expqx.svg" alt="\exp_2(x) = 1 + \frac{x^2}{2!} + \frac{x^4}{4!} + \frac{x^6}{6!} + \cdots = \cosh(x)" width="359" height="44" /></p>
<p>In general,</p>
<p><img loading="lazy" decoding="async" class="aligncenter" style="background-color: white;" src="https://www.johndcook.com/expq2.svg" alt="\exp_q(x) = \sum_{n=0}^\infty [q \mid n] \frac{x^n}{n!} = \sum_{n=0}^\infty \frac{x^{nq}}{(nq)!}" width="285" height="54" /></p>
<p>The first sum uses <a href="https://www.johndcook.com/blog/2023/07/01/activation-functions/">Iverson&#8217;s bracket notation</a>: a Boolean expression in brackets denotes the function that returns 1 when the expression is true and zero when it is false. Here the bracket equals 1 when <em>q</em> divides <em>n</em> and is zero otherwise.</p>
<h2>Closed forms</h2>
<p>Let ω = exp(2π<em>i</em> / <em>q</em>). Then</p>
<p><img loading="lazy" decoding="async" class="aligncenter" style="background-color: white;" src="https://www.johndcook.com/expq3.svg" alt="\exp_q(x) = \frac{1}{q}\sum_{k=0}^{q-1} \exp(\omega^k x)" width="209" height="60" /></p>
<p>This lets us find closed-form expressions for exp<sub><em>q</em></sub>(<em>x</em>). For example, when <em>q</em> = 4, ω = <em>i</em> and</p>
<p><img loading="lazy" decoding="async" class="aligncenter" style="background-color: white;" src="https://www.johndcook.com/expq4.svg" alt="\exp_4(x) = \frac{1}{2}\left( \cosh(x) + \cos(x) \right)" width="257" height="40" /></p>
<p>Here&#8217;s a proof of the identity above:</p>
<p><img loading="lazy" decoding="async" class="aligncenter" style="background-color: white;" src="https://www.johndcook.com/expq5.svg" alt="\begin{align*} \frac{1}{q} \sum_{k=0}^{q-1} \exp(\omega^k x) &amp;= \frac{1}{q} \sum_{k=0}^{q-1} \sum_{n=0}^\infty \frac{\omega^{kn}x^n}{n!} \\ &amp;= \sum_{n=0}^\infty \left( \frac{1}{q} \sum_{k=0}^{q-1} \omega^{kn}\right) \frac{x^n}{n!} \\ &amp;= \sum_{n=0}^\infty [q \mid n] \frac{x^n}{n!} \\ &amp;= \exp_q(x) \end{align*} " width="296" height="230" /></p>
<p>In the proof we used the identity</p>
<p><img loading="lazy" decoding="async" class="aligncenter" style="background-color: white;" src="https://www.johndcook.com/expq6.svg" alt="\frac{1}{q} \sum_{k=0}^{q-1} \omega^{kn} = [q \mid n]" width="150" height="60" /></p>
<p>which is important in deriving the properties of the discrete Fourier transform.</p>
<h2>Differential equations</h2>
<p>The first time I saw the function exp<sub><em>q</em></sub>(<em>x</em>) was in differential equations, though I didn&#8217;t know at the time the function had a name.</p>
<p>When a course in differential equations gets to power series solutions, a common example or homework problem is to solve</p>
<p><img loading="lazy" decoding="async" class="aligncenter" style="background-color: white;" src="https://www.johndcook.com/expq7.svg" alt="y^{(k)}(x) = y(x)" width="117" height="23" /></p>
<p>for <em>k</em> = 3 or 4, i.e. to find a function that equals its third or fourth derivative.</p>
<p>If the initial conditions are</p>
<p><img loading="lazy" decoding="async" class="aligncenter" style="background-color: white;" src="https://www.johndcook.com/expq8.svg" alt="y(0) = 0" width="70" height="18" /></p>
<p>and</p>
<p><img loading="lazy" decoding="async" class="aligncenter" style="background-color: white;" src="https://www.johndcook.com/expq9.svg" alt="y^\prime(0) = y^{\prime\prime}(0) = \cdots = y^{(k-1)}(0) = 0" width="285" height="23" /></p>
<p>the unique solution to</p>
<p><img loading="lazy" decoding="async" class="aligncenter" style="background-color: white;" src="https://www.johndcook.com/expq7.svg" alt="y^{(k)}(x) = y(x)" width="117" height="23" /></p>
<p>is <em>y</em>(<em>x</em>) = exp<sub><em>k</em></sub>(<em>x</em>).</p>
<h2>Mathematica and Mittag-Leffler</h2>
<p>Mathematica does not have a built-in function implementing exp<sub><em>q</em></sub>(<em>x</em>), but it does have an implementation of the <a href="https://www.johndcook.com/blog/2016/07/17/mittag-leffler-function-and-probability-distribution/">Mittag-Leffler function</a>, and so thanks to a relation between this function and exp<sub><em>q</em></sub>(<em>x</em>) you can implement the latter as</p>
<pre>expq[x_, q_] := MittagLefflerE[q, x^q]</pre>
<h2>Combinatorics</h2>
<p>The first time I saw the <em>notation</em> exp<sub><em>q</em></sub>(<em>x</em>) was in combinatorics. I had intended to include an application from that book here, but I make that the topic for the <a href="https://www.johndcook.com/blog/2026/07/26/permutation-roots/">next post</a>.</p>The post <a href="https://www.johndcook.com/blog/2026/07/26/exp-q/">exp_q</a> first appeared on <a href="https://www.johndcook.com/blog">John D. Cook</a>.]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>Excel column numbering</title>
		<link>https://www.johndcook.com/blog/2026/07/25/excel-column-numbering/</link>
					<comments>https://www.johndcook.com/blog/2026/07/25/excel-column-numbering/#comments</comments>
		
		<dc:creator><![CDATA[John]]></dc:creator>
		<pubDate>Sat, 25 Jul 2026 13:37:30 +0000</pubDate>
				<category><![CDATA[Math]]></category>
		<category><![CDATA[Number systems]]></category>
		<guid isPermaLink="false">https://www.johndcook.com/blog/?p=247423</guid>

					<description><![CDATA[<p>I was working with a wide spreadsheet from a client the other day and I had to convert between Excel column labels and column numbers. I had never paid attention to how Excel labels columns and implicitly thought it was base 26 using letters rather than digits. But then I realized that&#8217;s not right. Excel [&#8230;]</p>
The post <a href="https://www.johndcook.com/blog/2026/07/25/excel-column-numbering/">Excel column numbering</a> first appeared on <a href="https://www.johndcook.com/blog">John D. Cook</a>.]]></description>
										<content:encoded><![CDATA[<p>I was working with a wide spreadsheet from a client the other day and I had to convert between Excel column labels and column numbers. I had never paid attention to how Excel labels columns and implicitly thought it was base 26 using letters rather than digits. But then I realized that&#8217;s not right.</p>
<p>Excel labels columns A through Z, then AA through AZ, then BA through BZ, etc. If this is base 26, then does A correspond to 0? That could work for A through Z, but then what about AA? Then you&#8217;d have to say the first A corresponds to 26 but the second A corresponds to 0.</p>
<p>Does Z correspond to 0? If so then the column numbers would be 1 through 25, followed by 0, then 27. And it would mean that columns ZA through ZZ are the same as A through Z.</p>
<p>In fact nothing in Excel column labeling corresponds to 0. The labels cannot be interpreted as a positional number system.</p>
<p>There&#8217;s a name for this kind of number system: <strong>bijective base 26</strong>. The concept extends generally to bijective base <em>b</em> for any positive integer <em>b</em>. The idea is ancient, but the name was coined recently. It has also been called <em>k</em>-adic numbering. For most of history it didn&#8217;t have a name.</p>
<p>The motivation behind the name bijective base <em>b</em> is that there is a bijection (a one-to-one correspondence) between these symbols and positive integers; there&#8217;s no possibility of leading zeros that would keep the mapping from being a bijection, unlike say 7 and 07 representing the same number.</p>
<h2>Excel limits</h2>
<p>Before 2007, an Excel file could have a maximum of 2<sup>8</sup> = 256 columns, and so the largest column label was IV.</p>
<p>Then in 2007 the column limit was increased to 2<sup>14</sup> = 16,384 and the largest column label is XFD.</p>
<h2>Conversion code</h2>
<p>Converting from column labels to integers is easy; going the other way is a little more complicated.</p>
<pre>letter_to_ordinal = lambda c: ord(c) - ord('A') + 1
ordinal_to_letter = lambda n: chr(ord('A') + n - 1)

def label_to_num(label):
    label = label.upper()
    n = 0
    for c in label:
        n = n*26 + letter_to_ordinal(c)
    return n

def num_to_label(n):
    letters = []
    while n &gt; 0:
        n, remainder = divmod(n - 1, 26)
        letters.append(ordinal_to_letter(remainder + 1))
    return ''.join(reversed(letters))
</pre>
<p>Here&#8217;s an <a href="https://www.johndcook.com/excel_labels.html">online calculator</a> based on the code above.</p>
<h2>Tests</h2>
<p>The following code verifies the assertions above about the maximum number of Excel columns over time.</p>
<pre>assert(num_to_label(256) == "IV")
assert(label_to_num("IV") == 256)

assert(num_to_label(2**14) == "XFD")
assert(label_to_num("XFD") == 2**14)
</pre>
<p>The conversion routines are not limited to actual Excel labels but work for arbitrarily large integers and bijective base 26 representations. For example, the following code shows that the bijective base 26 representation of Avogadro&#8217;s number is MUAEKAUDYDXEWOSDD.</p>
<pre>avogadro = 602_214_076_000_000_000_000_000
assert(label_to_num(num_to_label(avogadro)) == avogadro)
print(num_to_label(avogadro))
</pre>
<h2>Related posts</h2>
<ul>
<li class="link"><a href="https://www.johndcook.com/radix_conversion.html">Radix conversion calculator</a></li>
<li class="link"><a href="https://www.johndcook.com/blog/2020/03/16/round-trip-radix-conversion/">When is floating point radix conversion exact?</a></li>
<li class="link"><a href="https://www.johndcook.com/blog/2019/09/07/excel-r-bom/">Excel, R, and Unicode</a></li>
</ul>The post <a href="https://www.johndcook.com/blog/2026/07/25/excel-column-numbering/">Excel column numbering</a> first appeared on <a href="https://www.johndcook.com/blog">John D. Cook</a>.]]></content:encoded>
					
					<wfw:commentRss>https://www.johndcook.com/blog/2026/07/25/excel-column-numbering/feed/</wfw:commentRss>
			<slash:comments>1</slash:comments>
		
		
			</item>
		<item>
		<title>An almost periodic function</title>
		<link>https://www.johndcook.com/blog/2026/07/23/an-almost-periodic-function/</link>
		
		<dc:creator><![CDATA[John]]></dc:creator>
		<pubDate>Thu, 23 Jul 2026 14:04:34 +0000</pubDate>
				<category><![CDATA[Math]]></category>
		<guid isPermaLink="false">https://www.johndcook.com/blog/?p=247418</guid>

					<description><![CDATA[<p>This post takes a more abstract view of the previous post. That post looked at the concrete question of whether a number ever has the same sine in radians as in degrees. The relation between radians and degrees is irrelevant except that π/180 is an irrational number. Suppose α and β are two positive numbers [&#8230;]</p>
The post <a href="https://www.johndcook.com/blog/2026/07/23/an-almost-periodic-function/">An almost periodic function</a> first appeared on <a href="https://www.johndcook.com/blog">John D. Cook</a>.]]></description>
										<content:encoded><![CDATA[<p>This post takes a more abstract view of the <a href="https://www.johndcook.com/blog/2026/07/22/degrees-radians/">previous post</a>. That post looked at the concrete question of whether a number ever has the same sine in radians as in degrees. The relation between radians and degrees is irrelevant except that π/180 is an irrational number.</p>
<p>Suppose α and β are two positive numbers such that α/β is irrational. In the previous post, α = 1 and β = π/180. Then the function</p>
<p style="padding-left: 40px;"><em>f</em>(<em>x</em>) = sin(α<em>x</em>) − sin(β<em>x</em>)</p>
<p>is almost periodic: it is not periodic, but it comes close to being periodic, as close as you&#8217;d like provided you&#8217;re willing to look over a sufficiently long range of <em>x</em>&#8216;s.</p>
<p>The identity</p>
<p style="padding-left: 40px;">sin(α<em>x</em>) − sin(β<em>x</em>) = 2 cos((α + β)<em>x</em>/2) sin((α − β)<em>x</em>/2)</p>
<p>shows that <em>f</em>(<em>x</em>) is the product of two periodic functions but is not periodic itself. The periods of the cosine and sine above never coincide because the ratio of their frequencies is irrational.</p>
<p>The zeros of <em>f</em> are not periodic, though they can be divided into two subsequences that are periodic.</p>The post <a href="https://www.johndcook.com/blog/2026/07/23/an-almost-periodic-function/">An almost periodic function</a> first appeared on <a href="https://www.johndcook.com/blog">John D. Cook</a>.]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>When sine of x degrees equals sine of x radians</title>
		<link>https://www.johndcook.com/blog/2026/07/22/degrees-radians/</link>
		
		<dc:creator><![CDATA[John]]></dc:creator>
		<pubDate>Wed, 22 Jul 2026 12:17:11 +0000</pubDate>
				<category><![CDATA[Math]]></category>
		<category><![CDATA[Number theory]]></category>
		<guid isPermaLink="false">https://www.johndcook.com/blog/?p=247407</guid>

					<description><![CDATA[<p>Ordinarily the sine of x radians and the sine of x degrees are very different numbers. Having your calculator in radian mode when it should be in degree mode, or vice versa, results in a major error. But sometimes it doesn&#8217;t matter. A trivial example is when x = 0. A more interesting example is [&#8230;]</p>
The post <a href="https://www.johndcook.com/blog/2026/07/22/degrees-radians/">When sine of x degrees equals sine of x radians</a> first appeared on <a href="https://www.johndcook.com/blog">John D. Cook</a>.]]></description>
										<content:encoded><![CDATA[<p>Ordinarily the sine of <em>x</em> radians and the sine of <em>x</em> degrees are very different numbers. Having your calculator in radian mode when it should be in degree mode, or vice versa, results in a major error.</p>
<p>But sometimes it doesn&#8217;t matter. A trivial example is when <em>x</em> = 0. A more interesting example is</p>
<p style="padding-left: 40px;"><em>x</em> = 180π/(180 + π) = 3.08770208….</p>
<p>For that value of <em>x</em>,</p>
<p style="padding-left: 40px;">sin(<em>x</em>) = sin(<em>x</em>°).</p>
<p>In this article I&#8217;ll use the common convention of using radians by default and denoting degrees with ° as above.</p>
<p>Note that</p>
<p style="padding-left: 40px;"><em>x</em><em> = </em>π<em>x</em><em>°</em>/180</p>
<p>and so we are interested in solutions to the equation</p>
<p style="padding-left: 40px;">sin(<em>x</em>) = sin(π<em>x</em>/180)</p>
<p>Now two angles <em>A</em> and <em>B</em> have the same sine if they differ by a multiple of 2π, or if they&#8217;re supplementary (i.e. <em>A</em> = π − <em>B</em>), or both. To put it another way, if <em>A</em> and <em>B</em> have the same sine, they are either equal mod 2π or supplementary mod 2π. This means that</p>
<p style="padding-left: 40px;">sin(<em>x</em>) = sin(π<em>x</em>/180)</p>
<p>if and only if</p>
<p style="padding-left: 40px;"><em>x</em> = π<em>x</em>/180 + 2π<em>k</em></p>
<p>or</p>
<p style="padding-left: 40px;"><em>x</em> = π − π<em>x</em>/180 + 2π<em>k</em></p>
<p>for some integer <em>k</em>.</p>
<p>Therefore all solutions have the form</p>
<p style="padding-left: 40px;"><em>x</em> = 360π<em>k</em>/(180 − π)</p>
<p>or</p>
<p style="padding-left: 40px;"><em>x</em> = 180π(2<em>k</em> + 1)/(180 + π).</p>
<h2>Alternative solution</h2>
<p>The derivation above is correct, but it occurred to me later that a simpler argument would be to use the identity</p>
<p style="padding-left: 40px;">sin(<em>A</em>) − sin(<em>B</em>) = 2 cos((<em>A</em> + <em>B</em>)/2) sin((<em>A</em> − <em>B</em>)/2).</p>
<p>Thus <em>A</em> and <em>B</em> have the same sine if</p>
<p style="padding-left: 40px;">cos((<em>A</em> + <em>B</em>)/2) = 0</p>
<p>or if</p>
<p style="padding-left: 40px;">sin((<em>A</em> − <em>B</em>)/2) = 0.</p>
<p>These two possibilities correspond to the two families of solutions above.</p>
<h2>Density</h2>
<p>When reduced modulo 2π, both families are dense in [0, 2π]. This means that for every <em>y</em> in [−1, 1], there is a number <em>x</em> such that</p>
<p style="padding-left: 40px;">sin(<em>x</em>) = sin(<em>x</em>°) ≈ <em>y</em></p>
<p>and we can make the approximation as good as we&#8217;d like.</p>
<h3>Example 1</h3>
<p>For example, today is July 22, so let&#8217;s set <em>y</em> = 0.722. We&#8217;d like to find a value of <em>x</em> such that the sine of <em>x</em> radians and the sine of <em>x</em> degrees both approximately equal 0.722. And let&#8217;s say our approximation tolerance is ε = 0.0001.</p>
<p>We can search for a value of <em>x</em> in the first family of solutions by looking for a value of <em>k</em> with</p>
<p style="padding-left: 40px;">| sin(360π<em>k</em>/(180 − π)) − 0.722 | &lt; 0.0001</p>
<p>and the smallest such <em>k</em> is 96343 and so</p>
<p style="padding-left: 40px;"><em>x</em> = 360×96343 π/(180 − π) = 616093.78713621…</p>
<p>will do, and sin(<em>x</em>) = 0.72191…</p>
<h3>Example 2</h3>
<p>Now let&#8217;s set <em>y</em> = 0.2026 and look for a solution in the other family of solutions, and this time let&#8217;s set ε = 10<sup>−6</sup>. The smallest value of <em>k</em> such that</p>
<p style="padding-left: 40px;">| sin(180π(2<em>k</em> + 1)/(180 + π)) − 0.2026 | &lt; 10<sup>−6</sup></p>
<p>is <em>k</em> = 741141. Then</p>
<p style="padding-left: 40px;">sin( 4576848.310950611 ) = sin( 4576848.310950611° ) = 0.202600139…</p>The post <a href="https://www.johndcook.com/blog/2026/07/22/degrees-radians/">When sine of x degrees equals sine of x radians</a> first appeared on <a href="https://www.johndcook.com/blog">John D. Cook</a>.]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>Forensic accounting in Python</title>
		<link>https://www.johndcook.com/blog/2026/07/21/forensic-accounting-in-python/</link>
		
		<dc:creator><![CDATA[John]]></dc:creator>
		<pubDate>Tue, 21 Jul 2026 15:05:04 +0000</pubDate>
				<category><![CDATA[Statistics]]></category>
		<guid isPermaLink="false">https://www.johndcook.com/blog/?p=247395</guid>

					<description><![CDATA[<p>I recently had a project in which I had to reverse engineer a data analysis. There was some ambiguity regarding which of several possibilities someone chose for several of the variables, something analogous to the following example. Suppose you have three numbers with uncertain values with a known, or at least purported, sum. The first [&#8230;]</p>
The post <a href="https://www.johndcook.com/blog/2026/07/21/forensic-accounting-in-python/">Forensic accounting in Python</a> first appeared on <a href="https://www.johndcook.com/blog">John D. Cook</a>.]]></description>
										<content:encoded><![CDATA[<p>I recently had a project in which I had to reverse engineer a data analysis. There was some ambiguity regarding which of several possibilities someone chose for several of the variables, something analogous to the following example.</p>
<p>Suppose you have three numbers with uncertain values with a known, or at least purported, sum. The first number could be 31, 41, or 59; the second could be either 26 or 53; the last could be 58, 97, 93, or 23.</p>
<p>The following code enumerates all 3 &times; 2 &times; 4 = 24 possibilities and prints their sums.</p>
<pre>
from itertools import product

# Example input
possibilities = [(31, 41, 59), (26, 53), (58, 97, 93, 23)]

for combo in product(*possibilities):
    total = sum(combo) 
    print(f"Combination {combo} sums to: {total}")
</pre>
<p>In this example all the sums are unique, though of course that might not happen in practice. If, for example, you know the sum is 187, you know the three numbers were 41, 53, and 93. If the reported sum is 200, you know some assumption has been violated because none of the possible choices add up to 200.</p>
<h2>More forensics posts</h2>
<ul>
<li class='link'><a href='https://www.johndcook.com/blog/2009/09/18/make-up-your-own-rules-of-probability/'>Make up your own rules of probability</a></li>
<li class='link'><a href='https://www.johndcook.com/blog/2023/04/01/identifiable-to-man-or-machine/'>Identifiable to man or machine?</a></li>
<li class='link'><a href='https://www.johndcook.com/blog/2024/02/13/photo-metadata/'>Metadata in photos</a></li>
<li class='link'><a href='https://www.johndcook.com/blog/2026/05/05/changing-one-character-in-a-pdf/'>Changing one character in a PDF</a></li>
<li class='link'><a href='https://www.johndcook.com/blog/2018/03/02/bits-of-information-in-age-birthday-and-birthdate/'>Bits of information in age or birthday</a></li>
</ul>The post <a href="https://www.johndcook.com/blog/2026/07/21/forensic-accounting-in-python/">Forensic accounting in Python</a> first appeared on <a href="https://www.johndcook.com/blog">John D. Cook</a>.]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>Locally everywhere does not imply everywhere</title>
		<link>https://www.johndcook.com/blog/2026/07/21/jacobian-conjecture/</link>
					<comments>https://www.johndcook.com/blog/2026/07/21/jacobian-conjecture/#comments</comments>
		
		<dc:creator><![CDATA[John]]></dc:creator>
		<pubDate>Tue, 21 Jul 2026 12:14:03 +0000</pubDate>
				<category><![CDATA[Math]]></category>
		<category><![CDATA[Algebraic geometry]]></category>
		<guid isPermaLink="false">https://www.johndcook.com/blog/?p=247398</guid>

					<description><![CDATA[<p>A couple days ago, Levent Alpöge, a mathematician working at Anthropic, discovered a counterexample to the Jacobian conjecture using Claude Fable 5. I was curious whether most mathematicians were trying to prove or disprove the conjecture, so I asked Claude. Before a counterexample to the Jacobian conjecture was found, did most mathematicians believe it was [&#8230;]</p>
The post <a href="https://www.johndcook.com/blog/2026/07/21/jacobian-conjecture/">Locally everywhere does not imply everywhere</a> first appeared on <a href="https://www.johndcook.com/blog">John D. Cook</a>.]]></description>
										<content:encoded><![CDATA[<p>A couple days ago, Levent Alpöge, a mathematician working at Anthropic, discovered a counterexample to the Jacobian conjecture using Claude Fable 5.</p>
<p>I was curious whether most mathematicians were trying to prove or disprove the conjecture, so I asked Claude.</p>
<blockquote><p>Before a counterexample to the Jacobian conjecture was found, did most mathematicians believe it was true or false?</p></blockquote>
<p>Claude&#8217;s response was</p>
<blockquote><p>The premise of this question isn&#8217;t quite right — no counterexample to the Jacobian conjecture has been found. It remains an open problem in mathematics: no one has proven it true, and no one has found a counterexample disproving it. … If you encountered a claim that a counterexample was found, do you have a source for that? I&#8217;d be happy to look into it, since that would actually be a major result in algebraic geometry if true.</p></blockquote>
<p>Of course Claude doesn&#8217;t <em>know</em> that it solved the conjecture. It didn&#8217;t even solve the conjecture. It was an inanimate tool in the hand of a mathematician, just like a piece of chalk or a dry erase marker.</p>
<p>The middle part of Claude&#8217;s response was that mathematicians are (were) divided on whether the conjecture is true. So it was not like the Riemann hypothesis, which most people believe to be true, or the P = NP conjecture, which most people believe to be false.</p>
<p>Now what is the Jacobian conjecture? It says that a polynomial function from ℝ<sup><em>n</em></sup> to ℝ<sup><em>n</em></sup> with constant, non-zero Jacobian determinant has a polynomial inverse. (The conjecture was stated more generally for fields of characteristic 0, in which the derivatives defining the Jacobian would have to be defined algebraically, not in terms of limits.)</p>
<p>Alpöge came up with a counterexample, a polynomial function from ℝ³ to ℝ³ with constant Jacobian determinant −2. The function is</p>
<p><img loading="lazy" decoding="async" class="aligncenter" style="background-color: white;" src="https://www.johndcook.com/jacobian_conjecture.svg" alt="\begin{align*}F(x,y,z)={}\bigl(~\!&amp; z (1+xy)^3 + y^2 (1+xy) (4+3xy),\\ &amp;y + 3x(1+xy)^2 z + 3xy^2 (4+3xy), \\ &amp;2x - 3x^2 y - x^3 z ~\!\bigr).\end{align*}" width="385" height="88" /></p>
<p>It&#8217;s a tedious but simple calculus exercise to show that the determinant equals −2 everywhere. The inverse function theorem says that a function is locally invertible at any point where the Jacobian determinant is non-zero, so Alpöge&#8217;s function is locally invertible everywhere.</p>
<p>However, the function takes on some values more than once. For example, (0, 0, −1/4) and (1, −3/2, 13/2) both map to (−1/4, 0, 0). Therefore the function is not invertible globally. So not only does the function not have a polynomial inverse, it doesn&#8217;t have an inverse even if you allow non-polynomial functions.</p>
<p>Alpöge&#8217;s counterexample disproves the Jacobian conjecture for <em>n</em> = 3. It can trivially be extended to all <em>n</em> &gt; 3 by defining the function to be Alpöge&#8217;s function for three variables and the identity for the rest. The conjecture remains open for <em>n</em> = 2.</p>The post <a href="https://www.johndcook.com/blog/2026/07/21/jacobian-conjecture/">Locally everywhere does not imply everywhere</a> first appeared on <a href="https://www.johndcook.com/blog">John D. Cook</a>.]]></content:encoded>
					
					<wfw:commentRss>https://www.johndcook.com/blog/2026/07/21/jacobian-conjecture/feed/</wfw:commentRss>
			<slash:comments>3</slash:comments>
		
		
			</item>
		<item>
		<title>Volume to Area ratio for Regular Solids</title>
		<link>https://www.johndcook.com/blog/2026/07/20/volume-area-regular-solids/</link>
					<comments>https://www.johndcook.com/blog/2026/07/20/volume-area-regular-solids/#comments</comments>
		
		<dc:creator><![CDATA[John]]></dc:creator>
		<pubDate>Mon, 20 Jul 2026 14:35:21 +0000</pubDate>
				<category><![CDATA[Math]]></category>
		<category><![CDATA[Geometry]]></category>
		<guid isPermaLink="false">https://www.johndcook.com/blog/?p=247378</guid>

					<description><![CDATA[<p>The volume of a sphere of radius r is V = 4πr³ / 3 and the surface area is A = 4πr² and so the ratio of volume to area is V / A = r / 3. Surprisingly, the same ratio holds for all regular solids if r is the radius of the largest sphere [&#8230;]</p>
The post <a href="https://www.johndcook.com/blog/2026/07/20/volume-area-regular-solids/">Volume to Area ratio for Regular Solids</a> first appeared on <a href="https://www.johndcook.com/blog">John D. Cook</a>.]]></description>
										<content:encoded><![CDATA[<p>The volume of a sphere of radius <em>r</em> is</p>
<p style="padding-left: 40px;"><em>V</em> = 4π<em>r</em>³ / 3</p>
<p>and the surface area is</p>
<p style="padding-left: 40px;"><em>A</em> = 4π<em>r</em>²</p>
<p>and so the ratio of volume to area is</p>
<p style="padding-left: 40px;"><em>V</em> / <em>A</em> = <em>r</em> / 3.</p>
<p>Surprisingly, the same ratio holds for all regular solids if <em>r</em> is the radius of the largest sphere that can be inscribed inside the regular solid.</p>
<p>For example, if the edge of a cube is <em>a</em>, then <em>r</em> = <em>a</em>/2. The volume is 8<em>r</em>³, the area is 24<em>r</em>², and the ratio is <em>r</em>/3.</p>
<p>The relationship between edge length and radius, and between radius and volume, is more complicated for the four other regular solids (tetrahedron, octahedron, dodecahedron, and icosahedron). However, in each case the ratio of volume to area is <em>r</em>/3.</p>
<p>The proof is surprisingly simple. Pick a face and form a pyramid by connecting each face vertex to the center of the inscribed sphere. The pyramid has height <em>r</em> and volume equal to <em>B</em>/3 where <em>B</em> is the area of the base. If the regular solid has <em>f</em> faces, the volume of the solid is <em>fBr</em> / 3 and the area is <em>fB</em>. So the ratio of volume to area is <em>r</em>/3.</p>
<p>The theorem generalizes to <em>n</em> &gt; 3 dimensions. The formula for the volume of a pyramid in <em>n</em> dimensions is <em>Bh</em>/<em>n</em> where <em>B</em> is the (<em>n</em> − 1)-dimensional volume of the base, and so the ratio of <em>n</em>-dimensional volume of a regular solid to (<em>n</em> − 1)-dimensional volume of its boundary is <em>r</em>/<em>n</em>.</p>The post <a href="https://www.johndcook.com/blog/2026/07/20/volume-area-regular-solids/">Volume to Area ratio for Regular Solids</a> first appeared on <a href="https://www.johndcook.com/blog">John D. Cook</a>.]]></content:encoded>
					
					<wfw:commentRss>https://www.johndcook.com/blog/2026/07/20/volume-area-regular-solids/feed/</wfw:commentRss>
			<slash:comments>1</slash:comments>
		
		
			</item>
		<item>
		<title>Solving a chess puzzle with Grok 4.5</title>
		<link>https://www.johndcook.com/blog/2026/07/20/grok-chess/</link>
					<comments>https://www.johndcook.com/blog/2026/07/20/grok-chess/#comments</comments>
		
		<dc:creator><![CDATA[John]]></dc:creator>
		<pubDate>Mon, 20 Jul 2026 14:33:31 +0000</pubDate>
				<category><![CDATA[Uncategorized]]></category>
		<guid isPermaLink="false">https://www.johndcook.com/blog/?p=247386</guid>

					<description><![CDATA[<p>I&#8217;ve written several posts about using Claude or ChatGPT to generate Prolog or Lean code to solve a chess puzzle. I didn&#8217;t think Grok would be up to the task, though I didn&#8217;t try it. I&#8217;ve heard good things about Grok 4.5, so I gave it a shot. It did great. Here&#8217;s the problem, a [&#8230;]</p>
The post <a href="https://www.johndcook.com/blog/2026/07/20/grok-chess/">Solving a chess puzzle with Grok 4.5</a> first appeared on <a href="https://www.johndcook.com/blog">John D. Cook</a>.]]></description>
										<content:encoded><![CDATA[<p>I&#8217;ve written several posts about using Claude or ChatGPT to generate Prolog or Lean code to solve a chess puzzle. I didn&#8217;t think Grok would be up to the task, though I didn&#8217;t try it. I&#8217;ve heard good things about Grok 4.5, so I gave it a shot. It did great.</p>
<p>Here&#8217;s the problem, a variation on the <em>n</em>-queens problem formulated by Martin Gardner.</p>
<blockquote><p>Can you place five white queens and three black queens on a 5 × 5 board so that no queen of one color attacks a queen of another color?</p></blockquote>
<p>Here&#8217;s the prompt I have Grok 4.5.</p>
<blockquote><p>Generate SWI Prolog code to solve the following chess puzzle. On a 5 by 5 chessboard, place 5 white queens and 3 black queens so that no queen is attacking a queen of the other color. Have the code enumerate all solutions.</p></blockquote>
<p>The code ran correctly, giving all eight solutions.</p>
<p>I then asked Grok</p>
<blockquote><p>Now generate Lean 4 code to solve the same puzzle.</p></blockquote>
<p>The code did not run on the first or second attempts but ran correctly on the third iteration. That&#8217;s better than I&#8217;ve seen before. It&#8217;s not an apples-to-apples comparison, but it has usually taken many iterations with Claude before Lean code would run.</p>
<h2>Solutions</h2>
<p>There are eight solutions, each being a flip or a rotation of one fundamental solution. Here&#8217;s a representative.</p>
<p><img loading="lazy" decoding="async" class="aligncenter size-medium" src="https://www.johndcook.com/black_white_queens.png" width="400" height="400" /></p>
<h2>SWI Prolog code</h2>
<pre>:- use_module(library(clpfd)).

%% Enumerate all ways to place 5 white queens and 3 black queens
%% on a 5x5 board so that no queen attacks a queen of the opposite colour.
%% Same-colour queens are allowed to attack each other.
%%
%% Positions are numbered 1..25 in row-major order:
%%   1  2  3  4  5
%%   6  7  8  9 10
%%  11 12 13 14 15
%%  16 17 18 19 20
%%  21 22 23 24 25

solve :-
    solution(Whites, Blacks),
    print_solution(Whites, Blacks),
    fail.
solve :-
    writeln('No more solutions.').

solution(Whites, Blacks) :-
    length(Whites, 5),
    length(Blacks, 3),
    append(Whites, Blacks, All),
    All ins 1..25,
    all_distinct(All),
    % canonical order so each set of positions is generated once
    Whites = [W1,W2,W3,W4,W5],
    W1 #&lt; W2, W2 #&lt; W3, W3 #&lt; W4, W4 #&lt; W5,
    Blacks = [B1,B2,B3],
    B1 #&lt; B2, B2 #&lt; B3, % every black is safe from every white maplist(safe_from_all(Whites), Blacks), label(All). safe_from_all(Whites, Black) :- maplist(no_attack(Black), Whites). %% Two queens do *not* attack each other. no_attack(P, Q) :- R1 #= (P-1) div 5 + 1, C1 #= (P-1) mod 5 + 1, R2 #= (Q-1) div 5 + 1, C2 #= (Q-1) mod 5 + 1, R1 #\= R2, % different row C1 #\= C2, % different column abs(R1-R2) #\= abs(C1-C2). % different diagonal print_solution(Whites, Blacks) :- format('Whites: ~w~n', [Whites]), format('Blacks: ~w~n', [Blacks]), forall(between(1, 5, R), ( forall(between(1, 5, C), ( Pos is (R-1)*5 + C, ( memberchk(Pos, Whites) -&gt; write('W ')
                      ; memberchk(Pos, Blacks) -&gt; write('B ')
                      ; write('. ')
                      )
                    )),
             nl )),
    nl.
</pre>
<h2>Lean 4 code</h2>
<pre>-- Lean 4 solution for the 5×5 black-and-white queens puzzle.
-- 5 white queens + 3 black queens; no opposite-colour attacks.

def absDiff (a b : Nat) : Nat :=
  if a ≥ b then a - b else b - a

def attacks (p q : Nat × Nat) : Bool :=
  p.1 == q.1 || p.2 == q.2 || absDiff p.1 q.1 == absDiff p.2 q.2

/-- All 25 squares (0-based, row-major). -/
def allPositions : List (Nat × Nat) :=
  (List.range 5).flatMap fun r =&gt;
    (List.range 5).map fun c =&gt; (r, c)

/-- Combinations of length exactly `k` (order-preserving). -/
def combinations {α : Type} : Nat → List α → List (List α)
  | 0,     _      =&gt; [[]]
  | _,     []     =&gt; []
  | k + 1, x::ys  =&gt;
      (combinations k ys).map (fun zs =&gt; x :: zs) ++
      combinations (k + 1) ys

def isAttackedBy (pos : Nat × Nat) (queens : List (Nat × Nat)) : Bool :=
  queens.any (fun q =&gt; attacks q pos)

/-- Every solution as a pair (white squares, black squares). -/
def solve : List (List (Nat × Nat) × List (Nat × Nat)) :=
  (combinations 3 allPositions).foldl (fun acc blacks =&gt;
    let forbidden := allPositions.filter fun p =&gt;
      blacks.contains p || isAttackedBy p blacks
    let available := allPositions.filter fun p =&gt;
      !(forbidden.contains p)
    acc ++ (combinations 5 available).map fun whites =&gt; (whites, blacks)
  ) []

/-- Pretty-print one board. -/
def printBoard (whites blacks : List (Nat × Nat)) : IO Unit := do
  for r in List.range 5 do
    let mut row := ""
    for c in List.range 5 do
      let p := (r, c)
      row := row ++
        (if whites.contains p then "W "
         else if blacks.contains p then "B "
         else ". ")
    IO.println row
  IO.println ""

/-- Enumerate and display every solution. -/
def main : IO Unit := do
  let sols := solve
  IO.println s!"Found {sols.length} solutions.\n"
  for p in (List.range sols.length).zip sols do
    let i := p.1
    let (whites, blacks) := p.2
    IO.println s!"Solution {i + 1}:"
    printBoard whites blacks

#eval main
</pre>The post <a href="https://www.johndcook.com/blog/2026/07/20/grok-chess/">Solving a chess puzzle with Grok 4.5</a> first appeared on <a href="https://www.johndcook.com/blog">John D. Cook</a>.]]></content:encoded>
					
					<wfw:commentRss>https://www.johndcook.com/blog/2026/07/20/grok-chess/feed/</wfw:commentRss>
			<slash:comments>1</slash:comments>
		
		
			</item>
		<item>
		<title>Fitting a regular expression to a list of words</title>
		<link>https://www.johndcook.com/blog/2026/07/19/fitting-a-regex/</link>
					<comments>https://www.johndcook.com/blog/2026/07/19/fitting-a-regex/#comments</comments>
		
		<dc:creator><![CDATA[John]]></dc:creator>
		<pubDate>Sun, 19 Jul 2026 19:30:17 +0000</pubDate>
				<category><![CDATA[Computing]]></category>
		<category><![CDATA[Regular expressions]]></category>
		<guid isPermaLink="false">https://www.johndcook.com/blog/?p=247380</guid>

					<description><![CDATA[<p>Suppose you want to search for a list of words. If you&#8217;re using grep, you can add the -f flag provide a file of regular expressions, and you can add the -F to tell it that the regular expressions are in fact just words. I did something like this a couple days ago when searching [&#8230;]</p>
The post <a href="https://www.johndcook.com/blog/2026/07/19/fitting-a-regex/">Fitting a regular expression to a list of words</a> first appeared on <a href="https://www.johndcook.com/blog">John D. Cook</a>.]]></description>
										<content:encoded><![CDATA[<p>Suppose you want to search for a list of words. If you&#8217;re using grep, you can add the <code>-f</code> flag provide a file of regular expressions, and you can add the <code>-F</code> to tell it that the regular expressions are in fact just words. I did something like this <a href="https://www.johndcook.com/blog/2026/07/17/regex-speed-error/">a couple days ago</a> when searching for diagnosis codes.</p>
<pre>grep -w -F -o -f icd10codes.txt notes.txt</pre>
<p>Now you might want to combine your list of words into a singular regular expression, for efficiency or possibly for some other reason. Apparently ripgrep does this because when I tried replacing grep with ripgrep in the command above I got an error saying &#8220;Compiled regex exceeds size limit of 104857600 bytes.&#8221;</p>
<h2>Beating brute force</h2>
<p>Say you wanted to search for the strings &#8220;bluecross&#8221;, &#8220;blueshield&#8221;, and &#8220;bluey&#8221;. You could simply form the brute force regular expression</p>
<pre>bluecross|blueshied|bluey</pre>
<p>but that doesn&#8217;t take advantage of the fact that all three strings begin with &#8220;blue.&#8221; A smaller regular expression would be</p>
<pre>blue(shield|cross|y)</pre>
<p>Finding the shortest regular expression that matches a list of words is a hard problem, but finding a regular expression that&#8217;s shorter than brute force is not. The Python package <a href="https://github.com/ermanh/trieregex">trieregex</a> will do this. According to the documentation,</p>
<blockquote><p>trieregex creates efficient regular expressions (regexes) by storing a list of words in a trie structure, and translating the trie into a more compact pattern.</p></blockquote>
<p>Let&#8217;s try our blue example with trieregex.</p>
<pre>import re
from trieregex import TrieRegEx as TRE

words = ['bluecross', 'blueshield', 'bluey']
tre = TRE(*words) 
print(tre.regex())
</pre>
<p>This produces the same regular expression as above, except it adds <code>?:</code> to make the parentheses non-capturing.</p>
<pre>blue(?:shield|cross|y)</pre>
<h2>Prefixes versus suffixes</h2>
<p>The library builds a trie data structure using common prefixes. That works well in the example above, but the result is disappointing when we have common suffixes rather than common prefixes. The following code</p>
<pre>words = ['javascript', 'typescript']
tre = TRE(*words) 
print(tre.regex())
</pre>
<p>produces the regular expression</p>
<pre>(?:javascript|typescript)</pre>
<p>which is no better than brute force, whereas we might have hoped for</p>
<pre>(?:java|type)script</pre>
<h2>HCPCS examples</h2>
<p>As mentioned at the top of the post, ripgrep failed to search on a list of ICD-10 codes. The list of HCPCS codes is about 10x smaller, and more compressible. Ripgrep was able to fit all HCPCS codes into a single regex and was able to search the test file much faster than grep. The command</p>
<pre>grep -w -F -o -f hcpcs.txt notes.txt</pre>
<p>took 73.426 seconds to execute, while the command</p>
<pre>rg -w -F -o -f hcpsc.txt notes.txt</pre>
<p>took 0.078 seconds, three orders of magnitude faster.</p>
<p>The following code will read a list of HCPCS codes from a file and create a regular expression.</p>
<pre>tre = TRE()
with open('hcpcs.txt', 'r') as file:
    for line in file:
        tre.add(line.strip())
print(len(tre.regex()))
</pre>
<p>This shows that the resulting regular expression has 17,198 characters. The file of codes has 8725 five-character codes, so the regex compresses the code characters by roughly a ratio of 5 to 2.</p>The post <a href="https://www.johndcook.com/blog/2026/07/19/fitting-a-regex/">Fitting a regular expression to a list of words</a> first appeared on <a href="https://www.johndcook.com/blog">John D. Cook</a>.]]></content:encoded>
					
					<wfw:commentRss>https://www.johndcook.com/blog/2026/07/19/fitting-a-regex/feed/</wfw:commentRss>
			<slash:comments>5</slash:comments>
		
		
			</item>
		<item>
		<title>Sum of low squares</title>
		<link>https://www.johndcook.com/blog/2026/07/19/sum-of-low-squares/</link>
					<comments>https://www.johndcook.com/blog/2026/07/19/sum-of-low-squares/#comments</comments>
		
		<dc:creator><![CDATA[John]]></dc:creator>
		<pubDate>Sun, 19 Jul 2026 16:41:48 +0000</pubDate>
				<category><![CDATA[Math]]></category>
		<category><![CDATA[Number theory]]></category>
		<category><![CDATA[SymPy]]></category>
		<guid isPermaLink="false">https://www.johndcook.com/blog/?p=247374</guid>

					<description><![CDATA[<p>Squares, high and low Let p be an odd prime number. Then half the numbers from 1 through p − 1 are squares and half are not. That is, for half of numbers 1 ≤ k &#60; p, the equation x² = k mod p has a solution. The traditional name for these numbers is [&#8230;]</p>
The post <a href="https://www.johndcook.com/blog/2026/07/19/sum-of-low-squares/">Sum of low squares</a> first appeared on <a href="https://www.johndcook.com/blog">John D. Cook</a>.]]></description>
										<content:encoded><![CDATA[<h2>Squares, high and low</h2>
<p>Let <em>p</em> be an odd prime number. Then half the numbers from 1 through <em>p</em> − 1 are squares and half are not. That is, for half of numbers 1 ≤ <em>k</em> &lt; <em>p</em>, the equation</p>
<p style="padding-left: 40px;"><em>x</em>² = <em>k</em> mod <em>p</em></p>
<p>has a solution. The traditional name for these numbers is &#8220;quadratic residues&#8221; but we can just say &#8220;squares&#8221; if the context is clear. So, for example, the numbers 1, 2, and 4 are squares mod 7, and the numbers 3, 5, and 6 are not.</p>
<p>If <em>k</em> is a square mod <em>p</em> we will call is a <strong>low square</strong> if 0 ≤ <em>k</em> &lt; <em>p</em>/2 and a <strong>high square</strong> if <em>p</em>/2 &lt; <em>k</em> &lt; <em>p</em>.</p>
<h2>Signatures</h2>
<p>Now let <em>p</em> &gt; 3 be a prime congruent to 3 mod 4. Add up all the low squares mod <em>p</em> and take the remainder mod <em>p</em>. Call this the signature of <em>p</em>. Here&#8217;s Python code to make this explicit.</p>
<pre>from sympy import isprime, factorint, is_quad_residue

def signature(p):
    assert(p &gt; 3)
    assert(isprime(p))
    assert(p % 4 == 3)
    s = 0
    for k in range(1, 1 + p//2):
        if is_quad_residue(k, p):
            s += k
    return s % p
</pre>
<h2>Inverse signatures</h2>
<p>Surprisingly, the signature of each <em>p</em> is unique. Given the signature of <em>p</em>, you can uniquely determine <em>p</em>, and in fact you can do so easily. I ran across this in a paper [1] that presented the results in the form of a parlor trick: have someone pick a prime <em>p</em> such that <em>p</em> = 3 mod 4 and ask them to compute its signature, the sum of the low squares mod <em>p</em>. Then you can quickly tell them what their choice of <em>p</em> was.</p>
<p>Given a signature <em>s</em>, the corresponding prime <em>p</em> is the largest prime factor of 16<em>s</em> + 1.</p>
<p>Not only that,</p>
<p style="padding-left: 40px;"><em>p</em> = (16<em>s</em> + 1)/<em>m</em></p>
<p>where <em>m</em> is the smallest of the numbers {3, 7, 11, 15} such that the fraction above is a prime number. In term of Python code, both the following functions should invert the signature of <em>p</em>.</p>
<pre>def inverse_signature1(s):
    n = 16*s + 1
    return max(factorint(n).keys())

def inverse_signature2(s):
    n = 16*s + 1
    for m in [3, 7, 11, 15]:
        if n % m == 0 and isprime(n // m):
            return n // m
</pre>
<p>The following code demonstrates that this is the case for numbers less than 1,000.</p>
<pre>for n in range(7, 1000, 4):
    if isprime(n):
        s = signature(n)
        assert(n == inverse_signature1(s))
        assert(n == inverse_signature2(s))        
</pre>
<p>[1] David M. Bloom. A Quadratic Residues Parlor Trick. Mathematics Magazine, Vol. 71, No. 3 (Jun., 1998), pp. 201–203.</p>The post <a href="https://www.johndcook.com/blog/2026/07/19/sum-of-low-squares/">Sum of low squares</a> first appeared on <a href="https://www.johndcook.com/blog">John D. Cook</a>.]]></content:encoded>
					
					<wfw:commentRss>https://www.johndcook.com/blog/2026/07/19/sum-of-low-squares/feed/</wfw:commentRss>
			<slash:comments>1</slash:comments>
		
		
			</item>
		<item>
		<title>Visualizing Medical Code Hierarchy</title>
		<link>https://www.johndcook.com/blog/2026/07/17/visualizing-medical-code-hierarchy/</link>
					<comments>https://www.johndcook.com/blog/2026/07/17/visualizing-medical-code-hierarchy/#comments</comments>
		
		<dc:creator><![CDATA[John]]></dc:creator>
		<pubDate>Fri, 17 Jul 2026 16:12:31 +0000</pubDate>
				<category><![CDATA[Uncategorized]]></category>
		<category><![CDATA[Medical codes]]></category>
		<guid isPermaLink="false">https://www.johndcook.com/blog/?p=247369</guid>

					<description><![CDATA[<p>Quick follow up to the previous two posts on ICD-10 codes and HCPCS codes. This post uses Python&#8217;s squarify library to create treemaps visualizing how many codes begin with each letter. Here&#8217;s the treemap for HCPCS codes. And here&#8217;s the treemap for ICD-10 codes. The sizes of the squares are proportional to the number of [&#8230;]</p>
The post <a href="https://www.johndcook.com/blog/2026/07/17/visualizing-medical-code-hierarchy/">Visualizing Medical Code Hierarchy</a> first appeared on <a href="https://www.johndcook.com/blog">John D. Cook</a>.]]></description>
										<content:encoded><![CDATA[<p>Quick follow up to the previous two posts on ICD-10 codes and HCPCS codes. This post uses Python&#8217;s squarify library to create treemaps visualizing how many codes begin with each letter.</p>
<p>Here&#8217;s the treemap for HCPCS codes.</p>
<p><img loading="lazy" decoding="async" class="aligncenter size-medium" src="https://www.johndcook.com/HCPCS_treemap.png" alt="HCPCS code treemap" width="600" height="400" /></p>
<p>And here&#8217;s the treemap for ICD-10 codes.</p>
<p><img loading="lazy" decoding="async" class="aligncenter size-medium" src="https://www.johndcook.com/ICD10_treemap.png" alt="HCPCS code treemap" width="600" height="400" /></p>
<p>The sizes of the squares are proportional to the number of codes beginning with that letter. Note that they are not necessarily proportional to how often codes are used.</p>
<p>The HCPCS map omits R and U because these are tiny relative to the rest. The ICD-10 map omits U for the same reason.</p>
<p>Here&#8217;s the code that was used to create the HCPCS map.</p>
<pre>
import matplotlib.pyplot as plt
import squarify

# HCPCS
data = {
    "G": 2010,
    "J": 1232,
    "L": 940,
    "A": 862,
    "E": 671,
    "Q": 639,
    "C": 619,
    "S": 533,
    "M": 506,
    "V": 212,
    "K": 175,
    "T": 114,
    "H": 94,
    "P": 59,
    "B": 51,
  # "U": 5,
  # "R": 3,
}

labels = list(data.keys())
sizes = list(data.values())

# Labels are just the letters (no counts)
display_labels = labels

# Color map — one distinct color per box
colors = plt.cm.tab20.colors[: len(labels)]

fig, ax = plt.subplots(figsize=(12, 8))
squarify.plot(
    sizes=sizes,
    label=display_labels,
    color=colors,
    alpha=0.85,
    ax=ax,
    text_kwargs={"fontsize": 30, "weight": "bold"},
    pad=True,
)
ax.axis("off")

plt.tight_layout()
plt.savefig("treemap.png", dpi=72)
plt.show()
</pre>
<p>The code to create the ICD-10 map differs only in its data.</p>
<pre>
# ICD-10
data = {
    "S": 31052, 
    "T": 10090, 
    "M":  6665, 
    "V":  4086, 
    "H":  3330, 
    "O":  2437, 
    "Y":  1590, 
    "I":  1427, 
    "Z":  1411, 
    "W":  1290, 
    "C":  1226, 
    "L":  1000, 
    "E":   971, 
    "Q":   894, 
    "F":   871, 
    "K":   857, 
    "N":   836, 
    "D":   824, 
    "R":   773, 
    "G":   700, 
    "A":   573, 
    "X":   495, 
    "B":   495, 
    "P":   463, 
    "J":   360, 
  # "U":     3,
}  
</pre>The post <a href="https://www.johndcook.com/blog/2026/07/17/visualizing-medical-code-hierarchy/">Visualizing Medical Code Hierarchy</a> first appeared on <a href="https://www.johndcook.com/blog">John D. Cook</a>.]]></content:encoded>
					
					<wfw:commentRss>https://www.johndcook.com/blog/2026/07/17/visualizing-medical-code-hierarchy/feed/</wfw:commentRss>
			<slash:comments>1</slash:comments>
		
		
			</item>
		<item>
		<title>Regular expressions for HCPCS codes</title>
		<link>https://www.johndcook.com/blog/2026/07/17/regular-expressions-for-hcpcs-codes/</link>
		
		<dc:creator><![CDATA[John]]></dc:creator>
		<pubDate>Fri, 17 Jul 2026 14:02:52 +0000</pubDate>
				<category><![CDATA[Computing]]></category>
		<category><![CDATA[Medical codes]]></category>
		<category><![CDATA[Regular expressions]]></category>
		<guid isPermaLink="false">https://www.johndcook.com/blog/?p=247363</guid>

					<description><![CDATA[<p>Since I revisited my old post on ICD code matching, I thought I&#8217;d revisit by post on HCPCS codes too. HCPCS stands for Healthcare Common Procedure Coding System, and is pronounced &#8220;hick picks.&#8221; When most people say HCPCS, they technically mean HCPCS Level II, and that&#8217;s what I mean here. The format of a HCPCS [&#8230;]</p>
The post <a href="https://www.johndcook.com/blog/2026/07/17/regular-expressions-for-hcpcs-codes/">Regular expressions for HCPCS codes</a> first appeared on <a href="https://www.johndcook.com/blog">John D. Cook</a>.]]></description>
										<content:encoded><![CDATA[<p>Since I revisited my old post on ICD code matching, I thought I&#8217;d revisit by <a href="https://www.johndcook.com/blog/2022/09/23/hcpcs-codes/">post on HCPCS codes</a> too.</p>
<p>HCPCS stands for Healthcare Common Procedure Coding System, and is pronounced &#8220;hick picks.&#8221; When most people say HCPCS, they technically mean HCPCS Level II, and that&#8217;s what I mean here.</p>
<p>The format of a HCPCS code is simple: one letter and four digits. In regex terms,</p>
<pre>    [A-Z]\d{4}</pre>
<p>Not all letters are used, so you can get more specific and say</p>
<pre>    [A-CEGHJ-MP-V][0-9]\d{4}</pre>
<p>Some sources say no codes begin with U, but there are currently five codes that begin with U.</p>
<p>When I was doing some research on HCPCS codes recently using AI, I was told there is a D code for dentistry, but that was a hallucination.</p>
<p>HCPCS codes can also have modifiers. These consist of a letter and either a letter or digit:</p>
<pre>    [A-Z][A-Z0-9]</pre>
<p>Not all letters actually appear in modifiers—I, O, W, and Y are missing—so you could be more specific. At the time of writing there are 384 official modifiers.</p>
<p>Modifiers are often stored in a separate column in a database, but in text you&#8217;ll see a HCPCS code optionally followed by a dash and a modifier. So a regex to match HCPCS codes with possible modifiers would be</p>
<pre>    [A-CEGHJ-MP-V][0-9]\d{4}(-[A-Z][A-Z0-9])?</pre>
<p>This regex will have some false positives, but it should not have false negatives: every real HCPCS code should match.</p>
<p>Of course you could search against a complete list of HCPCS codes. This would be more accurate and slower. I did a test similar to the one in the <a href="https://www.johndcook.com/blog/2026/07/17/regex-speed-error/">previous post</a> and found a search with the regex above took 20 milliseconds, while a search against the list of HCPCS codes took 46 seconds.</p>
<p>However, the regex searched for possible modifiers and the exhaustive search only looked for unmodified HCPCS codes. A complete list of HCPCS codes with possible modifiers would be tedious to create because some combinations of codes and modifiers make no sense. And I imagine that some combinations that would make sense are not used in practice.</p>The post <a href="https://www.johndcook.com/blog/2026/07/17/regular-expressions-for-hcpcs-codes/">Regular expressions for HCPCS codes</a> first appeared on <a href="https://www.johndcook.com/blog">John D. Cook</a>.]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>Regular expression speed and error rates</title>
		<link>https://www.johndcook.com/blog/2026/07/17/regex-speed-error/</link>
					<comments>https://www.johndcook.com/blog/2026/07/17/regex-speed-error/#comments</comments>
		
		<dc:creator><![CDATA[John]]></dc:creator>
		<pubDate>Fri, 17 Jul 2026 11:26:19 +0000</pubDate>
				<category><![CDATA[Computing]]></category>
		<category><![CDATA[Medical codes]]></category>
		<category><![CDATA[Regular expressions]]></category>
		<guid isPermaLink="false">https://www.johndcook.com/blog/?p=247356</guid>

					<description><![CDATA[<p>Seven years ago I wrote a post about regular expressions to match diagnosis codes. I wanted to revisit that post looking at speed and error rates. Regular expressions usually do not exactly match what you&#8217;re looking for and nothing else. They have error false positives and false negatives. But they also have advantages, and context [&#8230;]</p>
The post <a href="https://www.johndcook.com/blog/2026/07/17/regex-speed-error/">Regular expression speed and error rates</a> first appeared on <a href="https://www.johndcook.com/blog">John D. Cook</a>.]]></description>
										<content:encoded><![CDATA[<p><a href="https://www.johndcook.com/blog/2019/05/05/regex_icd_codes/">Seven years ago</a> I wrote a post about regular expressions to match diagnosis codes. I wanted to revisit that post looking at speed and error rates.</p>
<p>Regular expressions usually do not exactly match what you&#8217;re looking for and nothing else. They have error false positives and false negatives. But they also have advantages, and context determines whether the advantages make the error rates tolerable.</p>
<p>The post mentioned above gave the following regular expression for ICD-10 diagnosis codes.</p>
<pre>    [A-TV-Z][0-9][0-9AB]\.?[0-9A-TV-Z]{0,4}</pre>
<p>As cryptic as this may look at first glance, it&#8217;s straight-forward to interpret. It says that an ICD-10 code</p>
<ol>
<li>Begins with a capital letter, excluding U</li>
<li>Followed by a digit</li>
<li>Followed by a digit or A or B</li>
<li>Optionally followed by a period</li>
<li>Followed by up to 4 digits or capital letters, excluding U.</li>
</ol>
<h2>Speed</h2>
<p>Now suppose you want to scan a text document for ICD-10 codes. One approach would be to use the regex above. Another would be to compare every alphanumeric sequence in the document to a list of ICD-10. Currently this list has 74,719 codes. </p>
<p>I tested both approaches on a 800kb text file. The regex search</p>
<pre>    egrep -o '[A-TV-Z][0-9][0-9AB]\.?[0-9A-TV-Z]{0,4}' notes.txt</pre>
<p>took 18 milliseconds. Searching against the list of codes</p>
<pre>    grep -w -F -o -f icd10codes.txt notes.txt</pre>
<p>took 386 seconds, about six and a half minutes or five orders of magnitude longer.</p>
<h2>Error rates</h2>
<p>The regex</p>
<pre>    [A-TV-Z][0-9][0-9AB]\.?[0-9A-TV-Z]{0,4}</pre>
<p>had a false negative rate of zero at the time it was written. I tested the regex against the current list of codes with the following command.</p>
<pre>    egrep -v '[A-TV-Z][0-9][0-9AB]\.?[0-9A-TV-Z]{0,4}' icd10codes.txt</pre>
<p>The <code>-v</code> flag reverses the sense of the search, reporting lines that do not match the regular expression. This returned three matches: U070, U071, and U099. So 3 out of 74,719 valid ICD-10 codes were reported as invalid.</p>
<p>Codes beginning with U are designated for provisional/emergency/special purposes, but these three have become essentially permanent. A change in the application of the ICD-10 standard caused an error in the regular expression. </p>
<p>But the change would also have caused an error in code that did an exhaustive search against the list of ICD-10 codes at the time. In fact, every new code not starting with U would also be reported in error. So the regex is actually more future-proof than an exhaustive search. Presumably the simplified regex</p>
<pre>    [A-Z][0-9][0-9AB]\.?[0-9A-Z]{0,4}</pre>
<p>will remain valid for the foreseeable future.</p>
<p>We&#8217;ve looked at false negatives. What about false positives? That depends on context. The false positive rate when searching medical notes is low: a word matching the regex above in a medical record is most likely an ICD-10 code. But the number of conceivable false positives is enormous. If you were searching a file of randomly generated alphanumeric text, the regex matches would overwhelmingly be false positives [1].</p>
<p>The number of strings matching</p>
<pre>    [A-Z][0-9][0-9AB]\.?[0-9A-Z]{0,4}</pre>
<p>would be</p>
<p style="padding-left: 40px;">26 × 10 × 12 × (1 + 36 + 36<sup>2</sup> + 36<sup>3</sup> + 36<sup>4</sup>) = 5,390,127,600.</p>
<p>Out of over five billion strings matching the regular expression, only around 75,000 are valid ICD-10 codes. So a naive theoretical calculation would say the false positive rate is 99.9986%, whereas in practice the false positive rate is very low, though there&#8217;s no way to say <em>a priori</em> exactly how low.</p>
<h2>Related posts</h2>
<ul>
<li class='link'><a href='https://www.johndcook.com/blog/2022/09/23/hcpcs-codes/'>HCPCS (“hick pics”) codes</a></li>
<li class='link'><a href='https://www.johndcook.com/blog/2024/06/26/npi-number/'>NPI numbers and checksum</a></li>
<li class='link'><a href='https://www.johndcook.com/blog/expert-hipaa-deidentification/'>HIPAA expert determination</a></li>
</ul>
<p>[1] You could argue that all positives would be false positives in this context because you&#8217;re looking at noise. You couldn&#8217;t find an ICD code, though you could find a string of characters that coincides with an ICD code. That may sound like a pedantic distinction, but it matters in the context of evaluating deidentification quality: you want to find instances of PHI, not instances of strings that match the character pattern of PHI.</p>The post <a href="https://www.johndcook.com/blog/2026/07/17/regex-speed-error/">Regular expression speed and error rates</a> first appeared on <a href="https://www.johndcook.com/blog">John D. Cook</a>.]]></content:encoded>
					
					<wfw:commentRss>https://www.johndcook.com/blog/2026/07/17/regex-speed-error/feed/</wfw:commentRss>
			<slash:comments>4</slash:comments>
		
		
			</item>
		<item>
		<title>ICD-10 chapters and code letters</title>
		<link>https://www.johndcook.com/blog/2026/07/14/icd-10-chapters-letters/</link>
		
		<dc:creator><![CDATA[John]]></dc:creator>
		<pubDate>Tue, 14 Jul 2026 14:24:12 +0000</pubDate>
				<category><![CDATA[Uncategorized]]></category>
		<category><![CDATA[Medical codes]]></category>
		<guid isPermaLink="false">https://www.johndcook.com/blog/?p=247340</guid>

					<description><![CDATA[<p>I&#8217;ve been thinking about ICD-10 codes; they come up a lot in my work. The ICD-10-CM standard is divided into 21 chapters, which generally correspond to the first letter of a code. However, a chapter may contain blocks beginning with more than one letter, and codes starting with a single letter, namely D, can span [&#8230;]</p>
The post <a href="https://www.johndcook.com/blog/2026/07/14/icd-10-chapters-letters/">ICD-10 chapters and code letters</a> first appeared on <a href="https://www.johndcook.com/blog">John D. Cook</a>.]]></description>
										<content:encoded><![CDATA[<p>I&#8217;ve been thinking about ICD-10 codes; they come up a lot in my work. </p>
<p>The ICD-10-CM standard is divided into 21 chapters, which <em>generally</em> correspond to the first letter of a code. However, a chapter may contain blocks beginning with more than one letter, and codes starting with a single letter, namely D, can span two chapters.</p>
<p>Here&#8217;s a diagram I made to visualize the relationship between chapters and initial letters of codes.</p>
<p><img loading="lazy" decoding="async" src="https://www.johndcook.com/icd10_chapters_letters.svg" width="400" height="1056" class="aligncenter size-medium" style="background-color:white" /></p>
<p>Notice there&#8217;s no letter U on the diagram. That&#8217;s because U is reserved for special/provisional codes.</p>
<h2>Related posts</h2>
<ul>
<li class='link'><a href='https://www.johndcook.com/blog/2019/04/27/rare-and-strange-icd-10-codes/'>Rare and strange ICD-10 codes</a></li>
<li class='link'><a href='https://www.johndcook.com/blog/2019/05/05/regex_icd_codes/'>Regex to match ICD-9 and ICD-10 codes</a></li>
<li class='link'><a href='https://www.johndcook.com/blog/2021/04/20/new-icd-10-weirdness/'>New ICD-10 code weirdness</a></li>
</ul>The post <a href="https://www.johndcook.com/blog/2026/07/14/icd-10-chapters-letters/">ICD-10 chapters and code letters</a> first appeared on <a href="https://www.johndcook.com/blog">John D. Cook</a>.]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>Posterior variance</title>
		<link>https://www.johndcook.com/blog/2026/07/12/posterior-variance/</link>
		
		<dc:creator><![CDATA[John]]></dc:creator>
		<pubDate>Sun, 12 Jul 2026 19:26:29 +0000</pubDate>
				<category><![CDATA[Statistics]]></category>
		<category><![CDATA[Bayesian]]></category>
		<category><![CDATA[Probability and Statistics]]></category>
		<guid isPermaLink="false">https://www.johndcook.com/blog/?p=247336</guid>

					<description><![CDATA[<p>A few days ago I wrote a post entitled Does additional data always reduce posterior variance?. In a nutshell, the answer is no, not always. That led the previous post which looked at posterior means for three Bayesian models, showing how the posterior mean is a weighted average of the prior mean and the mean [&#8230;]</p>
The post <a href="https://www.johndcook.com/blog/2026/07/12/posterior-variance/">Posterior variance</a> first appeared on <a href="https://www.johndcook.com/blog">John D. Cook</a>.]]></description>
										<content:encoded><![CDATA[<p>A few days ago I wrote a post entitled <a href="https://www.johndcook.com/blog/2026/07/03/does-additional-data-always-reduce-posterior-variance/">Does additional data always reduce posterior variance?</a>. In a nutshell, the answer is no, not always.</p>
<p>That led the <a href="https://www.johndcook.com/blog/2026/07/12/posterior-mean/">previous post</a> which looked at posterior means for three Bayesian models, showing how the posterior mean is a weighted average of the prior mean and the mean of the new data. The weights are <em>precisions</em>, which means something different for each model.</p>
<p>For the beta-binomial model, variance may increase when seeing unexpected data (details <a href="https://www.johndcook.com/blog/2026/07/03/does-additional-data-always-reduce-posterior-variance/">here</a>), but precision always increases.</p>
<p>For the normal-normal model precision is the reciprocal of variance. Every new data point makes precision go up and posterior variance go down.</p>
<p>The Poisson-gamma model may be the most interesting. As stated in the previous post, if data has a Poisson distribution with parameter λ, and λ has a gamma(α<sub>0</sub>, β<sub>0</sub>) prior distribution, then the posterior distribution on λ after observing <em>k</em> events over time <em>t</em> has a gamma(α<sub>0</sub> + <em>k</em>, β<sub>0</sub> + <em>t</em>) posterior distribution. Therefore the posterior variance is</p>
<p style="padding-left: 40px;">(α<sub>0</sub> + <em>k</em>) / (β<sub>0</sub> + <em>t</em>)².</p>
<p>Note the posterior variance is an increasing function of <em>k</em> and a decreasing function of <em>t</em>. This means that the posterior variance increases <em>every time</em> an event is observed, and it decreases quadratically between observations.</p>
<p>Here&#8217;s an illustration. I simulated data from a Poisson process with λ and used a gamma(1, 1) prior on λ. Here&#8217;s a plot of the posterior variance.<img loading="lazy" decoding="async" class="aligncenter size-medium" src="https://www.johndcook.com/posterior_variance.png" width="480" height="360" /></p>The post <a href="https://www.johndcook.com/blog/2026/07/12/posterior-variance/">Posterior variance</a> first appeared on <a href="https://www.johndcook.com/blog">John D. Cook</a>.]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>Posterior mean</title>
		<link>https://www.johndcook.com/blog/2026/07/12/posterior-mean/</link>
		
		<dc:creator><![CDATA[John]]></dc:creator>
		<pubDate>Sun, 12 Jul 2026 17:48:14 +0000</pubDate>
				<category><![CDATA[Statistics]]></category>
		<category><![CDATA[Bayesian]]></category>
		<guid isPermaLink="false">https://www.johndcook.com/blog/?p=247322</guid>

					<description><![CDATA[<p>Common sense says that what you believe after seeing new data should be some sort of compromise between what you believed before and what the new data says. You don&#8217;t want to ignore previous information or new information. How much should new data change your prior beliefs? When prior judgment and new information are in [&#8230;]</p>
The post <a href="https://www.johndcook.com/blog/2026/07/12/posterior-mean/">Posterior mean</a> first appeared on <a href="https://www.johndcook.com/blog">John D. Cook</a>.]]></description>
										<content:encoded><![CDATA[<p>Common sense says that what you believe after seeing new data should be some sort of compromise between what you believed before and what the new data says. You don&#8217;t want to ignore previous information or new information.</p>
<p>How much should new data change your prior beliefs? When prior judgment and new information are in conflict, which one should be given the benefit of the doubt?</p>
<p>Bayesian data models provide a framework for making such decisions quantitative and objective. The choice of a data model is somewhat subjective—whether it&#8217;s a Bayesian model or not—but given a Bayesian model, the rules for updating the representation of your beliefs are objective. As some put it, you &#8220;turn the Bayesian crank.&#8221; A likelihood model and a prior on parameters together specify how new data changes the prior distribution into a posterior distribution.</p>
<p>We will make this more concrete with three examples.</p>
<h2>Normal-normal model</h2>
<p>Suppose that data <em>X</em> has a normal distribution with unknown mean μ and known variance σ², and we assume that <em>a priori</em> μ has a normal distribution with mean μ<sub>0</sub> and variance σ<sub>0</sub>².</p>
<p>After observing <em>x</em>, the posterior distribution on μ also has a normal distribution, but with a different mean and variance. Its mean is somewhere between the prior mean and <em>x</em>. We will ignore the change in the variance for this post.</p>
<p>The posterior mean of μ is</p>
<p><img loading="lazy" decoding="async" class="aligncenter" style="background-color: white;" src="https://www.johndcook.com/postmean1.svg" alt="\mu_{\text{post}} = \frac{\dfrac{\mu_0}{\sigma_0^2} + \dfrac{x}{\sigma^2}}{\dfrac{1}{\sigma_0^2} + \dfrac{1}{\sigma^2}}" width="128" height="103" /></p>
<p>This equation becomes more understandable when we introduce precisions τ = 1/σ² and τ<sub>0</sub> = 1/σ<sub>0</sub>².</p>
<p>Then we have</p>
<p><img loading="lazy" decoding="async" class="aligncenter" style="background-color: white;" src="https://www.johndcook.com/postmean2.svg" alt="\mu_{\text{post}} = \frac{\mu_0 \cdot \tau_0 + x \cdot \tau}{\tau_0 + \tau}" width="163" height="41" /></p>
<p>which you can read as saying the posterior mean is the weighted average of the prior mean and <em>x</em>, with the weights given by the precision. Intuitively, you take the weighted mean of your conclusions from previous data and new data, weighting the mean according to how much confidence you have in each.</p>
<h2>Beta-binomial model</h2>
<p>Now let&#8217;s switch over to a different data model. Now assume <em>X</em> is a binary random variable, with probability of success <em>p</em> and probability of failure 1 − <em>p</em>, and we assume <em>p</em> has a beta(<em>a</em>, <em>b</em>) distribution.</p>
<p>After observing <em>s</em> successes and <em>f</em> failures, the posterior mean of the distribution on <em>p</em> becomes</p>
<p><img loading="lazy" decoding="async" class="aligncenter" style="background-color: white;" src="https://www.johndcook.com/postmean3.svg" alt="p_{\text{post}} = \frac{a + s}{a + b + s + f}" width="164" height="42" /></p>
<p>We can rewrite this as</p>
<p><img loading="lazy" decoding="async" class="aligncenter" style="background-color: white;" src="https://www.johndcook.com/postmean4.svg" alt="p_{\text{post}} = \frac{(a + b) \dfrac{a}{a+b} + (s + f) \dfrac{s}{s+f}}{(a + b) + (s +f)}" width="286" height="68" /></p>
<p>This says that the posterior mean is the weighted average of the prior mean <em>a</em>/(<em>a</em> + <em>b</em>) and the mean of the data <em>s</em>/<em>n</em>. The weights are the prior effective sample size <em>a</em> + <em>b</em> and the sample size of the new data <em>n</em>. In this example (effective) sample size is playing the role that precision played in the normal-normal model above.</p>
<h2>Gamma-Poisson model</h2>
<p>Suppose data have a Poisson distribution with parameter λ, and λ has a gamma(α<sub>0</sub>, β<sub>0</sub>) prior distribution [1]. And suppose you observe <em>k</em> events over time <em>t</em>. Then the posterior distribution of λ given the data has a gamma(α<sub>0</sub> + <em>k</em>, β<sub>0</sub> + <em>t</em>) prior distribution and the mean of the posterior distribution is given by</p>
<p><img loading="lazy" decoding="async" class="aligncenter" style="background-color: white;" src="https://www.johndcook.com/postmean5.svg" alt="\lambda_{\text{post}} = \frac{\alpha_0 + k}{\beta_0 + t} = \frac{\beta_0 (\alpha_0 / \beta_0) + t (k / t)}{\beta_0 + t}" width="291" height="46" /></p>
<p>As before, the posterior mean is a weighted average of the prior mean and new data, and the weights are interpretable as some sort of measure of confidence, namely time. The variable <em>t</em> is directly time and the parameter β<sub>0</sub> is sort of an effective time, just as <em>a</em> + <em>b</em> is an effective sample size for the beta distribution.</p>
<h2>Common thread</h2>
<p>In each example the posterior mean is the weighted average of the prior mean and the mean of the data, with the weights given by a precision. However, precision means something different in each example. In the normal-normal model, precision is the reciprocal of variance, but in the beta-binomial model precision is sample size and in the Poisson-gamma model precision is time.</p>
<p>What all three examples have in common is that they are conjugate models using distributions from the &#8220;exponential family&#8221; of probability distributions. In technical terms, precision is the multiplicative factor on the sufficient statistic in the exponent of the posterior kernel.</p>
<h2>Related posts</h2>
<ul>
<li class="link"><a href="https://www.johndcook.com/blog/2026/07/03/does-additional-data-always-reduce-posterior-variance/">Does additional data always reduce posterior variance?</a></li>
<li class="link"><a href="https://www.johndcook.com/blog/distribution_chart/">Diagram of probability distribution relationships</a></li>
<li class="link"><a href="https://www.johndcook.com/blog/bayesian-consulting/">Bayesian statistics consulting</a></li>
</ul>
<p>[1] There are multiple conventions for parameterizing the gamma distribution. Here we&#8217;re using the shape-rate parameterization, where the mean is α/β.</p>The post <a href="https://www.johndcook.com/blog/2026/07/12/posterior-mean/">Posterior mean</a> first appeared on <a href="https://www.johndcook.com/blog">John D. Cook</a>.]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>Progress on Gilbreath&#8217;s conjecture</title>
		<link>https://www.johndcook.com/blog/2026/07/11/progress-on-gilbreaths-conjecture/</link>
		
		<dc:creator><![CDATA[John]]></dc:creator>
		<pubDate>Sat, 11 Jul 2026 21:30:11 +0000</pubDate>
				<category><![CDATA[Math]]></category>
		<category><![CDATA[Number theory]]></category>
		<guid isPermaLink="false">https://www.johndcook.com/blog/?p=247319</guid>

					<description><![CDATA[<p>Years ago I wrote about Gilbreath&#8217;s conjecture. It&#8217;s a simple conjecture; you could explain it to anyone who understands what prime numbers are. See the linked post for a description of the problem. Gilbreath&#8217;s conjecture is simple, but it&#8217;s also kinda weird. As I wrote before, Paul Erdős speculated that Gilbreath’s conjecture is true but [&#8230;]</p>
The post <a href="https://www.johndcook.com/blog/2026/07/11/progress-on-gilbreaths-conjecture/">Progress on Gilbreath’s conjecture</a> first appeared on <a href="https://www.johndcook.com/blog">John D. Cook</a>.]]></description>
										<content:encoded><![CDATA[<p>Years ago I wrote about <a href="https://www.johndcook.com/blog/2009/09/09/gilbreath-conjecture/">Gilbreath&#8217;s conjecture</a>. It&#8217;s a simple conjecture; you could explain it to anyone who understands what prime numbers are. See the linked post for a description of the problem.</p>
<p>Gilbreath&#8217;s conjecture is simple, but it&#8217;s also kinda weird. As I wrote before,</p>
<blockquote><p>Paul Erdős speculated that Gilbreath’s conjecture is true but it would be 200 years before anyone could prove it. I find Erdős’s conjecture more interesting than Gilbreath’s conjecture.</p></blockquote>
<p>The conjecture is hard in a way that, say, solving a nasty-looking differential equation is not. Over the last three centuries, mathematics has developed quite a toolbox for solving differential equations. But Gilbreath&#8217;s conjecture is just odd enough that it&#8217;s not at all clear what kind of tool might be useful in approaching it.</p>
<p>Terence Tao has a <a href="https://terrytao.wordpress.com/2026/07/11/gilbreaths-conjecture-a-cramer-random-model-and-a-deterministic-analysis/">new blog post</a> announcing a <a href="https://arxiv.org/abs/2607.08712">paper</a> that he and two coauthors wrote on a random model intended to mimic Gilbreath&#8217;s calculation on primes. This random model is more sophisticated than the little game Gilbreath was playing, but it&#8217;s also much more amenable to analysis by established techniques. Tao&#8217;s post gives a heuristic explanation for why Gilbreath&#8217;s conjecture is plausible, but then adds</p>
<blockquote><p>However, it seems well beyond current technology to try to make these heuristics rigorous; even the first step … is far out of reach.</p></blockquote>
<h2>Related posts</h2>
<ul>
<li class="link"><a href="https://www.johndcook.com/blog/2025/07/29/moessners-magic/">Moessner’s Magic</a></li>
<li class="link"><a href="https://www.johndcook.com/blog/2024/04/25/closed-form-pde/">Closed form solutions to PDEs</a></li>
</ul>The post <a href="https://www.johndcook.com/blog/2026/07/11/progress-on-gilbreaths-conjecture/">Progress on Gilbreath’s conjecture</a> first appeared on <a href="https://www.johndcook.com/blog">John D. Cook</a>.]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>Reproducing a geometry theorem diagram</title>
		<link>https://www.johndcook.com/blog/2026/07/06/arc-hypotenuse/</link>
					<comments>https://www.johndcook.com/blog/2026/07/06/arc-hypotenuse/#comments</comments>
		
		<dc:creator><![CDATA[John]]></dc:creator>
		<pubDate>Mon, 06 Jul 2026 14:22:35 +0000</pubDate>
				<category><![CDATA[Math]]></category>
		<category><![CDATA[Geometry]]></category>
		<guid isPermaLink="false">https://www.johndcook.com/blog/?p=247315</guid>

					<description><![CDATA[<p>I ran across a geometry theorem with the following diagram. The theorem corresponding to the diagram is interesting, but I found reproducing the diagram more interesting. The segment AB is a diameter and the line CD is perpendicular to the diameter. Assume the outer circle is a unit circle. I guessed C = (cos(1), sin(1)) and made the [&#8230;]</p>
The post <a href="https://www.johndcook.com/blog/2026/07/06/arc-hypotenuse/">Reproducing a geometry theorem diagram</a> first appeared on <a href="https://www.johndcook.com/blog">John D. Cook</a>.]]></description>
										<content:encoded><![CDATA[<p>I ran across a geometry theorem with the following diagram.</p>
<p><img loading="lazy" decoding="async" class="aligncenter size-medium" src="https://www.johndcook.com/three_circles1.png" width="407" height="360" /></p>
<p>The theorem corresponding to the diagram is interesting, but I found reproducing the diagram more interesting.</p>
<p>The segment <em>AB</em> is a diameter and the line <em>CD</em> is perpendicular to the diameter.</p>
<p>Assume the outer circle is a unit circle. I guessed <em>C</em> = (cos(1), sin(1)) and made the following diagram.</p>
<p><img loading="lazy" decoding="async" class="aligncenter size-medium" src="https://www.johndcook.com/three_circles2.png" width="360" height="360" /></p>
<p>I guessed the value of <em>C</em> by eyeballing it, but in retrospect this would have been a convenient value for the creator of the original diagram to have chosen.</p>
<p>Drawing the blue circle inscribed in the triangle was easy using the equations for the center and radius from <a href="https://www.johndcook.com/blog/2026/06/25/incircle-excircle/">this post</a>. Drawing the other two circles, the green and orange circles, was harder. They are also inscribed circles, but not inscribed in a triangle. They&#8217;re inscribed in a three-sided figure with two perpendicular sides and a circular arc.</p>
<p>The radius <em>r</em> of the green circle is the distance from the center of the circle to each of its tangent lines. Also, the distance from the origin to the center of the circle must be 1 − <em>r</em>. This is enough information to set up a quadratic equation for <em>r</em>. The same reasoning applies to the orange circle.</p>
<p>The original diagram comes from [1] and the theorem it illustrates says the diameter of the blue circle equals the sum of the radii of the green and orange circles.</p>
<h2>Python code</h2>
<p>In case you&#8217;re interested, here&#8217;s the code that created the diagram.</p>
<pre>
#!/usr/bin/env -S uv run --script

# /// script
# dependencies = ["numpy", "matplotlib"]
# ///

import numpy as np
import matplotlib.pyplot as plt

def connect(A, B, color='gray'):
    plt.plot([A[0], B[0]], [A[1], B[1]], color=color, linewidth=2)

def circle(c, r, color='gray'):
    t = np.linspace(0, 2*np.pi)
    plt.plot(c[0] + r*np.cos(t), c[1] + r*np.sin(t), color=color, linewidth=2)

def quadratic(a, b, c):
    det = b**2 - 4*a*c
    return ((-b - det**0.5)/(2*a), (-b + det**0.5)/(2*a))

A = np.array([-1, 0])
B = np.array([ 1, 0])
C = np.array([np.cos(1), np.sin(1)])
a = np.linalg.norm(B - C)
b = np.linalg.norm(A - C)
c = np.linalg.norm(B - A)
s = (a + b + c)/2

circle([0,0], 1)
connect(A, B,)
connect(A, C)
connect(C, B)
connect(C, C*np.array([1, -1]))

center = (a*A + b*B + c*C)/(2*s)
radius = 0.5*a*b/s
circle(center, radius, 'C0')

Ex = C[0]
roots = quadratic(1, 2 + 2*Ex, Ex**2 - 1)
r = roots[1] # Smaller root is negaive
print(roots)
center = (r + Ex, -r)
circle(center, r, 'C1')

roots = quadratic(1, 2 - 2*Ex, Ex**2 - 1)
r = roots[1] # Smaller root is negaive
center = (Ex - r, -r)
circle(center, r, 'C2')

plt.gca().set_aspect("equal")
plt.axis("off")
plt.show()
</pre>
<p>[1] Leon Bankoff. A Geometrical Coincidence. Mathematics Magazine, Vol. 37, No. 5 (Nov., 1964), p. 324.</p>The post <a href="https://www.johndcook.com/blog/2026/07/06/arc-hypotenuse/">Reproducing a geometry theorem diagram</a> first appeared on <a href="https://www.johndcook.com/blog">John D. Cook</a>.]]></content:encoded>
					
					<wfw:commentRss>https://www.johndcook.com/blog/2026/07/06/arc-hypotenuse/feed/</wfw:commentRss>
			<slash:comments>1</slash:comments>
		
		
			</item>
	</channel>
</rss>
