Files
MeanField/experiments/check_n3_mass_compatibility.py

86 lines
3.7 KiB
Python
Raw Normal View History

#!/usr/bin/env python3
"""Independent n=3 Lane–Emden integration; no FEM or production code changes."""
from fractions import Fraction
from pathlib import Path
import json
import math
import sys
import scipy
from scipy.integrate import solve_ivp
def central_series(x, terms=10):
# Recurrence from theta'' + 2 theta'/x + theta^3 = 0 avoids
# a singular initial-value evaluation and a hand-transcribed series.
coeff = [Fraction(1)]
for k in range(terms - 1):
cube = sum(
coeff[i] * coeff[j] * coeff[k-i-j]
for i in range(k+1) for j in range(k-i+1)
)
coeff.append(-cube / ((2*k+2)*(2*k+3)))
theta = math.fsum(float(a)*x**(2*k) for k, a in enumerate(coeff))
mu = -math.fsum(2*k*float(a)*x**(2*k+1) for k, a in enumerate(coeff) if k)
return theta, mu
def solve(rtol, atol, maximum_step, start):
def rhs(x, y):
theta, mu = y
return -mu / x**2, x**2 * theta**3
def zero(x, y):
return y[0]
zero.terminal = True
zero.direction = -1
result = solve_ivp(rhs, (start, 10.0), central_series(start),
events=zero, method="DOP853", rtol=rtol, atol=atol,
max_step=maximum_step)
if not result.success or len(result.t_events[0]) != 1:
raise RuntimeError(result.message)
xi = float(result.t_events[0][0])
mu = float(result.y_events[0][0, 1])
kcrit = math.pi / (4.0*math.pi*mu)**(2.0/3.0)
rho_for_unit_radius = xi**3/(4.0*math.pi*mu)
mass = 4.0*math.pi*(driver_k/math.pi)**1.5*mu
radius = xi*math.sqrt(driver_k/math.pi)*driver_rho**(-1.0/3.0)
return dict(rtol=rtol, atol=atol, maximum_step=maximum_step, start=start,
steps=len(result.t)-1, function_evaluations=result.nfev,
xi1=xi, mu1=mu, K_for_mass_one=kcrit,
rho_c_for_mass_and_radius_one=rho_for_unit_radius,
M_LE_at_driver_K=mass, M_target_minus_M_LE=1.0-mass,
R_LE_at_driver_K_and_rho_c=radius,
K_driver_relative_to_critical_minus_one=driver_k/kcrit-1.0,
virial_bound_T_over_absW_max=0.5*(1.0-mass**(2.0/3.0)))
metadata = Path(sys.argv[1]).resolve()
values = dict(line.split("=", 1) for line in metadata.read_text().splitlines() if "=" in line)
if float(values["n"]) != 3.0 or float(values["M"]) != 1.0:
raise ValueError("This diagnostic uses the campaign's n=3, M=1, G=1 convention")
driver_k = float(values["K"])
driver_rho = float(values["rho_c"])
checks = [solve(3e-12, 1e-14, 0.2, 1e-3),
solve(3e-13, 1e-15, 0.1, 1e-3),
solve(3e-14, 1e-16, 0.05, 1e-3),
solve(3e-14, 1e-16, 0.025, 1e-2),
solve(3e-14, 1e-16, 0.025, 1e-4)]
mu = checks[-1]["mu1"]
def mass_for_k(k):
return 4.0*math.pi*(k/math.pi)**1.5*mu
out = dict(metadata=str(metadata), python=sys.version, scipy=scipy.__version__,
convention="G=1; theta'= -mu/xi^2; mu'=xi^2 theta^3; first theta zero",
driver={name: values[name] for name in ("n", "K", "rho_c", "M", "J", "xi1", "mu1")},
checks=checks,
mass_check_spread=max(x["M_LE_at_driver_K"] for x in checks)-min(x["M_LE_at_driver_K"] for x in checks),
full_metadata_K_binary64_ulp=math.ulp(driver_k),
M_LE_for_K_adjacent_binary64=[mass_for_k(math.nextafter(driver_k, -math.inf)),
mass_for_k(math.nextafter(driver_k, math.inf))],
K_quoted_12_decimal_rounding_interval=[0.3639390599515, 0.3639390599525],
M_LE_for_K_quoted_rounding_interval=[mass_for_k(0.3639390599515), mass_for_k(0.3639390599525)],
limitation="Tolerance/step/start sensitivity check, not an interval-arithmetic error certificate.")
print(json.dumps(out, indent=2))