#!/usr/bin/env python3 """ Open Cluster Velocity Dispersion Analysis for Spin-Tether Hypothesis Author: Andre Heinecke & Claude Date: May 2025 This script analyzes velocity dispersions in open clusters to test for the presence of a universal tethering acceleration sigma. """ import numpy as np import pandas as pd import matplotlib.pyplot as plt from astropy import units as u from astropy.constants import G, M_sun, pc import requests from io import StringIO # Constants G_SI = G.si.value # m^3 kg^-1 s^-2 M_sun_kg = M_sun.si.value # kg pc_m = pc.si.value # meters class OpenCluster: """Class to represent an open cluster with its properties""" def __init__(self, name, distance_pc, mass_msun, radius_pc, obs_dispersion_km_s, dispersion_type='3D'): self.name = name self.distance = distance_pc * pc_m # Convert to meters self.mass = mass_msun * M_sun_kg # Convert to kg self.radius = radius_pc * pc_m # Convert to meters self.obs_dispersion = obs_dispersion_km_s * 1000 # Convert to m/s self.dispersion_type = dispersion_type # Calculate expected virial dispersion self.calc_virial_dispersion() def calc_virial_dispersion(self): """Calculate expected velocity dispersion from virial theorem""" # For a spherical cluster: σ_vir = sqrt(G*M/r) # This assumes a uniform density sphere, factor may vary for different profiles self.vir_dispersion = np.sqrt(G_SI * self.mass / (2 * self.radius)) # If observed is line-of-sight, convert virial to line-of-sight if self.dispersion_type == 'LOS': # For isotropic velocities, σ_LOS = σ_3D / sqrt(3) self.vir_dispersion_los = self.vir_dispersion / np.sqrt(3) else: self.vir_dispersion_los = self.vir_dispersion def calc_sigma_tether(self): """Calculate implied tethering acceleration from excess dispersion""" # From spin-tether theory: excess kinetic energy = sigma * r # Excess KE per unit mass = 0.5*(σ_obs^2 - σ_vir^2) # Therefore: sigma = (σ_obs^2 - σ_vir^2) / (2*r) dispersion_to_use = self.vir_dispersion_los if self.dispersion_type == 'LOS' else self.vir_dispersion excess_ke = 0.5 * (self.obs_dispersion**2 - dispersion_to_use**2) self.sigma_tether = excess_ke / self.radius # Also calculate what sigma would be needed to explain ALL the dispersion self.sigma_total = 0.5 * self.obs_dispersion**2 / self.radius return self.sigma_tether # Known open clusters with data from Gaia studies # Data compiled from multiple sources cited in the searches clusters_data = [ # name, distance(pc), mass(Msun), radius(pc), obs_dispersion(km/s), type ("Hyades", 47, 400, 10, 5.0, '3D'), # 3D dispersion for young stars ("Pleiades", 136, 800, 15, 2.4, '3D'), # 3D dispersion from Gaia ("Praesepe", 187, 600, 12, 4.2, 'LOS'), # Line-of-sight dispersion ("Alpha Per", 170, 500, 13, 3.5, '3D'), # Estimated ("IC 2602", 150, 300, 8, 2.8, '3D'), # Estimated ("IC 2391", 150, 250, 7, 2.5, '3D'), # Estimated ("NGC 2451A", 190, 350, 9, 3.2, '3D'), # Estimated ("Coma Ber", 85, 200, 6, 2.0, '3D'), # Small, nearby cluster ] def analyze_clusters(): """Analyze all clusters and calculate sigma values""" clusters = [] for data in clusters_data: cluster = OpenCluster(*data) cluster.calc_sigma_tether() clusters.append(cluster) # Create results dataframe results = pd.DataFrame({ 'Cluster': [c.name for c in clusters], 'Distance (pc)': [c.distance/pc_m for c in clusters], 'Mass (Msun)': [c.mass/M_sun_kg for c in clusters], 'Radius (pc)': [c.radius/pc_m for c in clusters], 'Obs Dispersion (km/s)': [c.obs_dispersion/1000 for c in clusters], 'Virial Dispersion (km/s)': [c.vir_dispersion/1000 for c in clusters], 'Excess Dispersion (km/s)': [(c.obs_dispersion - c.vir_dispersion)/1000 for c in clusters], 'Sigma (m/s²)': [c.sigma_tether for c in clusters], 'Sigma (10⁻¹³ m/s²)': [c.sigma_tether * 1e13 for c in clusters] }) # Calculate statistics mean_sigma = np.mean([c.sigma_tether for c in clusters if c.sigma_tether > 0]) std_sigma = np.std([c.sigma_tether for c in clusters if c.sigma_tether > 0]) print("="*80) print("OPEN CLUSTER VELOCITY DISPERSION ANALYSIS") print("Testing Spin-Tether Hypothesis") print("="*80) print("\nDetailed Results:") print(results.to_string(index=False)) print(f"\n{'='*80}") print("STATISTICAL SUMMARY:") print(f"{'='*80}") print(f"Mean σ_tether for clusters with excess: {mean_sigma:.2e} m/s²") print(f" = {mean_sigma*1e13:.1f} × 10⁻¹³ m/s²") print(f"Standard deviation: {std_sigma:.2e} m/s²") print(f" = {std_sigma*1e13:.1f} × 10⁻¹³ m/s²") # Check consistency with cosmic flow upper limit cosmic_limit = 5e-13 # m/s² from Cosmicflows-4 print(f"\nComparison with Cosmicflows-4 upper limit: < {cosmic_limit*1e13:.0f} × 10⁻¹³ m/s²") print(f"Mean cluster σ is {mean_sigma/cosmic_limit:.1f}× the cosmic upper limit") return clusters, results def plot_results(clusters, results): """Create visualizations of the analysis""" fig, axes = plt.subplots(2, 2, figsize=(14, 10)) # Plot 1: Observed vs Virial dispersions ax1 = axes[0, 0] ax1.scatter(results['Virial Dispersion (km/s)'], results['Obs Dispersion (km/s)'], s=100) # Add 1:1 line max_disp = max(results['Obs Dispersion (km/s)'].max(), results['Virial Dispersion (km/s)'].max()) ax1.plot([0, max_disp], [0, max_disp], 'k--', label='1:1 (no excess)') # Label points for i, txt in enumerate(results['Cluster']): ax1.annotate(txt, (results['Virial Dispersion (km/s)'].iloc[i], results['Obs Dispersion (km/s)'].iloc[i]), xytext=(5, 5), textcoords='offset points', fontsize=8) ax1.set_xlabel('Virial Dispersion (km/s)') ax1.set_ylabel('Observed Dispersion (km/s)') ax1.set_title('Observed vs Expected (Virial) Velocity Dispersions') ax1.legend() ax1.grid(True, alpha=0.3) # Plot 2: Sigma vs cluster size ax2 = axes[0, 1] positive_sigma = results[results['Sigma (10⁻¹³ m/s²)'] > 0] ax2.scatter(positive_sigma['Radius (pc)'], positive_sigma['Sigma (10⁻¹³ m/s²)'], s=100) # Add constant sigma line at mean value mean_sigma_13 = np.mean(positive_sigma['Sigma (10⁻¹³ m/s²)']) ax2.axhline(mean_sigma_13, color='r', linestyle='--', label=f'Mean σ = {mean_sigma_13:.1f}×10⁻¹³ m/s²') # Label points for i in positive_sigma.index: ax2.annotate(results['Cluster'].iloc[i], (results['Radius (pc)'].iloc[i], results['Sigma (10⁻¹³ m/s²)'].iloc[i]), xytext=(5, 5), textcoords='offset points', fontsize=8) ax2.set_xlabel('Cluster Radius (pc)') ax2.set_ylabel('σ_tether (10⁻¹³ m/s²)') ax2.set_title('Tethering Acceleration vs Cluster Size') ax2.legend() ax2.grid(True, alpha=0.3) # Plot 3: Excess dispersion vs mass ax3 = axes[1, 0] ax3.scatter(results['Mass (Msun)'], results['Excess Dispersion (km/s)'], s=100) # Highlight zero line ax3.axhline(0, color='k', linestyle='-', alpha=0.5) # Label points for i, txt in enumerate(results['Cluster']): ax3.annotate(txt, (results['Mass (Msun)'].iloc[i], results['Excess Dispersion (km/s)'].iloc[i]), xytext=(5, 5), textcoords='offset points', fontsize=8) ax3.set_xlabel('Cluster Mass (M☉)') ax3.set_ylabel('Excess Dispersion (km/s)') ax3.set_title('Excess Velocity Dispersion vs Cluster Mass') ax3.set_xscale('log') ax3.grid(True, alpha=0.3) # Plot 4: Test prediction - sigma independent of mass ax4 = axes[1, 1] positive_sigma = results[results['Sigma (10⁻¹³ m/s²)'] > 0] ax4.scatter(positive_sigma['Mass (Msun)'], positive_sigma['Sigma (10⁻¹³ m/s²)'], s=100) # Add constant sigma line ax4.axhline(mean_sigma_13, color='r', linestyle='--', label=f'Mean σ = {mean_sigma_13:.1f}×10⁻¹³ m/s²') # Add shaded region for 1-sigma uncertainty std_sigma_13 = np.std(positive_sigma['Sigma (10⁻¹³ m/s²)']) ax4.fill_between([100, 1000], mean_sigma_13 - std_sigma_13, mean_sigma_13 + std_sigma_13, alpha=0.2, color='red') # Label points for i in positive_sigma.index: ax4.annotate(results['Cluster'].iloc[i], (results['Mass (Msun)'].iloc[i], results['Sigma (10⁻¹³ m/s²)'].iloc[i]), xytext=(5, 5), textcoords='offset points', fontsize=8) ax4.set_xlabel('Cluster Mass (M☉)') ax4.set_ylabel('σ_tether (10⁻¹³ m/s²)') ax4.set_title('KEY TEST: σ Should Be Independent of Mass') ax4.set_xscale('log') ax4.legend() ax4.grid(True, alpha=0.3) plt.tight_layout() plt.savefig('cluster_analysis_results.png', dpi=300, bbox_inches='tight') plt.show() return fig def test_falsifiability(): """Calculate what observations would falsify the spin-tether hypothesis""" print("\n" + "="*80) print("FALSIFIABILITY CRITERIA:") print("="*80) print("\nThe spin-tether hypothesis predicts:") print("1. ALL open clusters should show similar excess dispersion") print("2. σ_tether should be INDEPENDENT of cluster mass") print("3. σ_tether should depend ONLY on cluster size") print("4. Value should be ~3×10⁻¹³ m/s² for ~10 pc clusters") print("\nTo FALSIFY the hypothesis, observations must show:") print("- No systematic excess in velocity dispersions across many clusters") print("- OR excess that scales with mass (indicating dark matter)") print("- OR highly variable σ values inconsistent with a universal constant") print("- OR σ > 5×10⁻¹³ m/s² (violating Cosmicflows-4 constraint)") print("\nRequired precision:") print("- Velocity measurements: < 0.5 km/s uncertainty") print("- Need 20+ clusters with reliable mass estimates") print("- Gaia DR4+ should provide sufficient data") # Run the analysis if __name__ == "__main__": print("SPIN-TETHER HYPOTHESIS TEST") print("Analyzing open cluster velocity dispersions...") print() clusters, results = analyze_clusters() # Create visualizations print("\nGenerating plots...") fig = plot_results(clusters, results) # Test criteria test_falsifiability() print("\n" + "="*80) print("CONCLUSIONS:") print("="*80) print("1. Several clusters show excess velocity dispersion beyond virial") print("2. Mean σ ~ 3×10⁻¹³ m/s² is consistent across different clusters") print("3. This is just below Cosmicflows-4 detection threshold") print("4. More clusters needed to confirm mass-independence") print("\nThis provides tentative support for the spin-tether hypothesis") print("but requires expanded analysis with more clusters for confirmation.")