Translational Dipole Scaling: A Second, Different "Neutral" Configuration¶
shell_multipole_verification.ipynb tested one specific way of building a charge-neutral
source: two concentric shells of different radius (an inner $-Q$ shell, an outer $+Q$
shell, sharing a center) — motivated by BUT's own bound states, which show radial
charge segregation. That configuration was found to scale as $R^{-3}$, not $R^{-2}$.
This notebook checks a second, textbook-standard kind of neutral configuration: two shells of the same radius, opposite charge, with their centers displaced by a small distance $\delta$ — the ordinary electric-dipole construction (two nearby opposite point charges, generalized to extended sources). This is a genuinely different operation on the source (a spatial derivative, not a radius difference), and it is checked here because the two are not guaranteed to give the same exponent, and in fact do not.
No retardation is assumed here either — this is still a fully static configuration, in the same category the paper's Section VII.D discusses. The result below is therefore a correction to how completely that section's "no static geometry works" conclusion should be read, not a violation of it via some new mechanism — the conclusion needs to be narrowed to the specific kind of neutrality tested, not read as covering every possible static neutral configuration.
1. Method¶
Reuse shell_multipole_verification.ipynb's exact shell-integration method (unchanged,
so results are directly comparable):
$$F_{\text{shell}}(R; a, Q, n) = \frac{kQ}{2}\int_0^{\pi} \sin\theta\,\frac{R - a\cos\theta}{s(\theta)^{n+1}}\,d\theta, \qquad s(\theta)=\sqrt{R^2+a^2-2aR\cos\theta}$$
A translational dipole is two same-radius shells, charge $+Q$ centered at $+\delta/2$ and charge $-Q$ centered at $-\delta/2$ along the test axis, evaluated at distance $R$ from the shared axis origin:
$$F_{\text{trans}}(R) = F_{\text{shell}}(R-\delta/2;a,Q,n) - F_{\text{shell}}(R+\delta/2;a,Q,n)$$
import numpy as np
def shell_force_numeric(R, a, n_power, npts=400_000):
theta = np.linspace(0, np.pi, npts)
s = np.sqrt(R**2 + a**2 - 2*a*R*np.cos(theta))
integrand = np.sin(theta) * (R - a*np.cos(theta)) / s**(n_power + 1)
return 0.5 * np.trapezoid(integrand, theta)
def translational_dipole(R, a, delta, n_power=1):
return shell_force_numeric(R - delta/2, a, n_power) - shell_force_numeric(R + delta/2, a, n_power)
2. Why this should differ from the concentric-shell case: the calculus¶
The concentric radius-difference case cancels the monopole term using the fact that a single shell's own force, expanded in $t=a/R$, is an even function of $t$ — a direct consequence of the shell integral being symmetric under $x \to -x$ (where $x=\cos\theta$) combined with $t \to -t$ leaving the integrand unchanged, so relabeling the (symmetric, $[-1,1]$) integration variable proves $F(R;t) = F(R;-t)$ exactly. An even function of $t$ has only even powers of $t=a/R$ in its Taylor series, so $F_{\text{shell}}(R) = \tfrac{1}{R}\left[c_0 + c_2(a/R)^2 + c_4(a/R)^4 + \dots\right]$ — only odd powers of $1/R$ ever appear ($R^{-1}, R^{-3}, R^{-5}, \dots$). Cancelling $c_0$ (equal charge, different radius) leaves the next available term, $R^{-3}$ — not $R^{-2}$, because $R^{-2}$ was never a term in the series to begin with.
A translational dipole cancels the monopole differently: it doesn't touch the shape's own $(a/R)$ expansion at all. For small $\delta$,
$$F_{\text{trans}}(R) \approx -\delta \frac{d}{dR}F_{\text{shell}}(R;a)$$
which is just the definition of a spatial derivative — and differentiating any smooth function of $R$ shifts its leading power by exactly $1$, regardless of what powers exist in the function being differentiated. Applied to $F_{\text{shell}}(R) \approx Q/R$ (BUT's $n=1$ monopole term), this predicts $F_{\text{trans}}(R) \propto 1/R^2$ — landing exactly on the observed Coulomb/gravity exponent, not on $-3$. Checked below three independent ways: exact numeric quadrature at fixed $\delta$, robustness across a wide range of $\delta/a$ (not just the infinitesimal limit), and an entirely separate discrete point-particle Monte Carlo cross-check.
Rs = np.geomspace(50, 3200, 7)
vals = np.array([translational_dipole(R, a=1.0, delta=0.05, n_power=1) for R in Rs])
slopes = np.log(vals[:-1]/vals[1:]) / np.log(Rs[1:]/Rs[:-1])
print("R values:", np.round(Rs, 1))
print("Local power-law slope between consecutive R values (expect -2):")
for i, s in enumerate(slopes):
print(f" R={Rs[i]:>8.1f} to R={Rs[i+1]:>8.1f}: slope = {-s:.4f}")
fit = np.polyfit(np.log(Rs), np.log(vals), 1)
print(f"\nFar-tail fitted exponent: {fit[0]:.5f} (theory: -2)")
R values: [ 50. 100. 200. 400. 800. 1600. 3200.] Local power-law slope between consecutive R values (expect -2): R= 50.0 to R= 100.0: slope = -1.9996 R= 100.0 to R= 200.0: slope = -1.9999 R= 200.0 to R= 400.0: slope = -2.0000 R= 400.0 to R= 800.0: slope = -2.0000 R= 800.0 to R= 1600.0: slope = -2.0000 R= 1600.0 to R= 3200.0: slope = -2.0000 Far-tail fitted exponent: -1.99993 (theory: -2)
3. Robustness across $\delta/a$¶
The derivative argument above is a small-$\delta$ (point-dipole limit) approximation. Checking whether the $-2$ exponent survives at displacement scales comparable to the shell's own radius (not just $\delta \to 0$) — i.e. whether this is a real asymptotic result or only an idealization that degrades for any physically realistic separation.
Rs_far = np.geomspace(500, 50000, 12)
print(f"{'delta/a':>10} {'fitted exponent':>18}")
for delta_over_a in [0.001, 0.01, 0.1, 0.3, 0.6, 0.9]:
a = 1.0
delta = delta_over_a * a
vals = np.array([translational_dipole(R, a, delta, n_power=1) for R in Rs_far])
fit = np.polyfit(np.log(Rs_far), np.log(np.abs(vals)), 1)
print(f"{delta_over_a:>10.3f} {fit[0]:>18.5f}")
delta/a fitted exponent
0.001 -2.00000
0.010 -2.00000
0.100 -2.00000
0.300 -2.00000
0.600 -2.00000
0.900 -2.00000
The exponent holds at exactly $-2$ across the whole range tested, including $\delta/a=0.9$ (centers displaced by 90% of the shell radius — not an infinitesimal separation). This is not merely a leading-order idealization that degrades at realistic scales.
4. Independent cross-check: discrete point-particle clusters¶
A completely separate method from the continuous shell integral above — direct
brute-force pairwise summation over random point clusters, the same computation
SimulationKernels.metal's Kernel 2 performs, mirroring how but_binary_io.py and the
Swift engine actually represent particles (not a smooth charge density).
rng = np.random.default_rng(7)
def sphere_cluster(n, radius, center):
u = rng.uniform(0, 1, n)
r = radius * u**(1.0/3.0)
theta = rng.uniform(0, 2*np.pi, n)
cosphi = rng.uniform(-1, 1, n)
sinphi = np.sqrt(1 - cosphi**2)
x = r*sinphi*np.cos(theta); y = r*sinphi*np.sin(theta); z = r*cosphi
return np.stack([x, y, z], axis=1) + np.array(center)
def but_force_on_point(src_pos, src_q, point_pos):
d = point_pos - src_pos
r2 = np.maximum(np.sum(d*d, axis=1), 1e-12)
return np.sum((src_q / r2)[:, None] * d, axis=0)
N, a, delta = 400, 1.0, 0.1
posA = sphere_cluster(N, a, (+delta/2, 0, 0)); qA = np.ones(N)
posB = sphere_cluster(N, a, (-delta/2, 0, 0)); qB = -np.ones(N)
src_pos = np.vstack([posA, posB])
src_q = np.concatenate([qA, qB])
Rs2 = np.geomspace(200, 20000, 10)
vals2 = np.array([np.linalg.norm(but_force_on_point(src_pos, -src_q, np.array([[R, 0, 0]]))) for R in Rs2])
fit2 = np.polyfit(np.log(Rs2), np.log(vals2), 1)
print(f"Discrete N={N}-particle-per-sphere translational dipole, fitted exponent = {fit2[0]:.5f} (theory: -2)")
Discrete N=400-particle-per-sphere translational dipole, fitted exponent = -1.99992 (theory: -2)
5. Visual summary¶
import matplotlib.pyplot as plt
R_range = np.geomspace(20, 4000, 40)
monopole = np.array([shell_force_numeric(R, a=1.0, n_power=1) for R in R_range])
concentric_dipole = np.array([shell_force_numeric(R, 1.5, 1) - shell_force_numeric(R, 1.0, 1) for R in R_range])
translational = np.array([translational_dipole(R, a=1.0, delta=0.1, n_power=1) for R in R_range])
def fit_slope(R_range, vals, lo_idx=20):
logR = np.log(R_range[lo_idx:])
logF = np.log(np.abs(vals[lo_idx:]))
return np.polyfit(logR, logF, 1)[0]
s_mono = fit_slope(R_range, monopole)
s_conc = fit_slope(R_range, concentric_dipole)
s_trans = fit_slope(R_range, translational)
fig, ax = plt.subplots(figsize=(7.5, 5.8))
ax.loglog(R_range, monopole, 'o-', label=f'charged shell (monopole), slope={s_mono:.3f}', markersize=3)
ax.loglog(R_range, np.abs(concentric_dipole), 's-', label=f'concentric different-radius dipole, slope={s_conc:.3f}', markersize=3)
ax.loglog(R_range, np.abs(translational), '^-', label=f'translational (positional) dipole, slope={s_trans:.3f}', markersize=3)
ax.loglog(R_range, 1/R_range**2, 'r:', alpha=0.7, label='reference $1/R^2$ (observed Coulomb/gravity)')
ax.set_xlabel('R (test point distance, shell units)')
ax.set_ylabel('Force (arbitrary units)')
ax.set_title("BUT $1/r$ field: two different kinds of 'neutral' give different exponents")
ax.legend(fontsize=8)
ax.grid(True, which='both', alpha=0.3)
plt.tight_layout()
plt.savefig('translational_dipole_scaling.png', dpi=150)
plt.show()
print(f"Monopole slope: {s_mono:.4f} (theory -1)")
print(f"Concentric different-radius dipole slope: {s_conc:.4f} (theory -3, matches shell_multipole_verification.ipynb)")
print(f"Translational dipole slope: {s_trans:.4f} (theory -2, matches observed Coulomb/gravity)")
Monopole slope: -1.0000 (theory -1) Concentric different-radius dipole slope: -3.0000 (theory -3, matches shell_multipole_verification.ipynb) Translational dipole slope: -2.0000 (theory -2, matches observed Coulomb/gravity)
Summary¶
| Configuration | What's held fixed | What differs | Fitted exponent | Matches observed? |
|---|---|---|---|---|
| Charged shell (monopole) | — | — | $-1.00$ | No |
| Concentric, different-radius (core-shell) neutral | center | radius | $-3.00$ | No |
| Translational (positional) dipole | radius | center | $-2.00$ | Yes |
The paper's existing negative result (shell_multipole_verification.ipynb,
coil_multipole_verification.ipynb) is correct as far as it goes, but tested only one
specific way of building a neutral source — the one motivated by BUT's own observed
radial charge segregation. A different, arguably more standard kind of neutral
configuration (simple positional displacement of otherwise-identical opposite charge,
the textbook electric-dipole picture) reproduces the observed $R^{-2}$ exponent exactly,
robustly across displacement scale, and cross-checked via an entirely independent
discrete-particle method.
This narrows, rather than removes, Section VII.D's "static geometry doesn't work"
claim — it shows the claim was true for the specific configuration tested, not for
every static neutral configuration. Whether BUT's actual bound states (per the real
n-body simulation's own observed pairing/segregation behavior — see
long_stability_search_v2's finding of >90% opposite-charge nearest neighbors) look more
like the concentric core-shell picture or the translational-dipole picture is an open,
empirical question this notebook doesn't settle - worth checking directly against real
simulation output before treating this as a resolution rather than a promising lead.