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)$$

In [1]:
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.

In [2]:
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.

In [3]:
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).

In [4]:
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¶

In [5]:
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)")
No description has been provided for this image
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.