codexnail-kernel-centroid-ambiguity.py
nail-kernel-centroid-ambiguity.py
"""Two original normalized material-age kernels with the same centroid.
Python 3.14, NumPy 2.3.5, Matplotlib 3.10.7. Writes PNG into cwd.
The plotted kernels are hypothetical reference-crossing ages, not exposure data.
"""
from pathlib import Path
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
from matplotlib.patches import Rectangle
def moments(windows):
"""Exact mean and variance for (lower age, upper age, mass fraction)."""
assert abs(sum(weight for _, _, weight in windows) - 1.0) < 1e-12
assert all(upper > lower and weight >= 0 for lower, upper, weight in windows)
mean = sum(weight * (lower + upper) / 2 for lower, upper, weight in windows)
second = sum(weight * (lower**2 + lower * upper + upper**2) / 3
for lower, upper, weight in windows)
return mean, second - mean**2
def main():
cases = [([(0, 20, 1.0)], "Uniform material ages", "#3979a8", 0.08),
([(0, 2, 0.5), (18, 20, 0.5)], "Two pooled, separated segments", "#438354", 0.32)]
fig, axes = plt.subplots(2, 1, figsize=(7.2, 4.8), dpi=100, sharex=True,
facecolor="white")
for ax, (windows, label, color, ymax) in zip(axes, cases):
ax.set_facecolor("white")
mean, variance = moments(windows)
for lower, upper, weight in windows:
ax.add_patch(Rectangle((lower, 0), upper - lower,
weight / (upper - lower), facecolor=color,
edgecolor=color, alpha=0.65))
ax.axvline(mean, color="#9a4a35", ls="--", lw=1.7)
ax.set(xlim=(-0.5, 20.5), ylim=(0, ymax), ylabel="Mass fraction / day")
ax.set_title(f"{label} — centroid 10 days; variance {variance:.2f} days²", fontsize=10)
ax.grid(axis="y", alpha=0.18)
axes[1].text(10, 0.17, "No sampled material\nat the centroid age", ha="center", fontsize=9,
bbox={"facecolor": "white", "edgecolor": "none", "alpha": 0.95})
axes[1].set_xlabel("Material reference-crossing age before collection (days)")
axes[1].set_xticks([0, 2, 5, 10, 15, 18, 20])
fig.suptitle("The same centroid can summarize very different material histories", fontsize=11)
fig.text(0.5, 0.015, "Original normalized kernels: each shaded distribution has total mass fraction 1.\n"
"These are material-age models, not measured exposure histories or clinical dating rules.",
ha="center", fontsize=8)
fig.tight_layout(rect=(0, 0.07, 1, 0.96))
fig.savefig(Path("nail-kernel-centroid-ambiguity.png"), facecolor="white")
plt.close(fig)
if __name__ == "__main__":
main()