"""VMS + NFW + MOND analysis for SPARC rotation curvesFixed units, safe file loading, correct statistics (Nov 2025 version)"""import numpy as npimport pandas as pdfrom scipy.optimize import curve_fitimport glob# =============================================================================# Constants & units (SPARC standard system)# =============================================================================G_KPC = 4.302e-6          # G in kpc (km/s)? / M?   (do NOT apply any further conversion!)A0 = 1.2e-10              # Standard MOND acceleration in m/s? (fixed for all galaxies)# =============================================================================# Data loading Ð handles both 5-column (no bulge) and 6-column SPARC files# =============================================================================def load_sparc_galaxy(galaxy_name: str, data_dir: str = './sparc_data/') -> pd.DataFrame:    pattern = f"{data_dir}*{galaxy_name}*.dat"    files = glob.glob(pattern)    if not files:        raise FileNotFoundError(f"No data file found for {galaxy_name}")    filepath = files[0]    raw = pd.read_csv(filepath, delim_whitespace=True, comment='#', header=None)    if raw.shape[1] == 5:        raw.columns = ['Rad', 'Vobs', 'errV', 'Vgas', 'Vdisk']        raw['Vbul'] = 0.0    elif raw.shape[1] == 6:        raw.columns = ['Rad', 'Vobs', 'errV', 'Vgas', 'Vdisk', 'Vbul']    else:        raise ValueError(f"Unexpected column count in {filepath}")    data = raw.dropna().reset_index(drop=True)    print(f"Loaded {galaxy_name}: {len(data)} points")    return data# =============================================================================# Velocity models# =============================================================================def vms_velocity(r: np.ndarray, rho0: float, rs: float) -> np.ndarray:    """VMS (Zhao 1996, n=0) spherical profile Ð Eq. (4) in the paper"""    v_squared = 4 * np.pi * G_KPC * rho0 * r / (r + rs)    return np.sqrt(np.abs(v_squared))          # abs just in case of tiny negative floatsdef nfw_mass(r: np.ndarray, rho_s: float, r_s: float) -> np.ndarray:    x = r / r_s    return 4 * np.pi * rho_s * r_s**3 * (np.log(1 + x) - x / (1 + x))def nfw_velocity(r: np.ndarray, rho_s: float, r_s: float) -> np.ndarray:    r_safe = np.maximum(r, 1e-10)  # Guard against r=0 division    M = nfw_mass(r_safe, rho_s, r_s)    return np.sqrt(G_KPC * M / r_safe)def mond_velocity(r: np.ndarray, Vbar: np.ndarray, a0: float = A0) -> np.ndarray:    """Flat-space interpolating function MOND Ð v? = Vbar? a? r"""    r_m = r * 3.085677581e19          # kpc ? metres    Vbar_ms = Vbar * 1e3    v_ms = (Vbar_ms**2 * a0 * r_m)**0.25    return v_ms / 1e3# =============================================================================# Statistics# =============================================================================def calculate_statistics(v_obs, v_model, v_err, n_params: int):    residuals = v_obs - v_model    chi2 = np.sum((residuals / v_err)**2)    dof = len(v_obs) - n_params    chi2_red = np.nan if dof <= 0 else chi2 / dof    rms = np.sqrt(np.mean(residuals**2))    return {'chi2': chi2, 'chi2_red': chi2_red, 'rms': rms, 'dof': dof}# =============================================================================# Fitting functions (one free parameter each)# =============================================================================def fit_vms(r: np.ndarray, v_obs: np.ndarray, v_err: np.ndarray, rs: float, v_flat: float, v_bar: np.ndarray):    rho0_guess = v_flat**2 / (4 * np.pi * G_KPC * rs)    def model(r, rho0):        v_dm = vms_velocity(r, rho0, rs)        return np.sqrt(v_dm**2 + v_bar**2)    popt, pcov = curve_fit(model, r, v_obs, p0=[rho0_guess], sigma=v_err, bounds=(0, np.inf), absolute_sigma=True)    return popt[0], np.sqrt(pcov[0][0])def fit_nfw(r: np.ndarray, v_obs: np.ndarray, v_err: np.ndarray, r_s: float, v_flat: float, v_bar: np.ndarray):    # Rough asymptotic guess: v_flat? Å 4¹ G ?_s r_s? / ln(2) or similar; this is heuristic    rho_s_guess = v_flat**2 / (4 * np.pi * G_KPC * r_s**2 * np.log(2))    def model(r, rho_s):        v_dm = nfw_velocity(r, rho_s, r_s)        return np.sqrt(v_dm**2 + v_bar**2)    popt, pcov = curve_fit(model, r, v_obs, p0=[rho_s_guess], sigma=v_err, bounds=(0, np.inf), absolute_sigma=True)    return popt[0], np.sqrt(pcov[0][0])# =============================================================================# Example / single-galaxy test# =============================================================================if __name__ == '__main__':    galaxy = 'UGC02259'          # change as needed    df = load_sparc_galaxy(galaxy)    r = df['Rad'].values    v_obs = df['Vobs'].values    v_err = np.maximum(df['errV'].values, 0.5)  # Tiny floor to avoid zero errors    # Baryonic velocity (quadratic sum Ð standard in SPARC papers)    v_bar = np.sqrt(df['Vgas']**2 + df['Vdisk']**2 + df['Vbul']**2)    # MOND prediction (0 free parameters)    v_mond = mond_velocity(r, v_bar)    mond_stats = calculate_statistics(v_obs, v_mond, v_err, n_params=0)    # Very rough guesses for scale radii & flat velocity    v_flat = np.median(v_obs[-5:]) if len(v_obs) >= 5 else np.max(v_obs)  # last few points Å flat part    rs_guess = np.max(r) / 3.0          # reasonable starting guess    # VMS fit (1 free parameter: ??)    rho0, rho0_err = fit_vms(r, v_obs, v_err, rs_guess, v_flat, v_bar)    v_dm_vms = vms_velocity(r, rho0, rs_guess)    v_vms = np.sqrt(v_dm_vms**2 + v_bar**2)    vms_stats = calculate_statistics(v_obs, v_vms, v_err, n_params=1)    # NFW fit (1 free parameter: ?_s)    rho_s, rho_s_err = fit_nfw(r, v_obs, v_err, rs_guess, v_flat, v_bar)    v_dm_nfw = nfw_velocity(r, rho_s, rs_guess)    v_nfw = np.sqrt(v_dm_nfw**2 + v_bar**2)    nfw_stats = calculate_statistics(v_obs, v_nfw, v_err, n_params=1)    print("\nResults for", galaxy)    print(f"MOND ??_red = {mond_stats['chi2_red']:.3f}  (rms = {mond_stats['rms']:.2f} km/s)")    print(f"VMS  ??_red = {vms_stats['chi2_red']:.3f}   ?? = {rho0:.2e} ± {rho0_err:.2e} M?/kpc?")    print(f"NFW  ??_red = {nfw_stats['chi2_red']:.3f}   ?_s = {rho_s:.2e} ± {rho_s_err:.2e} M?/kpc?")