#!/usr/bin/env python3 """ Pulsar Genus-3 Two-Component Simulation Candidate Research Stack model: no canonical vertical in n-space "down" = increasing torsion / decreasing accessible phase volume / decreasing free energy This script models a two-component pulsar-like system: crust / charged component: Ω_c neutron superfluid component: Ω_s external magnetic dipole braking: N_ext = -K Ω_c^n internal coupling / creep: N_int glitch flash: vortex-unpinning avalanche when lag crosses threshold genus-3 route weights: three circulation channels that redistribute avalanche flow Doppler beaming trace: D = 1 / (γ(1 - β cos θ_obs)) It is not proof. It is an executable sketch that emits invariants and traces. """ from __future__ import annotations import json import math from dataclasses import asdict, dataclass from pathlib import Path from typing import Dict, List, Tuple import matplotlib.pyplot as plt import numpy as np # SI anchors used only for scale-aware proxies. H = 6.62607015e-34 # exact SI Planck constant, J s M_N = 1.67492749804e-27 # neutron mass, kg; measured reference seed C = 299_792_458.0 # exact SI speed of light, m/s KAPPA = H / (2.0 * M_N) # neutron superfluid circulation quantum proxy, m^2/s @dataclass class GlitchEvent: step: int time: float lag_before: float lag_after: float delta_omega_c: float delta_omega_s: float delta_L_internal: float flash_energy_proxy: float vortex_flux_proxy: float route_open_field: float route_closed_field: float route_return_sheet: float doppler_before: float doppler_after: float @dataclass class RouteState: """Three independent circulation channels: reduced genus-3 abstraction.""" open_field: float = 0.34 closed_field: float = 0.33 return_sheet: float = 0.33 def normalized(self) -> "RouteState": values = np.array([self.open_field, self.closed_field, self.return_sheet], dtype=float) values = np.maximum(values, 1e-9) values = values / values.sum() return RouteState(float(values[0]), float(values[1]), float(values[2])) def as_array(self) -> np.ndarray: n = self.normalized() return np.array([n.open_field, n.closed_field, n.return_sheet], dtype=float) def drift_after_event(self, lag: float, rng: np.random.Generator) -> "RouteState": """ Event history slightly reweights circulation channels. This is FAMM-like behavior in miniature: a successful release route becomes marginally easier next time, but noise prevents fake determinism. """ base = self.as_array() stress = min(1.0, max(0.0, lag / 0.02)) perturb = rng.normal(0.0, 0.015 + 0.025 * stress, size=3) # Return sheet gets extra weight under high lag: reconnection-like route. perturb[2] += 0.02 * stress values = np.maximum(base + perturb, 1e-5) values = values / values.sum() return RouteState(float(values[0]), float(values[1]), float(values[2])) def doppler_factor(omega: float, radius: float, theta_obs: float) -> float: """Special-relativistic rotational Doppler/beaming proxy.""" beta = min(0.85, abs(omega * radius) / C) gamma = 1.0 / math.sqrt(max(1e-12, 1.0 - beta * beta)) return 1.0 / (gamma * (1.0 - beta * math.cos(theta_obs))) def vortex_count_proxy(omega_s: float, radius: float) -> float: """Feynman vortex density relation n_v = 2 Ω_s / κ, integrated over area.""" area = math.pi * radius * radius return 2.0 * omega_s * area / KAPPA def simulate( steps: int = 30_000, dt: float = 0.02, seed: int = 42, ) -> Tuple[Dict[str, np.ndarray], List[GlitchEvent]]: """ Two-component spin evolution. Equations: I_c dΩ_c/dt = N_ext + N_int I_s dΩ_s/dt = -N_int d/dt(I_c Ω_c + I_s Ω_s) = N_ext Internal glitch window approximately conserves angular momentum: I_c ΔΩ_c + I_s ΔΩ_s ≈ 0 """ rng = np.random.default_rng(seed) # Dimensionless but pulsar-shaped parameters. I_total = 1.0 I_c = 0.12 I_s = I_total - I_c # Vela-like order-of-magnitude rotation frequency in rad/s: 11.2 Hz * 2π. omega_c = 2.0 * math.pi * 11.2 omega_s = omega_c * (1.0 + 4.0e-4) # External braking is deliberately small; lag builds slowly. K_brake = 1.1e-10 braking_index = 3.0 # Creep/mutual friction and glitch threshold. creep_coeff = 1.8e-5 omega_crit_base = 0.018 avalanche_fraction_base = 0.11 avalanche_width = 0.0028 # Emission radius proxy: neutron star radius order. radius = 12_000.0 theta_obs = math.radians(35.0) routes = RouteState() glitches: List[GlitchEvent] = [] t = np.zeros(steps) omega_c_trace = np.zeros(steps) omega_s_trace = np.zeros(steps) lag_trace = np.zeros(steps) L_total_trace = np.zeros(steps) L_expected_trace = np.zeros(steps) N_ext_trace = np.zeros(steps) N_int_trace = np.zeros(steps) doppler_trace = np.zeros(steps) vortex_trace = np.zeros(steps) torsion_trace = np.zeros(steps) accessible_phase_trace = np.zeros(steps) route_trace = np.zeros((steps, 3)) L_expected = I_c * omega_c + I_s * omega_s previous_lag = omega_s - omega_c for step in range(steps): time = step * dt lag = omega_s - omega_c route_weights = routes.as_array() # Magnetic dipole braking: N_ext = -K Ω_c^n. N_ext = -K_brake * (omega_c ** braking_index) # Smooth mutual friction / creep tries to reduce lag. # Route weights bias the effective creep channel. route_coupling = 0.75 * route_weights[0] + 0.45 * route_weights[1] + 1.10 * route_weights[2] N_int = creep_coeff * route_coupling * lag # Integrate pre-glitch continuous evolution. domega_c = (N_ext + N_int) / I_c domega_s = -N_int / I_s omega_c += domega_c * dt omega_s += domega_s * dt L_expected += N_ext * dt # Threshold fluctuates weakly, representing disorder in pinning landscape. omega_crit = omega_crit_base * (1.0 + 0.08 * math.sin(0.003 * step) + 0.02 * rng.normal()) lag_after_creep = omega_s - omega_c if lag_after_creep >= omega_crit: before_c = omega_c before_s = omega_s before_lag = lag_after_creep before_L = I_c * before_c + I_s * before_s doppler_before = doppler_factor(before_c, radius, theta_obs) # Avalanche fraction depends on excess lag and genus-route distribution. excess = max(0.0, before_lag - omega_crit) route_release_gain = 0.80 * route_weights[0] + 0.55 * route_weights[1] + 1.35 * route_weights[2] frac = avalanche_fraction_base * route_release_gain * (1.0 + excess / avalanche_width) frac = float(np.clip(frac, 0.035, 0.38)) # Reduce the differential lag by transferring angular momentum from superfluid to crust. delta_lag = frac * before_lag # Conservation: I_c ΔΩ_c + I_s ΔΩ_s = 0 and ΔΩ_c - ΔΩ_s = delta_lag. delta_omega_c = (I_s / (I_c + I_s)) * delta_lag delta_omega_s = -(I_c / (I_c + I_s)) * delta_lag omega_c += delta_omega_c omega_s += delta_omega_s after_lag = omega_s - omega_c after_L = I_c * omega_c + I_s * omega_s delta_L_internal = after_L - before_L doppler_after = doppler_factor(omega_c, radius, theta_obs) vortex_before = vortex_count_proxy(before_s, radius) vortex_after = vortex_count_proxy(omega_s, radius) vortex_flux = abs(vortex_before - vortex_after) flash_energy_proxy = 0.5 * I_s * (before_s ** 2 - omega_s ** 2) + 0.5 * I_c * (omega_c ** 2 - before_c ** 2) glitches.append( GlitchEvent( step=step, time=time, lag_before=before_lag, lag_after=after_lag, delta_omega_c=delta_omega_c, delta_omega_s=delta_omega_s, delta_L_internal=delta_L_internal, flash_energy_proxy=flash_energy_proxy, vortex_flux_proxy=vortex_flux, route_open_field=float(route_weights[0]), route_closed_field=float(route_weights[1]), route_return_sheet=float(route_weights[2]), doppler_before=doppler_before, doppler_after=doppler_after, ) ) routes = routes.drift_after_event(before_lag, rng) # Traces. lag = omega_s - omega_c L_total = I_c * omega_c + I_s * omega_s D = doppler_factor(omega_c, radius, theta_obs) Nv = vortex_count_proxy(omega_s, radius) # Torsion proxy: lag strain plus route circulation imbalance. route_entropy = -float(np.sum(route_weights * np.log(route_weights + 1e-12))) max_entropy = math.log(3.0) route_imbalance = 1.0 - route_entropy / max_entropy torsion = abs(lag) * (1.0 + 3.0 * route_imbalance) # Accessible phase volume proxy shrinks as lag/torsion stress rises. accessible_phase = math.exp(-70.0 * abs(lag)) * (route_entropy / max_entropy) t[step] = time omega_c_trace[step] = omega_c omega_s_trace[step] = omega_s lag_trace[step] = lag L_total_trace[step] = L_total L_expected_trace[step] = L_expected N_ext_trace[step] = N_ext N_int_trace[step] = N_int doppler_trace[step] = D vortex_trace[step] = Nv torsion_trace[step] = torsion accessible_phase_trace[step] = accessible_phase route_trace[step] = routes.as_array() previous_lag = lag traces = { "t": t, "omega_c": omega_c_trace, "omega_s": omega_s_trace, "lag": lag_trace, "L_total": L_total_trace, "L_expected": L_expected_trace, "N_ext": N_ext_trace, "N_int": N_int_trace, "doppler": doppler_trace, "vortex_count": vortex_trace, "torsion": torsion_trace, "accessible_phase": accessible_phase_trace, "route_open_field": route_trace[:, 0], "route_closed_field": route_trace[:, 1], "route_return_sheet": route_trace[:, 2], } return traces, glitches def plot_outputs(traces: Dict[str, np.ndarray], glitches: List[GlitchEvent], outdir: Path) -> None: outdir.mkdir(parents=True, exist_ok=True) t = traces["t"] glitch_times = [g.time for g in glitches] plt.figure(figsize=(11, 5)) plt.plot(t, traces["omega_c"] / (2.0 * math.pi), label="crust / charged component frequency Hz") plt.plot(t, traces["omega_s"] / (2.0 * math.pi), label="superfluid frequency Hz", alpha=0.78) for gt in glitch_times: plt.axvline(gt, alpha=0.25, linewidth=0.8) plt.title("Two-component pulsar spin traces with glitch flashes") plt.xlabel("model time") plt.ylabel("frequency proxy Hz") plt.legend() plt.tight_layout() plt.savefig(outdir / "pulsar_spin_traces.png", dpi=180) plt.close() plt.figure(figsize=(11, 5)) plt.plot(t, traces["lag"], label="lag Ω_s - Ω_c") plt.plot(t, traces["torsion"], label="torsion proxy", alpha=0.82) plt.plot(t, traces["accessible_phase"], label="accessible phase volume proxy", alpha=0.82) for gt in glitch_times: plt.axvline(gt, alpha=0.25, linewidth=0.8) plt.title("Lag builds, torsion rises, accessible phase volume contracts") plt.xlabel("model time") plt.legend() plt.tight_layout() plt.savefig(outdir / "pulsar_lag_torsion_phase.png", dpi=180) plt.close() plt.figure(figsize=(11, 5)) residual = traces["L_total"] - traces["L_expected"] plt.plot(t, residual, label="angular momentum residual L_total - integrated external torque") for gt in glitch_times: plt.axvline(gt, alpha=0.25, linewidth=0.8) plt.title("Conservation check: internal glitches should preserve angular momentum") plt.xlabel("model time") plt.ylabel("residual") plt.legend() plt.tight_layout() plt.savefig(outdir / "pulsar_angular_momentum_residual.png", dpi=180) plt.close() plt.figure(figsize=(11, 5)) plt.plot(t, traces["route_open_field"], label="cycle 1: open field route") plt.plot(t, traces["route_closed_field"], label="cycle 2: closed field route") plt.plot(t, traces["route_return_sheet"], label="cycle 3: return sheet route") for gt in glitch_times: plt.axvline(gt, alpha=0.25, linewidth=0.8) plt.title("Genus-3 route weights drift after avalanche events") plt.xlabel("model time") plt.ylabel("route weight") plt.legend() plt.tight_layout() plt.savefig(outdir / "pulsar_genus3_route_weights.png", dpi=180) plt.close() plt.figure(figsize=(11, 5)) plt.plot(t, traces["doppler"], label="Doppler / beaming factor") plt.plot(t, traces["vortex_count"] / np.max(traces["vortex_count"]), label="vortex count proxy normalized", alpha=0.82) for gt in glitch_times: plt.axvline(gt, alpha=0.25, linewidth=0.8) plt.title("Blue/red shift proxy and superfluid vortex count proxy") plt.xlabel("model time") plt.legend() plt.tight_layout() plt.savefig(outdir / "pulsar_doppler_vortex_proxy.png", dpi=180) plt.close() def write_outputs(traces: Dict[str, np.ndarray], glitches: List[GlitchEvent], outdir: Path) -> Dict[str, object]: outdir.mkdir(parents=True, exist_ok=True) residual = traces["L_total"] - traces["L_expected"] report = { "model_id": "pulsar_genus3_two_component_v0", "status": "HOLD", "proof_status": "simulation_sketch_not_proof", "rule": "No canonical vertical in n-space: descent is modeled as increasing torsion and decreasing accessible phase volume.", "constants": { "h_exact_SI": H, "c_exact_SI": C, "m_n_measured_reference": M_N, "kappa_proxy_h_over_2mn": KAPPA, }, "summary": { "steps": int(len(traces["t"])), "glitch_count": len(glitches), "initial_crust_frequency_hz": float(traces["omega_c"][0] / (2.0 * math.pi)), "final_crust_frequency_hz": float(traces["omega_c"][-1] / (2.0 * math.pi)), "max_lag": float(np.max(traces["lag"])), "max_torsion_proxy": float(np.max(traces["torsion"])), "min_accessible_phase_proxy": float(np.min(traces["accessible_phase"])), "max_angular_momentum_residual_abs": float(np.max(np.abs(residual))), "max_doppler_factor": float(np.max(traces["doppler"])), }, "glitches": [asdict(g) for g in glitches], "outputs": [ str(outdir / "pulsar_spin_traces.png"), str(outdir / "pulsar_lag_torsion_phase.png"), str(outdir / "pulsar_angular_momentum_residual.png"), str(outdir / "pulsar_genus3_route_weights.png"), str(outdir / "pulsar_doppler_vortex_proxy.png"), str(outdir / "pulsar_genus3_report.json"), ], "next_gate": "Compare qualitative traces against literature-aligned glitch features; do not use as proof or solar-system validation.", } (outdir / "pulsar_genus3_report.json").write_text(json.dumps(report, indent=2), encoding="utf-8") # Compact CSV for spreadsheet/Neo4j/forest-map ingestion. csv_path = outdir / "pulsar_genus3_traces.csv" names = [ "t", "omega_c", "omega_s", "lag", "L_total", "L_expected", "N_ext", "N_int", "doppler", "vortex_count", "torsion", "accessible_phase", "route_open_field", "route_closed_field", "route_return_sheet", ] matrix = np.column_stack([traces[name] for name in names]) header = ",".join(names) np.savetxt(csv_path, matrix, delimiter=",", header=header, comments="") report["outputs"].append(str(csv_path)) return report def main() -> None: outdir = Path("research-stack/models/pulsar_genus3_outputs") traces, glitches = simulate() plot_outputs(traces, glitches, outdir) report = write_outputs(traces, glitches, outdir) print(json.dumps(report["summary"], indent=2)) if __name__ == "__main__": main()