codex/gravitational-constant-scale-and-sensitivity.py

gravitational-constant-scale-and-sensitivity.py
"""Original Newtonian model diagram; Python 3.14, matplotlib 3.10.7, numpy 2.3.5.

Run from _media. G is the 2022 CODATA recommended value verified in NIST's
complete listing, https://physics.nist.gov/cuu/Constants/Table/allascii.txt.
The curves are model predictions and inference biases, never experimental data.
"""

from pathlib import Path

import matplotlib.pyplot as plt
import numpy as np


def main():
    plt.rcParams.update({"font.size": 11, "axes.spines.top": False,
                         "axes.spines.right": False, "figure.facecolor": "white"})
    fig, (force_ax, bias_ax) = plt.subplots(1, 2, figsize=(10.5, 4.9))
    fig.subplots_adjust(left=0.085, right=0.975, bottom=0.19, top=0.82, wspace=0.35)
    fig.suptitle("Gravitational constant: signal scale and model sensitivity", y=0.965, fontsize=15)
    gravity_constant = 6.67430e-11
    separation = np.geomspace(0.1, 1.0, 300)
    force_nn = gravity_constant / separation**2 * 1e9
    force_ax.loglog(separation, force_nn, color="#175a91", lw=2.5)
    force_ax.set(title="Two 1 kg point masses", xlabel="Actual center separation r (m)",
                 ylabel="Predicted attraction F (nN)", xlim=(0.085, 1.16), ylim=(0.045, 11))
    force_ax.set_xticks([0.1, 0.2, 0.5, 1.0], labels=["0.1", "0.2", "0.5", "1"])
    force_ax.set_yticks([0.1, 1, 10], labels=["0.1", "1", "10"])
    force_ax.minorticks_off()
    force_ax.grid(alpha=0.2)
    force_ax.scatter([0.1, 1], [6.67430, 0.0667430], color="#175a91", s=32, zorder=4)
    force_ax.annotate("6.6743 nN", (0.1, 6.6743), xytext=(12, -5), textcoords="offset points")
    force_ax.annotate("0.066743 nN", (1, 0.066743), xytext=(-104, 14), textcoords="offset points")
    force_ax.text(0.31, 0.48, r"$F=GMm/r^2$", fontsize=14, transform=force_ax.transAxes)

    separation_error_um = np.linspace(-10, 10, 401)
    true_separation = 0.1
    inferred_ratio = (1 + separation_error_um * 1e-6 / true_separation)**2
    bias_ppm = (inferred_ratio - 1) * 1e6
    bias_ax.plot(separation_error_um, bias_ppm, color="#c15716", lw=2.5)
    bias_ax.axhline(0, color="0.65", lw=0.8)
    bias_ax.axvline(0, color="0.65", lw=0.8)
    bias_ax.scatter([1], [(1.00001**2 - 1) * 1e6], color="#c15716", zorder=4)
    bias_ax.annotate("+1 μm → +20.0001 ppm", (1, 20.0001), xytext=(-125, 51),
                     textcoords="offset points", arrowprops={"arrowstyle": "->", "color": "0.3"})
    bias_ax.set(title="Same force; wrong entered distance", xlabel="Entered separation error δr (μm)",
                ylabel="Bias in inferred G (ppm)", xlim=(-10, 10), ylim=(-225, 225))
    bias_ax.text(0.08, 0.08, r"$G_{\rm inferred}/G_{\rm true}=(1+\delta r/r)^2$" + "\n" + r"True $r=0.100$ m; $F,M,m$ fixed",
                 transform=bias_ax.transAxes, fontsize=11,
                 bbox={"facecolor": "white", "edgecolor": "none", "pad": 3})
    bias_ax.grid(alpha=0.2)
    fig.text(0.5, 0.045, "Ideal point-mass calculations • 2022 CODATA G • no measurement data", ha="center", color="0.35", fontsize=10)
    fig.savefig(Path(__file__).stem + ".png", dpi=100, facecolor="white")
    plt.close(fig)


if __name__ == "__main__":
    main()