SOCR ≫ TCIU Website ≫ TCIU GitHub ≫

1 Summary

Note: This TCIU Section in Chapter 6 extends previous work on Chapter 3 work on Radon-Nikodym Derivatives, Kimemeasures, and Kime Operator.

Spacekime analytics is an emerging mathematical and computational framework that extends classical spacetime models by lifting the concept of time into the complex domain. Unlike traditional time representations, complex-time (kime) incorporates both magnitude (longitudinal event ordering, sequence or duration) and phase (characterizing the variability in repeated longitudinal experiments). This novel approach enhances our ability to represent, analyze, predict, and infer patterns within temporally dynamic systems. By blending statistical, quantum, and AI methodologies, spacekime analytics addresses fundamental questions in the modeling of longitudinal and multi-dimensional data, particularly through its applications in AI-driven inference and decision-making.

2 Mathematical Foundations

2.1 Complex-time (kime)

Complex time is denoted by \(\kappa = t e^{i\varphi}\), where the kime-magnitude \(t\) is the classical time (event ordering index) and the kime-phase \(\varphi\) reflects the random sampling from the repeated measurement distribution \(\Phi_{[-\pi, \pi)}\). This natural complex-time extension of time necessitates the reformulation of kime and spacekime events, spacekime metric tensor \(g_{\mu\nu}\), expected square interval \(\mathbb{E}[ds^2]\), and other classical concepts based on this kime-phase distribution.

2.2 Sigma-Algebra of Spacekime Probability Spaces

A rigorous mathematical formulation of spacekime analytics begins with defining the probability space \((\Omega, \mathcal{F}, P)\) over a spacekime manifold. This space allows for integration over kime-events, with the following foundational elements.

  1. Sample Space, \(\Omega\): The set of all possible outcomes, including all spatial and kime coordinates, \((\mathbf{x}, \kappa)\), where \(\mathbf{x} \in \mathbb{R}^n\) represents spatial dimensions, and \(\kappa \in \mathbb{C}\) represents complex time. The representation of kime as \(\kappa = r e^{i\phi}\) where \(r > 0\) and \(\phi\) is the kime-phase, defines each kime-coordinate in terms of event ordering and directional shifts.

  2. Sigma-Algebra, \(\mathcal{F}\): The collection of subsets of \(\Omega\), representing all possible measurable events (kime-events). In this complex space, \(\mathcal{F}\) is generated by sets of points in both the real and imaginary components of kime, ensuring compatibility with probabilistic operations defined over kime-surfaces.

  3. Probability Measure, \(P\): A probability measure assigning likelihoods to events in \(\mathcal{F}\), considering the topological and metric properties of spacekime. For a kime-event \(E \subset \mathcal{F}\), \(P(E)\) quantifies the probabilistic weighting of occurrences in both spatial and kime dimensions. This measure integrates over \((\mathbf{x}, \kappa)\), yielding probabilities influenced by both the magnitude and phase distributions of kime.

In the spacekime framework, probability densities must account for periodic behaviors and cyclic dependencies introduced by kime-phase. Standard probability density functions \(f(\mathbf{x}, r, \phi)\) are adjusted to include terms in both \(r\) (radial time component) and \(\phi\) (angular phase), allowing for more comprehensive modeling of time-varying dynamics.

2.3 Spacekime Metric and Distance Function

Formalizing the geometry of the spacekime manifold requires introducing an appropriate metric \(d_{kime}\), defined over \((\mathbf{x}, \kappa) \in \mathbb{R}^n \times \mathbb{C}\), that respects the complex nature of kime and supports well-defined norms for distance calculations.

To rigorously define the spacekime metric and the corresponding square interval in terms of a kime-phase distribution \(\varphi \sim \Phi_{[-\pi, \pi)}\), we need to account for the variability in the kime-phase that reflects the intrinsic variability of repeated measurements in the observable process. The metric should encapsulate both real-time evolution and stochastic kime-phase behavior, while maintaining the fundamental properties of a metric: non-negativity, symmetry, and adherence to the triangle inequality.

Capturing both the real-time and phase variability effects, the spacekime square interval \(ds^2\) is defined in terms of expected squared phase difference \(\mathbb{E}[\theta^2]\), where \(\theta\) represents the kime-phase difference between two consecutive measurements \(\mathbb{E}[ds^2] = -c^2 t^2 - c^2 \mathbb{E}[\theta^2] + dx^2 + dy^2 + dz^2\), where \(t\) is the real-time component, \(\theta = \varphi_1 - \varphi_2\) is the phase difference, where \(\varphi_1, \varphi_2 \sim \Phi\), and \(\mathbb{E}[\theta^2]\) is the expected squared phase difference under \(\Phi\).

Assuming that the kime-phases \(\varphi_1\) and \(\varphi_2\) are independently sampled from a comon distribution \(\Phi\) with mean zero, we the expected squared phase difference is \(\mathbb{E}[\theta^2] = 2 \operatorname{Var}(\varphi),\) where \(\operatorname{Var}(\varphi)\) is the variance of the kime-phase under \(\Phi\). For a uniform distribution on \([-\pi, \pi)\), for instance, we have \(\operatorname{Var}(\varphi) = \frac{\pi^2}{3}\), yielding \(\mathbb{E}[\theta^2] = \frac{2\pi^2}{3}.\)

The metric tensor \(g_{\mu\nu}\) corresponding to this square interval encapsulates the classical spacetime and the kime-phase variability. Given that \(\mathbb{E}[\theta^2]\) represents phase variability as a constant scalar term for a stationary \(\Phi\), we can define \(g_{\mu\nu}\) as follows in the \((t, x, y, z)\) coordinates \[g_{\mu\nu} = \begin{pmatrix} -c^2 & 0 & 0 & 0 \\ 0 & 1 & 0 & 0 \\ 0 & 0 & 1 & 0 \\ 0 & 0 & 0 & 1 \end{pmatrix} ,\] with an effective time component in the interval given by \(\mathbb{E}[ds^2] = -c^2 t^2 - c^2 \mathbb{E}[\theta^2] + dx^2 + dy^2 + dz^2.\) This metric reflects the additional length induced by the expected phase difference variability in complex time. This definition satisfies the mathematical conditions of a metric.

  1. Non-negativity: The term \(-c^2 t^2\) in \(\mathbb{E}[ds^2]\) is always negative, \(\forall t \in\mathbb{R}^+\), as it represents the time-like component in the spacekime interval, which aligns with the Lorentzian signature commonly used in spacetime metrics (i.e., \((- + + +)\)). This negative sign is intentional, as it reflects the causal structure where the time-like component contributes a “negative” distance, consistent with standard relativistic intervals. This term \(-c^2 t^2\) represents the contribution from the real-time evolution in a Lorentzian signature, which is conventionally negative for time-like intervals. The term \(-c^2 \mathbb{E}[\theta^2]\) introduces additional length in the complex time component due to the expected variance in phase, also contributing negatively to maintain the overall causal structure when considering the cumulative effect of repeated measurements. And the spatial terms \(dx^2 + dy^2 + dz^2\) are positive, preserving the usual Euclidean geometry in space dimensions.

In the spacetime geometry, non-negativity doesn’t imply that the interval must be positive; rather, it reflects the pseudo-Riemannian nature of the metric. A time-like interval can be negative as long as it maintains consistency with the causal structure. In our spacekime context, \(\mathbb{E}[ds^2]\) being negative reflects a time-like separation, consistent with relativistic interval interpretations. This structure captures phase variability as an intrinsic source of time-like separation in repeated measurements, where the negative contributions from \(t\) and \(\mathbb{E}[\theta^2]\) reflect time-evolution and phase uncertainty, respectively.

  1. Symmetry: The metric \(g_{\mu\nu}\) is symmetric, ensuring the symmetry of distances between any two points in the spacekime manifold.

  2. Triangle Inequality: The metric tensor, by incorporating a constant \(\mathbb{E}[\theta^2]\) as a phase-difference variability term, preserves the triangle inequality by preventing negative distances, maintaining consistent metric properties.

In spacekime, the real-time evolution is mapped to complex time, where phase variability is encoded as \(\mathbb{E}[\theta^2]\). This implies that each time step is associated with a corresponding phase variation that scales with \(t\), leading to a spread in complex time that is observable as measurement variability. Whereas the inverse transform reconstructs the classical time behavior by averaging over the phase variability, effectively removing the stochastic component while retaining the real-time structure.

This spacekime metric enables advanced distance-based analyses, such as clustering and manifold learning, by leveraging both spatial proximity and kime-phase coherence. The phase information in particular allows for representing and modeling recurrent processes as intrinsic components of data analysis, thereby enhancing AI models’ ability to interpret periodic or cyclic temporal behaviors.

3 Spacekime Applications Transcending Quantum Mechanics, Statistical Estimation, and AI Analytics

Spacekime analytics draws conceptual and operational parallels with quantum mechanics, statistical estimation, and AI. This interdisciplinary foundation allows the framework to exploit the structural advantages of complex time, with applications in higher-dimensional data analysis and quantum-inspired AI models:

  1. Quantum Mechanics: Complex time (kime) representation directly translates quantum mechanics principles such as wave-particle duality, phase coherence, and operator eigenfunctions into statistical inference. In spacekime, each observable corresponds to a complex event or kime-phase, analogous to quantum observables. The transformation of longitudinal observations into kime-surfaces echoes the Heisenberg uncertainty principle, where time-phase uncertainties offer dual interpretations in data science, improving the predictive performance and interpretability of models.

  2. Statistical Estimation and Bayesian Inference: Spacekime analytics provides a Bayesian framework where posterior distributions are conditioned on kime-phase aggregates or estimations. This approach enables inference on complex-valued processes, with posterior predictive distributions expanded as \(p(\gamma | X, \phi) \propto p(X | \gamma, \phi) \cdot p(\gamma | \phi),\) where \(\phi\) represents the kime-phase information aggregated across observations. This Bayesian structure allows for probabilistic estimation with reduced sample sizes, making spacekime analytics especially useful in high-dimensional, low-sample environments.

  3. AI and Machine Learning Analytics: By representing time-dependent data as kime-surfaces, spacekime analytics unlocks novel AI applications. Machine learning models can exploit these complex embeddings, allowing for richer feature spaces that improve clustering, classification, and forecasting tasks. Techniques such as kime-surface tensor decomposition provide powerful tools for understanding multivariate dependencies, facilitating the development of robust, invariant, and sample-efficient AI systems.

  4. Domain-specific Applicaitons: Spacekime analytics holds significant promise across diverse scientific and applied domains. Three primary applications demonstrate its transformative potential. For instance, in neuroimaging, spacekime analytics enables the representation of fMRI or EEG time-series as kime-surfaces, capturing both the temporal and phase-specific dynamics of brain activity. Through kime-phase estimation, researchers can model patterns in brain connectivity that fluctuate over time, providing insights into cognitive processes and neurological conditions. This approach has proven invaluable in studying conditions such as Alzheimer’s disease, where the spacekime framework allows researchers to track disease progression and identify early biomarkers by modeling both the magnitude and phase of neural signals.

The application of spacekime analytics in biomedical signal processing allows for continuous, multi-dimensional monitoring of physiological signals, such as heart rate variability (HRV) and glucose levels. Kime-based metrics provide more accurate modeling of circadian rhythms and patient-specific variations, enabling real-time tracking of patient health and the early detection of anomalies. In personalized healthcare, AI-driven kime-based models support decision-making by integrating phase information, leading to dynamic adjustments in patient care plans based on nuanced temporal patterns.

In financial and economic forecasting, complex time is utilized to represent time-series data with high-frequency fluctuations and periodic trends. Spacekime analytics allows for enhanced modeling of cyclical trends, including economic cycles and seasonal market patterns. By leveraging kime-phase structures, these models capture complex correlations across assets and markets, providing better risk assessment and portfolio optimization. Quantum-inspired approaches allow for time-dependent probabilistic forecasts that are less sensitive to noise, supporting more robust decision-making in volatile markets.

The development of complex-time (kime) representation and spacekime analytics marks a milestone in data science, blending elements of quantum mechanics, advanced statistical inference, and AI. The spacekime framework, with its sigma-algebra structure, complex metric space, and robust AI applications, redefines the analytical landscape for high-dimensional, longitudinal data. Its interdisciplinary foundation and application versatility position spacekime analytics as a transformative tool for advancing scientific discovery and practical applications in diverse domains, from neuroscience to finance.

By facilitating complex temporal analyses, spacekime analytics not only enhances our understanding of dynamic systems but also opens up new research areas in AI and machine learning, ultimately paving the way for breakthroughs across multiple scientific and technological fields.

4 Applications

4.1 Take 1: (Simplified/Initial) Kime-Prediction of Precession of Planitary Orbits

Following the notation in this description of General Relativity (GR) support for prediction of the precession of planitary orbits, we will explore the spacekime representation as another possible orbital prediciton strategy.

General Relativity (GR) predicts that the excess precession of the perihelion of Mercury’s orbit is approximately \(0.01^o\) per century. Mercury’s orbit precession had been perplexing for centuries, as Newton’s law of gravity can be used to model planetary orbits as closed ellipses with the Sun located at one focus of the corresponding ellipse orbit. Observations indicate that the major axes of these elliptical orbits are not stable and chance their direction with time. There are also gravitational interactions between planets also affect the precesional planetary orbits, complementing the primary solar gravitational force, which results in orbital motions that are not perfectly closed static ellipses but rather dynamic elliptical orbits within the solar orbital plane.

Ignoring the small interplanetary interactions, Newtonian gravity predicts that a planet’s orbit would be an idealized ellipse traversed by the planet at a distance from the Sun given by \(r = \frac{r_{min}}{1 + e \cos \phi}\), where \(r_{min}\) is the distance to the perihelion (the point of closest approach to the Sun) and \(e\) is the orbital eccentricity, representing the deviation from a circular orbit (\(e = 0\) for a circle).

The gravitational interactions between planets cause the perihelion to rotate. This perihelion precession results in a gradual rotation of the elliptical orbit’s major axis around the Sun.

Precession of Mercury's Elliptical Orbit

Precession of Mercury’s Elliptical Orbit

Repeated astronomical observations of Mercury’s orbit suggest a perihelion precession of \(9.55\) arc minutes per century. Newtonian mechanics modeling that accounts for the gravitational interactions of other planets in the solar system predict a lower precession of \(8.85\) arc minutes per century, suggests a difference of \(\sim 0.7\) arc minutes, i.e., \(42\) arc seconds per century. However, observed astronomical measurements show a discrepancy of \(\sim 43.1\) arc seconds per century. General Relativity explains some of this inconsistency by modifying Newton’s gravitational model correcting the orbital elliptical equation by including the term \(\Delta\varphi\) (relativistic correction) \[r = \frac{r_{min}}{1 + e \cos(\phi - \Delta \phi)}.\]

According to general relativity \[\Delta \phi = \frac{6 \pi G M}{c^2 (1 - e^2) R},\] where \(G\) is the gravitational constant, \(M\) is the mass of the Sun, \(c\) is the speed of light, \(e\) is the orbital eccentricity, and \(R\) is the semi-major axis of the orbit.

Mercury’s small orbit is highly eccentric and the planet has the largest relativistic precession among the 8 planets in the solar system. Plugging Mercury’s values into the GR-corrected equation model yields a precession of \(43.0\) arc seconds per century, which agrees with the observed value of \(43.1 \pm 0.5\) arc seconds.

The following table shows the observed and predicted precession rates for several planets:

Precession of Planetary Orbits
Planet Orbits.per.century Eccentricity r_min.AU GR.Predicted.arc.sec.per.century Observed.arc.sec.per.century
Mercury 415.2 0.2060 0.307 43.0 \(43.1 \pm 0.5\)
Venus 162.5 0.0068 0.717 8.6 \(8.4 \pm 4.8\)
Earth 100.0 0.0170 0.981 3.8 \(5.0 \pm 1.2\)
Icarus* 89.3 0.8270 0.186 10.0 \(9.8 \pm 0.8\)

Note: The perihelion of the asteroid Icarus, which is closer to the Sun than any other asteroid, is within Mercury’s orbit.

4.1.1 Mercury’s Perihelion

The precession of the perihelion of Mercury’s orbit is one of the classic tests of General Relativity (GR). In this derivation, we’ll show how GR predicts an additional precession of Mercury’s orbit that matches the observed anomaly not explained by Newtonian mechanics. Mercury’s orbit around the Sun exhibits a precession of its perihelion (the point of closest approach to the Sun) that cannot be fully explained by Newtonian mechanics and the gravitational perturbations from other planets.

The observed precession is about 43 arcseconds per century greater than what Newtonian physics predicts when accounting for perturbations from other planets. In Newtonian gravity, the orbit of a planet around the Sun is described by Kepler’s laws, resulting in elliptical orbits with no inherent precession (ignoring perturbations).

The equation of motion is \(\frac{d^2 \vec{r}}{dt^2} = -\frac{G M}{r^2} \hat{r}\), where conservation of Angular Momentum leads to a constant areal velocity. Einstein’s theory that describes gravity as the curvature of spacetime caused by mass and energy. Using the Schwarzschild Metric, the solution to Einstein’s field equations for the spacetime outside a spherical, non-rotating mass like the Sun is \[ds^2 = -\left(1 - \frac{2GM}{c^2 r}\right)c^2 dt^2 + \left(1 - \frac{2GM}{c^2 r}\right)^{-1} dr^2 + r^2 d\theta^2 + r^2 \sin^2 \theta \, d\phi^2 .\]

The derivation of the Equation of Motion in Schwarzschild Spacetime uses the assumptions that a test particle (Mercury) moving in the equatorial plane (\(\theta = \frac{\pi}{2}\)), so \(d\theta = 0\) with a simplified spacetime metric \[ds^2 = -\left(1 - \frac{2GM}{c^2 r}\right)c^2 dt^2 + \left(1 - \frac{2GM}{c^2 r}\right)^{-1} dr^2 + r^2 d\phi^2 . \] The equations of motion are derived from the geodesic equations for the Schwarzschild metric. The energy per Unit Mass (\(\tilde{E}\)) is \(\left(1 - \frac{2GM}{c^2 r}\right) c^2 \frac{dt}{d\tau} = \tilde{E}\), the angular momentum per Unit Mass (\(\tilde{L}\)) is \(r^2 \frac{d\phi}{d\tau} = \tilde{L},\) and the proper time (\(\tau\)) is measured by a clock moving with the particle.

The effective potential and radial equation of motion depend on the metric relation \[-\left(1 - \frac{2GM}{c^2 r}\right) c^2 \left( \frac{dt}{d\tau} \right)^2 + \left(1 - \frac{2GM}{c^2 r}\right)^{-1} \left( \frac{dr}{d\tau} \right)^2 + r^2 \left( \frac{d\phi}{d\tau} \right)^2 = -c^2.\]

Substituting the conserved quantities we obtain \[-\left(1 - \frac{2GM}{c^2 r}\right)^{-1} \left( \frac{\tilde{E}}{c} \right)^2 + \left(1 - \frac{2GM}{c^2 r}\right)^{-1} \left( \frac{dr}{d\tau} \right)^2 + \left( \frac{\tilde{L}}{r} \right)^2 = -c^2 .\]

Next, we can simplify the radial equation \[\left( \frac{dr}{d\tau} \right)^2 = \left( \frac{\tilde{E}}{c} \right)^2 - \left(1 - \frac{2GM}{c^2 r}\right) \left( c^2 + \left( \frac{\tilde{L}}{r} \right)^2 \right) .\] The effective potential (\(V_{\text{eff}}\)) is \[\left( \frac{dr}{d\tau} \right)^2 + V_{\text{eff}}(r) = \left( \frac{\tilde{E}}{c} \right)^2 ,\] where \[V_{\text{eff}}(r) = \left(1 - \frac{2GM}{c^2 r}\right) \left( c^2 + \left( \frac{\tilde{L}}{r} \right)^2 \right) .\]

We can simplify the equations by changing the variables and using the inverse radius, \(u = \frac{1}{r}\). The derivatives transformation \(\frac{dr}{d\tau} = -\frac{1}{u^2} \frac{du}{d\tau} .\) Using \(\frac{d\phi}{d\tau}\) and the differentiation chain rule we get \[\frac{du}{d\tau} = \frac{du}{d\phi} \frac{d\phi}{d\tau} = \frac{du}{d\phi} \frac{\tilde{L}}{r^2} \] and plugging in \(r = 1/u\) yields \(\frac{du}{d\tau} = \tilde{L} u^2 \frac{du}{d\phi} .\)

Substitute this result into the radial equation and simplify \[\left( -\frac{1}{u^2} \tilde{L} u^2 \frac{du}{d\phi} \right)^2 + V_{\text{eff}}(r) = \left( \frac{\tilde{E}}{c} \right)^2\]

\[\tilde{L}^2 \left( \frac{du}{d\phi} \right)^2 + V_{\text{eff}}(r) = \left( \frac{\tilde{E}}{c} \right)^2 .\]

This equation can be expressed as \[\left( \frac{du}{d\phi} \right)^2 = \frac{1}{\tilde{L}^2} \left( \left( \frac{\tilde{E}}{c} \right)^2 - V_{\text{eff}}(r) \right) \]

Approximating \(V_{\text{eff}}(r)\) to obtain the weak gravitational fields \(\left ( \frac{GM}{c^2 r} \ll 1 \right )\)

\[V_{\text{eff}}(r) \approx c^2 + \left( \frac{\tilde{L}}{r} \right)^2 - \frac{2GM}{r} \left( c^2 + \left( \frac{\tilde{L}}{r} \right)^2 \right) .\]

Some simplification and differentiation w.r.t. \(\phi\) leads to \[\left( \frac{du}{d\phi} \right)^2 + u^2 = \frac{GM}{\tilde{L}^2} + 3 \frac{GM}{c^2} u^3 .\]

\[\frac{d^2 u}{d\phi^2} + u = \frac{GM}{\tilde{L}^2} + 3 \frac{GM}{c^2} u^2 .\]

The homogeneous solution to this differential equation is \(\frac{d^2 u}{d\phi^2} + u = 0\), and assuming a solution of the form \(u_p = \frac{GM}{\tilde{L}^2},\) its solution is \(u_h(\phi) = A \cos \phi + B \sin \phi.\)

Due to the nonlinearity of the quadratic term \(u^2\), we need to consider perturbation methods. Suppose \(u = u_0 + \delta u\), where \(u_0\) is the Newtonian solution and \(\delta u\) is a small relativistic correction. the Newtonial zero-order equation \(\frac{d^2 u_0}{d\phi^2} + u_0 = \frac{GM}{\tilde{L}^2}\) has a solution \(u_0(\phi) = \frac{GM}{\tilde{L}^2} \left(1 + e \cos \phi \right),\) where \(e\) is the orbital eccentricity.

A first-order correction uses \(u = u_0 + \delta u\) into the full equation to linearize \(\frac{d^2 \delta u}{d\phi^2} + \delta u = 3 \frac{GM}{c^2} u_0^2\) and compute \(u_0^2\). \[u_0^2 = \left( \frac{GM}{\tilde{L}^2} \right)^2 \left(1 + 2 e \cos \phi + e^2 \cos^2 \phi \right). \]

A partial solution for \(\delta u\) of the first-order differential equation \[\frac{d^2 \delta u}{d\phi^2} + \delta u = 3 \frac{GM}{c^2} \left( \frac{GM}{\tilde{L}^2} \right)^2 \left(1 + 2 e \cos \phi + e^2 \cos^2 \phi \right) \] can be expressed as \(\delta u = K_0 + K_1 \phi \sin \phi + K_2 \cos \phi.\) However, the term involving \(\cos^2 \phi\) leads to secular terms (terms that grow with \(\phi\)). To avoid these secular terms we can use the method of undetermined coefficients or variation of parameters to find a solution that remains bounded. Hence, \(\delta u\) contains a term proportional to \(\phi \sin \phi\) \[\delta u = \frac{3 (GM)^3}{c^2 \tilde{L}^4} e \phi \sin \phi + \text{bounded terms} .\]

the complete solution is \[u(\phi) = u_0(\phi) + \delta u(\phi) = \frac{GM}{\tilde{L}^2} \left(1 + e \cos \phi \right) + \delta u(\phi) ,\] where the term \(\delta u\) effectively causes the orbit to precess because it modifies the angular dependence. This solution suggests that the argument of the perihelion advances by a small angle \(\delta \phi\) per orbit.

The presence of the term \(\phi\) in \(\delta u\) indicates that the orbit is not exactly periodic with period \(2\pi\). The new period \(\Phi\) satisfies \(\Phi = 2\pi \left(1 + \Delta \right)\), where \(\Delta\) is a small correction. From the perturbation, the shift per revolution is \(\delta \phi = 2\pi \Delta = \frac{6\pi GM}{c^2 a (1 - e^2)}\), where \(a\) is the semi-major axis of Mercury’s orbit and \(e\) is the orbital eccentricity.

The numerical precision calculation for Mercury uses the following parameters Gravitational constant \(G\); Mass of the Sun \(M\); Speed of light \(c\); Semi-major axis \(a\); and Eccentricity \(e\). \[\delta \phi = \frac{6\pi GM}{c^2 a (1 - e^2)}\]

We can convert to arcseconds per century. Mercury completes approximately \(415\) revolutions per century and we multiply \(\delta \phi\) by \(415\) to get the total precession per century. Therefore, \(\delta \phi_{\text{total}} = 43'' \text{ (arcseconds per century)},\) which is a good prediction of the observed excess precession of Mercury’s perihelion.

4.1.2 Earth’s Perihelion

Similarly, the Earth’s precession perihelion which is predicted by General Relativity (GR) to be \(3.8''\), whereas repeated observations estimate it to be \(5.0'' \pm 1.2''\). We follow a similar approach we used for Mercury, but with using Earth’s specific orbital parameters. The point of closest approach of a planet to the Sun (perihelion) shifts over time, resulting in a precession of the orbit. Newtonian Mechanics predicts some precession due to perturbations from other planets and General Relativity provides an additional correction to the precession not accounted for by Newtonian physics.

The relativistic correction to the perihelion precession per orbit is given by \(\delta\phi = \frac{6\pi G M}{c^2 a (1 - e^2)}\), where \(\delta\phi\) is Additional precession per orbit in radians; \(G\) is the gravitational constant; \(M\) is the Mass of the Sun; \(c\) is the Speed of light; \(a\) is the Semi-major axis of Earth’s orbit; and \(e\) is the orbital eccentricity of Earth.

These Earth’s orbital parameters have the following values:

  • Gravitational Constant (\(G\)): \(6.67430 \times 10^{-11} \, \text{m}^3\,\text{kg}^{-1}\,\text{s}^{-2}\)
  • Mass of the Sun (\(M\)): \(1.98847 \times 10^{30} \, \text{kg}\)
  • Speed of Light (\(c\)): \(299,792,458 \, \text{m/s}\)
  • Semi-major Axis (\(a\)): \(1.495978707 \times 10^{11} \, \text{m}\) (1 Astronomical Unit)
  • Orbital Eccentricity (\(e\)): \(0.0167086\)

We start by calculating the numerator \(N = 6\pi G M\) \[\begin{align*} N &= 6\pi \times (6.67430 \times 10^{-11}) \times (1.98847 \times 10^{30}) \\ &= 6\pi \times 1.3271244 \times 10^{20} \\ &= 6 \times 3.1415926536 \times 1.3271244 \times 10^{20} \\ &= 18.84955592 \times 1.3271244 \times 10^{20} \\ &= 25.0226386 \times 10^{20} \, \text{m}^3\,\text{s}^{-2} \end{align*} .\]

Next, we compute the denominator \(D = c^2 a (1 - e^2)\). First, compute \(c^2\), \(c^2 = (299,792,458)^2 = 8.98755179 \times 10^{16} \, \text{m}^2\,\text{s}^{-2}\), and \(1 - e^2\), \(1 - e^2 = 1 - (0.0167086)^2 = 1 - 0.000279 \approx 0.999721.\)

\[\begin{align*} D &= (8.98755179 \times 10^{16}) \times (1.495978707 \times 10^{11}) \times 0.999721 \\ &= 1.34562455 \times 10^{28} \times 0.999721 \\ &\approx 1.34524933 \times 10^{28} \, \text{m}^3\,\text{s}^{-2} \end{align*} .\]

\[\delta\phi = \frac{N}{D} = \frac{25.0226386 \times 10^{20}}{1.34524933 \times 10^{28}} = 1.860695 \times 10^{-7} \, \text{radians per orbit} .\]

Again, convert the precession to arcseconds per century, using 1 radian \(= 206,264.806247\) arcseconds

\[\delta\phi_{\text{arcsec/orbit}} = \delta\phi \times 206,264.806247 = (1.860695 \times 10^{-7}) \times 206,264.806247 \approx 0.038389 \, \text{arcseconds per orbit} .\]

The number of Earth’s orbits around the Sun in a century is approximately \(100\) (by definition of a century) \[\delta\phi_{\text{arcsec/century}} = \delta\phi_{\text{arcsec/orbit}} \times 100 = 0.038389 \times 100 = 3.8389 \, \text{arcseconds per century} .\]

Let’s compare the GR predictions against real observed data

  • GR Prediction: \(\approx 3.84''\) arcseconds per century
  • Observed Value: \(5.0'' \pm 1.2''\) arcseconds per century

The observed precession has an uncertainty of \(\pm 1.2''\), which means the observed value ranges from \(3.8''\) to \(6.2''\) arcseconds per century. The lower bound of the observed value (\(3.8''\)) coincides with the GR prediction (\(3.84''\)), suggesting that the discrepancy may be within observational uncertainties.

  • Possible Explanations for Discrepancy:
    • Observational Errors: Measurement limitations and errors can affect the accuracy of the observed precession.
    • Additional Perturbations: Gravitational influences from other celestial bodies (e.g., the Moon, other planets) not fully accounted for.
    • Non-gravitational Effects: Factors like solar mass loss, tidal effects, or general solar oblateness (though the Sun’s oblateness is minimal).
    • Data Analysis Methods: Differences in data reduction and analysis techniques can lead to variations in the estimated precession.

The calculation demonstrates that General Relativity predicts a perihelion precession for Earth’s orbit that is consistent with observational data when uncertainties are considered. The slight discrepancy can be attributed to observational errors and unmodeled perturbations, and does not indicate a failure of GR in explaining planetary motion.

4.1.3 Kime-representation model

A kime-representation model can be applied to the perihelion precession of Earth’s orbit to potentially improve upon the General Relativity (GR) prediction by incorporating a specific kime-phase distribution, such as the Laplace distribution. We’ll consider the observed measurements as repeated samples from a sampling distribution and see if this approach can account for the discrepancy between the GR prediction and the observed value.

A kime-phase difference (\(\Delta \phi\)) with a probability distribution \(P(\Delta \phi)\) represents the uncertainty, or fluctuations, in time measurements across repeated experiments. To see if incorporating a specific kime-phase distribution (e.g., Laplace or Normal distribution) into the perihelion precession calculation can adjust the GR prediction to better match the observed value.

Knowledge about the Earth’s Perihelion Precession include

  • Observed Precession: \(5.0'' \pm 1.2''\) arcseconds per century.

  • GR Prediction: Approximately \(3.84''\) arcseconds per century.

  • Discrepancy: The observed value is higher than the GR prediction, with an uncertainty that partially overlaps the predicted value.

The standard GR approach provides a deterministic prediction for perihelion precession based on the curvature of spacetime.

  • Kime-Representation Perspective: Introduces stochasticity into the time evolution, potentially affecting orbital dynamics.

  • Hypothesis: The discrepancies between GR predictions and observations might be accounted for by fluctuations in the kime-phase, modeled by a probability distribution.

The standard equation of motion is \(\frac{d^2 u}{d\phi^2} + u = \frac{GM}{\tilde{L}^2} + 3 \frac{GM}{c^2} u^2\), and the kime-adjusted equation includes a kime-phase correction term \(\delta \phi_k\) to the angular coordinate \(\phi\), \(\phi \rightarrow \phi + \delta \phi_k,\) where \(\delta \phi_k\) is a random variable with a probability distribution \(P(\delta \phi_k)\).

Consider modeling the kime-phase difference with a Laplace Distribution \(P(\delta \phi_k) = \frac{1}{2b} \exp\left( -\frac{|\delta \phi_k - \mu|}{b} \right),\) where \(\mu\): Location parameter (mean of the distribution) and \(b\): Scale parameter (related to the variance). Let’s try to adjust the calculation of the Earth’s perihelion precession by averaging over the Kime-Phase Distribution. The modified solution for \(u(\phi)\) is \(u(\phi) = u_0(\phi + \delta \phi_k)\), where \(u_0(\phi)\) is the solution without the kime-phase correction.

The expectation value of \(u(\phi)\) is \[\langle u(\phi) \rangle = \int_{-\infty}^{\infty} u_0(\phi + \delta \phi_k) P(\delta \phi_k) \, d(\delta \phi_k) .\]

For small \(\delta \phi_k\), expand \(u_0(\phi + \delta \phi_k)\) \[u_0(\phi + \delta \phi_k) \approx u_0(\phi) + \delta \phi_k \frac{du_0}{d\phi}\]

The expectation value is \[\langle u(\phi) \rangle = u_0(\phi) + \langle \delta \phi_k \rangle \frac{du_0}{d\phi}\]

Since the Laplace distribution is symmetric around \(\mu\), if we set \(\mu = 0\), then \(\langle \delta \phi_k \rangle = 0\). The variance \(\sigma^2\) of the Laplace distribution is \(2b^2\).

As \(\langle \delta \phi_k \rangle = 0\), the first-order term vanishes. The second-order term in the expansion is \[u_0(\phi + \delta \phi_k) \approx u_0(\phi) + \delta \phi_k \frac{du_0}{d\phi} + \frac{1}{2} (\delta \phi_k)^2 \frac{d^2 u_0}{d\phi^2} .\]

Taking the expectation value \[\langle u(\phi) \rangle = u_0(\phi) + \frac{1}{2} \langle (\delta \phi_k)^2 \rangle \frac{d^2 u_0}{d\phi^2} .\]

Since \(\langle (\delta \phi_k)^2 \rangle = 2b^2\) \(\frac{d^2 u_0}{d\phi^2} + u_0 = \frac{GM}{\tilde{L}^2}.\)

\[\langle u(\phi) \rangle = u_0(\phi) + b^2 \left( \frac{GM}{\tilde{L}^2} - u_0 \right)\]

\[\langle u(\phi) \rangle = u_0(\phi) + b^2 \left( \frac{GM}{\tilde{L}^2} - u_0(\phi) \right)\]

\[\langle u(\phi) \rangle = (1 - b^2) u_0(\phi) + b^2 \frac{GM}{\tilde{L}^2}\]

This suggests that the effect of the kime-phase distribution is to scale the Newtonian solution and add a constant term. The precession is related to the angular shift in the orbit due to deviations in \(u(\phi)\). Let’s consider how the scaling affects the precession term in GR where the extra precession per orbit is due to the \(3GM u^2 / c^2\) term in the equation of motion. With the kime-phase correction, this term becomes modified due to the change in \(u(\phi)\).

Assuming the kime-phase correction effectively scales the precession term, we can adjust the GR precession term \(\delta \phi_{\text{total}} = \delta \phi_{\text{GR}} \times f(b) ,\) where \(f(b)\) is a function depending on the scale parameter \(b\) of the Laplace distribution.

Can we determine an appropriate value for the scale parameter \(b\) that may link the observed Precession, \(\delta \phi_{\text{obs}} = 5.0''\) arcseconds per century with the GR prediction, \(\delta \phi_{\text{GR}} = 3.84''\) arcseconds per century? \[f(b) = \frac{\delta \phi_{\text{obs}}}{\delta \phi_{\text{GR}}} = \frac{5.0}{3.84} \approx 1.3021 \]

From our earlier expression, the precession term is proportional to \(u_0^2\). With the kime-phase correction, \(u_0\) is effectively scaled by \((1 - b^2)\). Thus, the precession term becomes \[\delta \phi_{\text{kime}} \propto \left( (1 - b^2) u_0 \right)^2 = (1 - b^2)^2 u_0^2 .\] To match the observed precession, we set \[f(b) = \frac{\delta \phi_{\text{kime}}}{\delta \phi_{\text{GR}}} = (1 - b^2)^2 .\] However, this leads to \(f(b) \leq 1\), which cannot increase the precession beyond the GR prediction.

Are there alternative approaches? If the assumption is that the kime-phase correction scales the precession term as \((1 - b^2)^2\), this may not increase in the precession value. Perhaps the kime-phase correction introduces an additive term to the precession? Is there is an additional precession \(\delta \phi_{\text{kime}}\) due to the kime-phase fluctuations? Let’s suppose the total precession is \[\delta \phi_{\text{total}} = \delta \phi_{\text{GR}} + \delta \phi_{\text{kime}}.\] To estimate \(\delta \phi_{\text{kime}}\) we can use a theoretical expression for \(\delta \phi_{\text{kime}}\) in terms of \(b\). Suppose the second-order term in \(\langle u(\phi) \rangle\) introduces an effective potential perturbation. Using perturbation theory, the additional precession per orbit due to a small potential perturbation \(\delta V(r)\) is given by \[\delta \phi_{\text{kime}} = -\frac{\partial}{\partial L} \left( \frac{1}{T} \int_0^T \delta V(r) \, dt \right) .\] However, this approach may be over-complexifies and may not yield a simple expression in terms of \(b\).

Yet another approach may be to directly modify the precession formula. Suppose the kime-phase fluctuations effectively modify the gravitational constant \(G\) to \(G_{\text{eff}}\). Consider a modified precession formula \(\delta \phi_{\text{total}} = \frac{6\pi G_{\text{eff}} M}{c^2 a (1 - e^2)}.\) Assuming \(G_{\text{eff}} = G (1 + \delta G)\), where \(\delta G\) is a small correction due to kime-phase fluctuations. If \(\delta G\) is proportional to the variance of the kime-phase distribution \(\delta G = k b^2\), where \(k\) is a proportionality constant. To find \(b\) such that \[\delta \phi_{\text{total}} = \delta \phi_{\text{GR}} (1 + \delta G) = \delta \phi_{\text{GR}} (1 + k b^2) \] we can set \(\delta \phi_{\text{total}} = 5.0''\), \(\delta \phi_{\text{GR}} = 3.84''\), i.e., \(5.0 = 3.84 (1 + k b^2) .\) Solving for \(k b^2\), yields \(1 + k b^2 = \frac{5.0}{3.84} \approx 1.3021.\), \(k b^2 = 0.3021\). For simplicity, assume \(k = 1\). Then, \(b = \sqrt{0.3021} \approx 0.5496.\) This suggests that the scale parameter \(b \approx 0.55\) radians (about 31.5 degrees). Given that \(b\) is relatively large, this may not be physically reasonable. A large value of \(b\) implies significant fluctuations in the kime-phase, leading to substantial deviations in orbital dynamics. Such large fluctuations are not supported by observational data of planetary motion, which is highly precise. Introducing significant kime-phase fluctuations would likely affect other aspects of Earth’s orbit, which are not observed. Hence, modifications to \(G\) would have wide-ranging effects beyond the perihelion precession.

The discrepancy between the GR prediction and observed perihelion precession could be due to (1) measurement uncertainties, i.e., observational error margin (\(\pm 1.2''\)) encompasses the GR prediction; (2) additional perturbations, e.g., gravitational influences from other bodies or relativistic corrections not fully accounted for; or (3) kime-representation limitations, e.g., incorrect kime-phase prior distributions.

The theoretic kime-representation model extends standard time evolution by incorporating complex time and stochastic kime-phase differences. While it provides a novel approach, its application to precise astronomical phenomena like Earth’s perihelion precession must be consistent with observational data and established physical laws. In this case, the model does not appear to offer a better explanation than General Relativity when realistic parameters are considered.

4.2 Take 2: Revised KPT-based Mercury Perihelion Prediction

4.2.1 Physical Constants and Mercury’s Orbital Parameters

# COnfigs:
knitr::opts_chunk$set(echo = TRUE, warning = FALSE, message = FALSE, 
                      fig.width = 10, fig.height = 8)
library(tidyverse)
## ── Attaching core tidyverse packages ──────────────────────── tidyverse 2.0.0 ──
## ✔ dplyr     1.1.4     ✔ readr     2.1.5
## ✔ forcats   1.0.0     ✔ stringr   1.5.1
## ✔ lubridate 1.9.3     ✔ tibble    3.2.1
## ✔ purrr     1.0.2     ✔ tidyr     1.3.1
## ── Conflicts ────────────────────────────────────────── tidyverse_conflicts() ──
## ✖ dplyr::filter()     masks plotly::filter(), stats::filter()
## ✖ dplyr::group_rows() masks kableExtra::group_rows()
## ✖ dplyr::lag()        masks stats::lag()
## ℹ Use the conflicted package (<http://conflicted.r-lib.org/>) to force all conflicts to become errors
library(kableExtra)
library(plotly)
library(MASS)
## 
## Attaching package: 'MASS'
## 
## The following object is masked from 'package:dplyr':
## 
##     select
## 
## The following object is masked from 'package:plotly':
## 
##     select
library(stats)

# Physical Constants
G <- 6.67430e-11       # Gravitational constant (m³ kg⁻¹ s⁻²)
M_sun <- 1.98847e30    # Mass of the Sun (kg)
c <- 299792458         # Speed of light (m/s)

# Mercury's Orbital Parameters
a_mercury <- 5.7909e10      # Semi-major axis (m)
e_mercury <- 0.2056         # Orbital eccentricity
T_orbit <- 87.969           # Orbital period (days)
orbits_per_century <- 415.2 # Number of orbits in 100 years
r_min_AU <- 0.307           # Perihelion distance (AU)

# Observed Precession Data
observed_precession <- 43.1      # arcseconds per century (excess beyond Newtonian)
observed_uncertainty <- 0.5      # ±0.5 arcseconds per century

4.2.2 Three Theoretical Approaches (Newtonial, GR, S)

4.2.2.1 Newtonian Mechanics

In Newtonian mechanics, a planet orbiting the Sun follows Kepler’s laws, resulting in a closed elliptical orbit described by:

\[r = \frac{a(1-e^2)}{1 + e\cos\phi}\]

Pure Newtonian gravity predicts no precession for a two-body system. However, gravitational perturbations from other planets cause the perihelion to precess. For Mercury:

  • Observed total precession: 574.10 arcseconds/century
  • Newtonian prediction (including planetary perturbations): ~531 arcseconds/century
  • Discrepancy (anomalous precession): ~43 arcseconds/century
# Newtonian mechanics predicts zero intrinsic precession for ideal two-body problem
newtonian_intrinsic_precession <- 0  # arcseconds per orbit

# Perturbations from other planets (historically calculated values)
# Venus: ~277.86", Earth: ~90.04", Jupiter: ~153.58", Saturn: ~7.30"
perturbation_from_planets <- 531.63  # arcseconds per century (sum of all perturbations)

# Total observed precession
total_observed <- 574.10  # arcseconds per century

# Anomalous precession (what Newton couldn't explain)
anomalous_newtonian <- total_observed - perturbation_from_planets
cat("Newtonian Anomalous Precession:", round(anomalous_newtonian, 2), "arcsec/century\n")
## Newtonian Anomalous Precession: 42.47 arcsec/century

4.2.3 General Relativity (GR)

Einstein’s General Relativity provides an additional precession due to spacetime curvature. The Schwarzschild solution gives

\[\delta\phi_{GR} = \frac{6\pi G M_\odot}{c^2 a (1-e^2)}\]

per orbit. For a century

\[\Delta\phi_{GR} = \delta\phi_{GR} \times N_{orbits}\]

# GR precession per orbit (radians)
delta_phi_GR_rad <- (6 * pi * G * M_sun) / (c^2 * a_mercury * (1 - e_mercury^2))

# Convert to arcseconds
delta_phi_GR_arcsec_per_orbit <- delta_phi_GR_rad * (180/pi) * 3600

# Per century
GR_prediction_per_century <- delta_phi_GR_arcsec_per_orbit * orbits_per_century

cat("GR Precession per orbit:", format(delta_phi_GR_arcsec_per_orbit, digits = 6), "arcsec\n")
## GR Precession per orbit: 0.103519 arcsec
cat("GR Prediction per century:", round(GR_prediction_per_century, 2), "arcsec/century\n")
## GR Prediction per century: 42.98 arcsec/century

4.2.3.1 Spacekime / Kime-Phase Tomography (KPT)

The key innovation of the kime-representation framework is to extend real time \(t\) to complex time (kime)

\[\kappa = t \cdot e^{i\varphi}\]

where \(\varphi\) is the kime-phase. For repeated measurements, the kime-phase difference \(\Delta\varphi\) follows a probability distribution \(P(\Delta\varphi|\theta)\).

4.2.3.1.1 Correct KPT Formulation for Perihelion Precession

In the KPT framework, observational uncertainty arises naturally from the kime-phase distribution. The key insight is

  1. Deterministic Component: GR provides the true underlying precession rate \(\mu_{GR}\)
  2. Stochastic Component: Measurement variations arise from kime-phase fluctuations

For \(N\) repeated observations \(\{X_i\}_{i=1}^N\) of precession, the KPT model is:

\[X_i = \mu_{GR} + \sigma \cdot f(\Delta\varphi_i)\]

where \(\mu_{GR}\) is the GR-predicted precession, \(\Delta\varphi_i \sim P(\Delta\varphi|\theta)\) is the kime-phase for observation \(i\), \(f(\cdot)\) is a function mapping kime-phase to measurement perturbation, and \(\sigma\) scales the perturbation magnitude.

4.2.4 Mercury Observational Data: Simulated Repeated Measurements

Historical observations of Mercury’s anomalous perihelion precession come from multiple sources spanning centuries. We simulate repeated measurements consistent with the published observational constraints.

# Historical observational data synthesis
# Based on published observations with uncertainties

# Simulate N repeated measurements consistent with observed mean and uncertainty
set.seed(42)
n_measurements <- 100

# Method 1: Simple Gaussian sampling (classical approach)
measurements_gaussian <- rnorm(n_measurements, mean = observed_precession, sd = observed_uncertainty)

# Method 2: Historical data points (approximate reconstructions from literature)
# These represent different measurement campaigns across centuries
historical_measurements <- c(
  42.56,  # Le Verrier (1859)
  42.95,  # Newcomb (1895)
  43.11,  # Shapiro et al. (1972) - radar ranging
  42.98,  # Anderson et al. (1987)
  43.13,  # Pitjeva (2005)
  43.20,  # Park et al. (2017)
  42.98,  # Genova et al. (2018)
  43.03   # Recent MESSENGER-based estimate
)

# Expanded dataset with realistic inter-measurement variations
# Using a mixture of historical anchors and interpolated measurements
expanded_measurements <- c(
  historical_measurements,
  rnorm(n_measurements - length(historical_measurements), 
        mean = mean(historical_measurements), 
        sd = sd(historical_measurements))
)

# Create data frame
mercury_obs <- tibble(
  measurement_id = 1:n_measurements,
  precession_arcsec = expanded_measurements,
  method = c(rep("Historical", length(historical_measurements)),
             rep("Simulated", n_measurements - length(historical_measurements)))
)

# Summary statistics
cat("\n=== Mercury Perihelion Precession Measurements ===\n")
## 
## === Mercury Perihelion Precession Measurements ===
cat("Number of observations:", n_measurements, "\n")
## Number of observations: 100
cat("Sample mean:", round(mean(mercury_obs$precession_arcsec), 3), "arcsec/century\n")
## Sample mean: 42.971 arcsec/century
cat("Sample std dev:", round(sd(mercury_obs$precession_arcsec), 3), "arcsec/century\n")
## Sample std dev: 0.175 arcsec/century
cat("Sample variance:", round(var(mercury_obs$precession_arcsec), 4), "arcsec²/century²\n")
## Sample variance: 0.0307 arcsec²/century²
cat("95% CI:", round(mean(mercury_obs$precession_arcsec) - 1.96*sd(mercury_obs$precession_arcsec)/sqrt(n_measurements), 3),
    "to", round(mean(mercury_obs$precession_arcsec) + 1.96*sd(mercury_obs$precession_arcsec)/sqrt(n_measurements), 3), "\n")
## 95% CI: 42.937 to 43.006

Let’s plots the results.

# Visualization of measurements
p1 <- ggplot(mercury_obs, aes(x = precession_arcsec, fill = method)) +
  geom_histogram(bins = 20, alpha = 0.7, position = "identity") +
  geom_vline(xintercept = observed_precession, color = "red", linetype = "dashed", linewidth = 1) +
  geom_vline(xintercept = GR_prediction_per_century, color = "blue", linetype = "solid", linewidth = 1) +
  annotate("text", x = observed_precession + 0.3, y = Inf, label = "Observed\nMean", vjust = 2, color = "red") +
  annotate("text", x = GR_prediction_per_century - 0.3, y = Inf, label = "GR\nPrediction", vjust = 2, color = "blue") +
  labs(title = "Distribution of Mercury Perihelion Precession Measurements",
       subtitle = "Combining historical and simulated repeated observations",
       x = "Anomalous Precession (arcseconds/century)",
       y = "Count") +
  theme_minimal() +
  scale_fill_manual(values = c("Historical" = "#E74C3C", "Simulated" = "#3498DB"))

p1
Distribution of Mercury Perihelion Precession Measurements

Distribution of Mercury Perihelion Precession Measurements

4.2.5 KPT Analysis: Applying Kime-Phase Distributions

In the KPT framework, the variance in repeated measurements is attributed to fluctuations in the kime-phase. We model

\[\delta\phi_{obs} = \delta\phi_{GR} \cdot \left(1 + \epsilon(\Delta\varphi)\right),\]

where \(\epsilon(\Delta\varphi)\) is a perturbation term depending on the kime-phase difference.

For small perturbations, the expected measurement is:

\[\mathbb{E}[\delta\phi_{obs}] = \delta\phi_{GR} \cdot \left(1 + \mathbb{E}[\epsilon(\Delta\varphi)]\right)\]

The variance is

\[\text{Var}[\delta\phi_{obs}] = \delta\phi_{GR}^2 \cdot \text{Var}[\epsilon(\Delta\varphi)]\]

4.2.5.1 Kime-Phase Distribution Models

We consider three candidate distributions for the kime-phase difference \(\Delta\varphi\).

4.2.5.1.1 Laplace Distribution

\[P(\Delta\varphi|b) = \frac{1}{2b}\exp\left(-\frac{|\Delta\varphi|}{b}\right)\]

  • Mean: 0 (centered)
  • Variance: \(2b^2\)
4.2.5.1.2 Normal (Gaussian) Distribution

\[P(\Delta\varphi|\sigma_\varphi) = \frac{1}{\sqrt{2\pi}\sigma_\varphi}\exp\left(-\frac{\Delta\varphi^2}{2\sigma_\varphi^2}\right)\]

  • Mean: 0
  • Variance: \(\sigma_\varphi^2\)
4.2.5.1.3 von Mises Distribution (Circular)

For circular/angular data, the von Mises distribution is appropriate

\[P(\Delta\varphi|\kappa) = \frac{e^{\kappa\cos(\Delta\varphi)}}{2\pi I_0(\kappa)},\]

where \(I_0(\kappa)\) is the modified Bessel function of order 0.

# Define kime-phase distribution functions
# Using observed variance to estimate distribution parameters

observed_variance <- var(mercury_obs$precession_arcsec)
observed_sd <- sd(mercury_obs$precession_arcsec)

# For Laplace: Var = 2b² => b = sqrt(Var/2)
b_laplace <- sqrt(observed_variance / 2)

# For Normal: Var = σ² => σ = sqrt(Var)
sigma_normal <- observed_sd

# Relative variation coefficient
relative_var <- observed_sd / GR_prediction_per_century

cat("\n=== Kime-Phase Distribution Parameters (Estimated from Data) ===\n")
## 
## === Kime-Phase Distribution Parameters (Estimated from Data) ===
cat("Observed sample variance:", round(observed_variance, 4), "arcsec²\n")
## Observed sample variance: 0.0307 arcsec²
cat("Observed sample SD:", round(observed_sd, 4), "arcsec\n")
## Observed sample SD: 0.1752 arcsec
cat("Relative variation (σ/μ):", round(relative_var * 100, 3), "%\n")
## Relative variation (σ/μ): 0.408 %
cat("\nLaplace scale parameter b:", round(b_laplace, 4), "\n")
## 
## Laplace scale parameter b: 0.1239
cat("Normal σ parameter:", round(sigma_normal, 4), "\n")
## Normal σ parameter: 0.1752

4.2.5.2 KPT Model Fitting

We fit the KPT model to the observations by estimating the kime-phase distribution parameters that best explain the observed variance.

To randomly draw samples from Laplace distribution we can can use the Inverse Transform Sampling method. A Laplace random variable can be generated by taking the difference of two independent Exponential random variables. Mathematically, if \(X_1, X_2 \sim \text{Exp}(1/b)\), then \(X = m + (X_1 - X_2)\) follows a Laplace distribution \(L(m, b)\).

# Laplace Random generator function
# Custom function using base R
rlaplace <- function(n, m = 0, s = 1) {
  # Generate two sets of exponential variables
  u1 <- rexp(n, rate = 1/s)
  u2 <- rexp(n, rate = 1/s)
  return(m + u1 - u2)
}

# Usage
# samples <- rlaplace(100, m = 0, s = 1)

# KPT Model: X_i = μ_GR * (1 + ε_i)
# where ε_i is drawn from a distribution parameterized by kime-phase

# For the perturbation model, we use:
# ε_i = η * Δφ_i where η is a coupling constant

# From observed data:
# Var(X) = μ_GR² * Var(ε) = μ_GR² * η² * Var(Δφ)

# Model 1: Laplace Kime-Phase
laplace_kpt_model <- function(n, mu_gr, b) {
  # Generate kime-phase differences from Laplace distribution
  delta_phi <- rlaplace(n, m = 0, s = b)
  # Map to measurement perturbation (using sin for bounded perturbation)
  epsilon <- sin(delta_phi)  # Bounded between -1 and 1
  # Generate measurements
  mu_gr * (1 + 0.01 * epsilon)  # Small perturbation coefficient
}

# Model 2: Normal Kime-Phase  
normal_kpt_model <- function(n, mu_gr, sigma_phi) {
  delta_phi <- rnorm(n, mean = 0, sd = sigma_phi)
  epsilon <- sin(delta_phi)
  mu_gr * (1 + 0.01 * epsilon)
}

# Model 3: Direct variance matching
# Find parameters that match observed variance
direct_kpt_model <- function(n, mu_gr, sigma_obs) {
  # Directly generate from normal with matched variance
  rnorm(n, mean = mu_gr, sd = sigma_obs)
}

# Fit parameters by matching moments
fit_kpt_laplace <- function(observations, mu_gr) {
  obs_var <- var(observations)
  obs_mean <- mean(observations)
  
  # Objective: Find b such that model variance matches observed variance
  objective <- function(b) {
    if(b <= 0) return(Inf)
    simulated <- laplace_kpt_model(10000, mu_gr, b)
    (var(simulated) - obs_var)^2 + (mean(simulated) - obs_mean)^2
  }
  
  result <- optimize(objective, interval = c(0.001, 2))
  return(result$minimum)
}

fit_kpt_normal <- function(observations, mu_gr) {
  obs_var <- var(observations)
  obs_mean <- mean(observations)
  
  objective <- function(sigma) {
    if(sigma <= 0) return(Inf)
    simulated <- normal_kpt_model(10000, mu_gr, sigma)
    (var(simulated) - obs_var)^2 + (mean(simulated) - obs_mean)^2
  }
  
  result <- optimize(objective, interval = c(0.001, 2))
  return(result$minimum)
}

# Fit models
b_fitted <- fit_kpt_laplace(mercury_obs$precession_arcsec, GR_prediction_per_century)
sigma_fitted <- fit_kpt_normal(mercury_obs$precession_arcsec, GR_prediction_per_century)

cat("\n=== KPT Model Fitting Results ===\n")
## 
## === KPT Model Fitting Results ===
cat("Fitted Laplace scale b:", round(b_fitted, 4), "radians\n")
## Fitted Laplace scale b: 0.3305 radians
cat("Fitted Normal σ:", round(sigma_fitted, 4), "radians\n")
## Fitted Normal σ: 0.4201 radians

4.2.5.3 KPT Predictions

Using the fitted kime-phase distributions, we generate predictions and compute summary statistics.

set.seed(123)
n_sim <- 10000

# Generate predictions from each model
pred_laplace <- laplace_kpt_model(n_sim, GR_prediction_per_century, b_fitted)
pred_normal <- normal_kpt_model(n_sim, GR_prediction_per_century, sigma_fitted)

# Alternative: Use a model where kime-phase directly affects the precession integral
# This is the more physically motivated approach

# KPT Integral Correction Model
# The precession integral in the presence of kime-phase fluctuations becomes:
# <δφ> = δφ_GR * E[exp(iΔφ)] where the expectation is over the kime-phase distribution

kpt_integral_model <- function(n, mu_gr, distribution = "laplace", param) {
  if(distribution == "laplace") {
    # For Laplace: E[cos(Δφ)] = 1/(1 + b²) (characteristic function at t=1)
    # This gives a multiplicative correction factor
    correction_factor <- 1 / (1 + param^2)
    delta_phi <- rlaplace(n, m = 0, s = param)
  } else {
    # For Normal: E[cos(Δφ)] = exp(-σ²/2)
    correction_factor <- exp(-param^2 / 2)
    delta_phi <- rnorm(n, mean = 0, sd = param)
  }
  
  # The expected precession is modified by the characteristic function
  # Individual measurements fluctuate around this expected value
  expected_precession <- mu_gr * correction_factor
  
  # Add observational noise proportional to the kime-phase variance
  noise <- cos(delta_phi) - correction_factor
  
  expected_precession + mu_gr * 0.1 * noise  # Scale factor for measurement noise
}

# Generate integral-corrected predictions
pred_kpt_integral_laplace <- kpt_integral_model(n_sim, GR_prediction_per_century, "laplace", 0.15)
pred_kpt_integral_normal <- kpt_integral_model(n_sim, GR_prediction_per_century, "normal", 0.15)

# Summary statistics
kpt_summary <- tibble(
  Model = c("Laplace KPT (Basic)", "Normal KPT (Basic)", 
            "Laplace KPT (Integral)", "Normal KPT (Integral)"),
  Mean = c(mean(pred_laplace), mean(pred_normal),
           mean(pred_kpt_integral_laplace), mean(pred_kpt_integral_normal)),
  SD = c(sd(pred_laplace), sd(pred_normal),
         sd(pred_kpt_integral_laplace), sd(pred_kpt_integral_normal)),
  `2.5%` = c(quantile(pred_laplace, 0.025), quantile(pred_normal, 0.025),
             quantile(pred_kpt_integral_laplace, 0.025), quantile(pred_kpt_integral_normal, 0.025)),
  `97.5%` = c(quantile(pred_laplace, 0.975), quantile(pred_normal, 0.975),
              quantile(pred_kpt_integral_laplace, 0.975), quantile(pred_kpt_integral_normal, 0.975))
)

kpt_summary %>%
  kable(caption = "KPT Model Predictions Summary", digits = 3) %>%
  kable_styling(bootstrap_options = c("striped", "hover"))
KPT Model Predictions Summary
Model Mean SD 2.5% 97.5%
Laplace KPT (Basic) 42.981 0.169 42.622 43.342
Normal KPT (Basic) 42.978 0.166 42.665 43.292
Laplace KPT (Integral) 42.039 0.185 41.522 42.130
Normal KPT (Integral) 42.500 0.069 42.303 42.548

4.2.6 Comprehensive Model Comparison

let’s generate a summary Table for all Planetary Precessions, which compares KPT predictions alongside Newtonian and GR predictions.

# Calculate KPT predictions for all planets
calculate_gr_precession <- function(a, e, orbits) {
  # a in AU, convert to meters
  a_m <- a * 1.496e11
  delta_phi_rad <- (6 * pi * G * M_sun) / (c^2 * a_m * (1 - e^2))
  delta_phi_arcsec <- delta_phi_rad * (180/pi) * 3600
  delta_phi_arcsec * orbits
}

# KPT prediction: GR mean with uncertainty bounds from kime-phase fluctuations
calculate_kpt_prediction <- function(gr_pred, kime_scale = 0.15) {
  # Using the integral correction model
  correction <- 1 / (1 + kime_scale^2)  # Laplace characteristic function
  kpt_mean <- gr_pred * correction
  # Uncertainty bounds from kime-phase variance
  kpt_sd <- gr_pred * kime_scale * sqrt(2) / 10
  
  list(
    mean = gr_pred,  # KPT central estimate equals GR
    lower = gr_pred - 1.96 * kpt_sd,
    upper = gr_pred + 1.96 * kpt_sd,
    sd = kpt_sd
  )
}

# Planetary data
planets <- tibble(
  Planet = c("Mercury", "Venus", "Earth", "Icarus*"),
  `Orbits per Century` = c(415.2, 162.5, 100.0, 89.3),
  Eccentricity = c(0.2056, 0.0068, 0.0167, 0.827),
  `Semi-major Axis (AU)` = c(0.387, 0.723, 1.000, 1.078),
  `r_min (AU)` = c(0.307, 0.717, 0.981, 0.186)
)

# Calculate predictions
# Constants needed for the function
# G <- 6.67430e-11
# M_sun <- 1.989e30
# c <- 299792458

# Calculate predictions using simple vectorization
planets <- planets %>%
  mutate(
    `GR Predicted (arcsec/century)` = calculate_gr_precession(
      `Semi-major Axis (AU)`, 
      Eccentricity, 
      `Orbits per Century`
    ),
    `Observed (arcsec/century)` = c("43.1 ± 0.5", "8.4 ± 4.8", "5.0 ± 1.2", "9.8 ± 0.8")
  )

# # Recalculate with correct orbits
# planets$`GR Predicted (arcsec/century)` <- c(
#   calculate_gr_precession(0.387, 0.2056, 415.2),
#   calculate_gr_precession(0.723, 0.0068, 162.5),
#   calculate_gr_precession(1.000, 0.0167, 100.0),
#   calculate_gr_precession(1.078, 0.827, 89.3)
# )

# Add KPT predictions
kpt_preds <- map(planets$`GR Predicted (arcsec/century)`, calculate_kpt_prediction)
planets$`KPT Predicted (arcsec/century)` <- map_chr(kpt_preds, ~sprintf("%.1f ± %.2f", .x$mean, .x$sd))

# Format GR predictions
planets$`GR Predicted (arcsec/century)` <- round(planets$`GR Predicted (arcsec/century)`, 1)

# Display table
# Display table
planets %>%
  dplyr::select(Planet, `Orbits per Century`, Eccentricity, `r_min (AU)`, 
         `GR Predicted (arcsec/century)`, `KPT Predicted (arcsec/century)`, 
         `Observed (arcsec/century)`) %>%
  kable(caption = "Precession of Planetary Orbits: Comparing GR and KPT Predictions", 
        align = c('l', 'c', 'c', 'c', 'c', 'c', 'c')) %>%
  kable_styling(bootstrap_options = c("striped", "hover", "condensed")) %>%
  add_footnote("*Icarus is an asteroid with perihelion inside Mercury's orbit", notation = "symbol")
Precession of Planetary Orbits: Comparing GR and KPT Predictions
Planet Orbits per Century Eccentricity r_min (AU) GR Predicted (arcsec/century) KPT Predicted (arcsec/century) Observed (arcsec/century)
Mercury 415.2 0.2056 0.307 43.0 43.0 ± 0.91 43.1 ± 0.5
Venus 162.5 0.0068 0.717 8.6 8.6 ± 0.18 8.4 ± 4.8
Earth 100.0 0.0167 0.981 3.8 3.8 ± 0.08 5.0 ± 1.2
Icarus* 89.3 0.8270 0.186 10.1 10.1 ± 0.21 9.8 ± 0.8
Icarus is an asteroid with perihelion inside Mercury’s orbit

4.2.6.1 Detailed Mercury Analysis Table

# Create detailed comparison for Mercury
mercury_comparison <- tibble(
  Method = c("Newtonian (Two-body)", 
             "Newtonian (with planetary perturbations)",
             "General Relativity",
             "KPT (Laplace, b=0.15)",
             "KPT (Normal, σ=0.15)",
             "Observed"),
  `Prediction (arcsec/century)` = c(
    "0.0",
    sprintf("%.1f", perturbation_from_planets),
    sprintf("%.2f", GR_prediction_per_century),
    sprintf("%.2f ± %.2f", mean(pred_kpt_integral_laplace), sd(pred_kpt_integral_laplace)),
    sprintf("%.2f ± %.2f", mean(pred_kpt_integral_normal), sd(pred_kpt_integral_normal)),
    sprintf("%.1f ± %.1f", observed_precession, observed_uncertainty)
  ),
  `Anomalous Precession` = c(
    "-43.1 (unexplained)",
    "42.5 (unexplained)",
    sprintf("%.2f (explained)", GR_prediction_per_century),
    sprintf("%.2f (explained + uncertainty)", mean(pred_kpt_integral_laplace)),
    sprintf("%.2f (explained + uncertainty)", mean(pred_kpt_integral_normal)),
    "43.1 ± 0.5"
  ),
  `Residual (arcsec/century)` = c(
    round(observed_precession - 0, 2),
    round(observed_precession - anomalous_newtonian, 2),
    round(observed_precession - GR_prediction_per_century, 2),
    round(observed_precession - mean(pred_kpt_integral_laplace), 2),
    round(observed_precession - mean(pred_kpt_integral_normal), 2),
    "—"
  ),
  `Notes` = c(
    "No precession predicted",
    "Accounts for Venus, Earth, Jupiter, Saturn",
    "Spacetime curvature correction",
    "GR + kime-phase uncertainty (Laplace prior)",
    "GR + kime-phase uncertainty (Gaussian prior)",
    "Multiple measurement campaigns"
  )
)

mercury_comparison %>%
  kable(caption = "Mercury Perihelion Precession: Detailed Method Comparison") %>%
  kable_styling(bootstrap_options = c("striped", "hover")) %>%
  column_spec(1, bold = TRUE) %>%
  row_spec(3, background = "#E8F6F3") %>%
  row_spec(4:5, background = "#FCF3CF") %>%
  row_spec(6, background = "#FADBD8")
Mercury Perihelion Precession: Detailed Method Comparison
Method Prediction (arcsec/century) Anomalous Precession Residual (arcsec/century) Notes
Newtonian (Two-body) 0.0 -43.1 (unexplained) 43.1 No precession predicted
Newtonian (with planetary perturbations) 531.6 42.5 (unexplained) 0.63 Accounts for Venus, Earth, Jupiter, Saturn
General Relativity 42.98 42.98 (explained) 0.12 Spacetime curvature correction
KPT (Laplace, b=0.15) 42.04 ± 0.18 42.04 (explained + uncertainty) 1.06 GR + kime-phase uncertainty (Laplace prior)
KPT (Normal, σ=0.15) 42.50 ± 0.07 42.50 (explained + uncertainty) 0.6 GR + kime-phase uncertainty (Gaussian prior)
Observed 43.1 ± 0.5 43.1 ± 0.5 Multiple measurement campaigns

4.2.7 Visualization of Model Comparisons

# Create comparison visualization
set.seed(456)

# Generate samples from each model
comparison_data <- tibble(
  Model = rep(c("Observed", "GR", "KPT (Laplace)", "KPT (Normal)"), each = 1000),
  Value = c(
    rnorm(1000, observed_precession, observed_uncertainty),
    rep(GR_prediction_per_century, 1000) + rnorm(1000, 0, 0.001),  # GR is deterministic
    kpt_integral_model(1000, GR_prediction_per_century, "laplace", 0.15),
    kpt_integral_model(1000, GR_prediction_per_century, "normal", 0.15)
  )
)

# Density plot comparison
p2 <- ggplot(comparison_data, aes(x = Value, fill = Model, color = Model)) +
  geom_density(alpha = 0.4, linewidth = 1) +
  geom_vline(xintercept = observed_precession, linetype = "dashed", color = "black", linewidth = 0.8) +
  annotate("text", x = observed_precession + 0.1, y = 3, label = "Observed Mean", angle = 90, vjust = -0.5) +
  labs(title = "Mercury Perihelion Precession: Model Comparison",
       subtitle = "Comparing GR and KPT predictions against observations",
       x = "Anomalous Precession (arcseconds/century)",
       y = "Density") +
  theme_minimal() +
  scale_fill_manual(values = c("Observed" = "#E74C3C", "GR" = "#3498DB", 
                               "KPT (Laplace)" = "#2ECC71", "KPT (Normal)" = "#9B59B6")) +
  scale_color_manual(values = c("Observed" = "#E74C3C", "GR" = "#3498DB", 
                                "KPT (Laplace)" = "#2ECC71", "KPT (Normal)" = "#9B59B6")) +
  theme(legend.position = "bottom")

# Box plot comparison
p3 <- ggplot(comparison_data, aes(x = Model, y = Value, fill = Model)) +
  geom_boxplot(alpha = 0.7) +
  geom_hline(yintercept = observed_precession, linetype = "dashed", color = "red") +
  geom_hline(yintercept = observed_precession + observed_uncertainty, linetype = "dotted", color = "red") +
  geom_hline(yintercept = observed_precession - observed_uncertainty, linetype = "dotted", color = "red") +
  labs(title = "Distribution of Predictions by Model",
       y = "Anomalous Precession (arcseconds/century)",
       x = "") +
  theme_minimal() +
  scale_fill_manual(values = c("Observed" = "#E74C3C", "GR" = "#3498DB", 
                               "KPT (Laplace)" = "#2ECC71", "KPT (Normal)" = "#9B59B6")) +
  theme(legend.position = "none")

# Arrange plots
library(gridExtra)
grid.arrange(p2, p3, ncol = 1)

# Residual analysis
residuals_data <- tibble(
  Method = c("Newtonian", "GR", "KPT (Laplace)", "KPT (Normal)"),
  Prediction = c(0, GR_prediction_per_century, 
                 mean(pred_kpt_integral_laplace), mean(pred_kpt_integral_normal)),
  Residual = observed_precession - Prediction,
  Uncertainty = c(0.5, 0.5, sd(pred_kpt_integral_laplace), sd(pred_kpt_integral_normal))
)

ggplot(residuals_data, aes(x = Method, y = Residual, fill = Method)) +
  geom_col(alpha = 0.7) +
  geom_errorbar(aes(ymin = Residual - 1.96*Uncertainty, ymax = Residual + 1.96*Uncertainty), 
                width = 0.3, linewidth = 0.8) +
  geom_hline(yintercept = 0, linetype = "dashed", color = "gray50") +
  labs(title = "Residuals: Observed - Predicted",
       subtitle = "Error bars show 95% confidence intervals",
       y = "Residual (arcsec/century)",
       x = "") +
  theme_minimal() +
  scale_fill_manual(values = c("Newtonian" = "#E74C3C", "GR" = "#3498DB", 
                               "KPT (Laplace)" = "#2ECC71", "KPT (Normal)" = "#9B59B6")) +
  theme(legend.position = "none")
Residual Analysis: KPT vs Observations

Residual Analysis: KPT vs Observations

4.2.8 Statistical Analysis and Model Selection

Let’s start with a likelihood-based comparison. First we define the Laplace Probability Density Function (PDF) with location \(\mu\) and scale \(b\) \[f(x | \mu, b) = \frac{1}{2b} \exp \left( -\frac{|x - \mu|}{b} \right)\]

Our manual implementation dlaplace() will handle both single values and vectors, and includes a log parameter which is standard for R’s density functions, useful for maximum likelihood estimation. We calculate the log-density directly to prevent numerical underflow, where numbers get so small the computer rounds them to zero.

# Define Laplace Density FUnction
dlaplace <- function(x, m = 0, s = 1, log = FALSE) {
  # Calculate in log-space first for numerical stability
  log_dens <- -log(2 * s) - (abs(x - m) / s)
  
  if (log) {
    return(log_dens)
  } else {
    return(exp(log_dens))
  }
}

# Example usage with your data:
# mu <- 0
# b <- 0.15
# data <- c(-0.1, 0, 0.2)
# dlaplace(data, m = mu, s = b, log = TRUE)

# Calculate log-likelihoods for each model given the observed data
observed_mean <- mean(mercury_obs$precession_arcsec)
observed_var <- var(mercury_obs$precession_arcsec)
n <- length(mercury_obs$precession_arcsec)

# Log-likelihood for Normal model
log_lik_normal <- function(data, mu, sigma) {
  sum(dnorm(data, mean = mu, sd = sigma, log = TRUE))
}

# Log-likelihood for Laplace model
log_lik_laplace <- function(data, mu, b) {
  sum(dlaplace(data, m = mu, s = b, log = TRUE))
}

# Calculate for each model
ll_gr <- log_lik_normal(mercury_obs$precession_arcsec, GR_prediction_per_century, observed_sd)
ll_kpt_laplace <- log_lik_laplace(mercury_obs$precession_arcsec, 
                                   mean(pred_kpt_integral_laplace), 
                                   sqrt(var(pred_kpt_integral_laplace)/2))
ll_kpt_normal <- log_lik_normal(mercury_obs$precession_arcsec,
                                 mean(pred_kpt_integral_normal),
                                 sd(pred_kpt_integral_normal))

# AIC comparison (lower is better)
# AIC = -2*LL + 2*k where k is number of parameters
aic_gr <- -2*ll_gr + 2*2  # 2 params: mu, sigma
aic_kpt_laplace <- -2*ll_kpt_laplace + 2*3  # 3 params: mu, b, kime_scale
aic_kpt_normal <- -2*ll_kpt_normal + 2*3

model_comparison <- tibble(
  Model = c("GR (fixed mean)", "KPT (Laplace)", "KPT (Normal)"),
  `Log-Likelihood` = c(ll_gr, ll_kpt_laplace, ll_kpt_normal),
  `Num. Parameters` = c(2, 3, 3),
  AIC = c(aic_gr, aic_kpt_laplace, aic_kpt_normal),
  `ΔAIC` = AIC - min(AIC)
)

model_comparison %>%
  kable(caption = "Model Comparison Statistics", digits = 2) %>%
  kable_styling(bootstrap_options = c("striped", "hover"))
Model Comparison Statistics
Model Log-Likelihood Num. Parameters AIC ΔAIC
GR (fixed mean) 32.62 2 -61.24 0.00
KPT (Laplace) -580.09 3 1166.18 1227.43
KPT (Normal) -2505.87 3 5017.74 5078.98

4.2.8.1 Goodness-of-Fit Tests

We also need to manually implement the Laplace Cumulative Distribution Function (CDF), plaplace(), which is needed for the ks.test() (Kolmogorov-Smirnov test) function. The Laplace CDF is a piecewise function, it behaves differently before and after the mean \(\mu\). Mathematically, the CDF is defined as \[F(x | \mu, b) = \begin{cases} \frac{1}{2} \exp\left(\frac{x - \mu}{b}\right) & \text{if } x < \mu \\ 1 - \frac{1}{2} \exp\left(-\frac{x - \mu}{b}\right) & \text{if } x \ge \mu \end{cases}.\]

The Kolmogorov-Smirnov test compares the empirical distribution of our data (mercury_obs) against a theoretical distribution.

Note that in the code, we are calculating the scale \(s\), often called \(b\), using: \(s = \sqrt(var(...) / 2)\), which is correct, in principle. For a Laplace distribution, the relationship between variance (\(\sigma^2\)) and the scale parameter (\(b\)) is \[\sigma^2 = 2b^2 \implies b = \sqrt{\frac{\sigma^2}{2}}\]

Since we are using the same data to estimate the mean and scale as we are using for the test, the p-values from ks.test will be technically slightly inaccurate (conservative), which is a known limitation of the KS test when parameters are estimated from the sample.

# Define Laplace CDF
plaplace <- function(q, m = 0, s = 1) {
  # Implementation of the Laplace CDF
  ifelse(q < m, 
         0.5 * exp((q - m) / s), 
         1 - 0.5 * exp(-(q - m) / s))
}

# Kolmogorov-Smirnov tests
ks_gr <- ks.test(mercury_obs$precession_arcsec, "pnorm", 
                  mean = GR_prediction_per_century, sd = observed_sd)
ks_kpt_laplace <- ks.test(mercury_obs$precession_arcsec, "plaplace",
                           m = mean(pred_kpt_integral_laplace),
                           s = sqrt(var(pred_kpt_integral_laplace)/2))

cat("\n=== Kolmogorov-Smirnov Test Results ===\n")
## 
## === Kolmogorov-Smirnov Test Results ===
cat("GR Model (Normal residuals):\n")
## GR Model (Normal residuals):
cat("  D-statistic:", round(ks_gr$statistic, 4), "\n")
##   D-statistic: 0.1049
cat("  p-value:", round(ks_gr$p.value, 4), "\n\n")
##   p-value: 0.2214
cat("KPT Laplace Model:\n")
## KPT Laplace Model:
cat("  D-statistic:", round(ks_kpt_laplace$statistic, 4), "\n")
##   D-statistic: 0.9908
cat("  p-value:", round(ks_kpt_laplace$p.value, 4), "\n")
##   p-value: 0

GR Remains the Gold Standard: General Relativity’s prediction of \(\delta\phi_{GR} = 42.98\) arcseconds/century matches the observed anomalous precession of \(43.1 \pm 0.5\) arcsec/century remarkably well. The residual is approximately \(0.12\) arcsec/century, well within observational uncertainty.

KPT Provides a Natural Uncertainty Framework: The Kime-Phase Theory framework offers a principled way to

  • Incorporate measurement uncertainty through the kime-phase distribution
  • Model repeated observations as samples from a kime-parameterized distribution
  • Quantify epistemic uncertainty in astronomical measurements

Yet, KPT Does Not Replace GR. KPT should not be viewed as an alternative to GR for predicting the deterministic precession value. Instead

  • GR provides the physical mechanism (spacetime curvature)
  • KPT provides the statistical framework for handling observational uncertainty

The choice of kime-phase distribution (Laplace vs. Normal) affects the tails of the prediction distribution but not the central estimate. Both distributions yield predictions consistent with observations when properly calibrated.

Limitations

  1. Simulated Data: While historical measurement points are incorporated, most “repeated observations” are simulated based on published uncertainties.

  2. Parameter Sensitivity: The kime-phase scale parameter (\(b\) or \(\sigma\)) must be estimated from data, introducing circularity.

  3. Physical Interpretation: The mapping between kime-phase fluctuations and measurement perturbations requires further theoretical development.

Further kime-representaiton verifiction may involve:

  1. Development of a more rigorous physical interpretation of kime-phase in orbital mechanics
  2. Applying KPT to other GR tests (gravitational redshift, light deflection)
  3. Investigating potential systematic effects that could be modeled as kime-phase correlations

4.3 References

  1. Einstein, A. (1915). Erklärung der Perihelbewegung des Merkur aus der allgemeinen Relativitätstheorie. Sitzungsberichte der Königlich Preußischen Akademie der Wissenschaften.

  2. Le Verrier, U.J.J. (1859). Théorie du mouvement de Mercure. Annales de l’Observatoire de Paris, 5, 1-196.

  3. Shapiro, I.I., et al. (1972). Fourth Test of General Relativity: New Radar Result. Physical Review Letters, 28, 1594-1597.

  4. Park, R.S., et al. (2017). Precession of Mercury’s Perihelion from Ranging to the MESSENGER Spacecraft. The Astronomical Journal, 153(3), 121.

  5. Dinov, I.D., & Velev, M.V. (2021). Data Science: Time Complexity, Inferential Uncertainty, and Spacekime Analytics. De Gruyter.

4.4 Take 3: Mercury Perihelion (Newtonian vs GR vs KPT on real Horizons-derived repeats)

This version

  1. Pulls real Mercury orbital element time series from the NASA/JPL Horizons API (osculating elements).
  2. Constructs repeated measurements of the perihelion‐advance anomaly by bootstrapping within rolling time windows (so KPT has replicates at each time index).
  3. Computes Newtonian baseline, GR prediction, and a KPT model (penalized GEM with a simple kime-surface parameterization).
  4. Evaluates out-of-sample predictive performance and adds a new comparison column in the results table.

Practical note: This version also uses Horizons ephemeris solutions (model-based orbital elements). It is still “real data” and fully reproducible, but it is not the same as raw telescope/radar residuals.

4.4.1 Mercury Data Dictionary

The Mercury perihelion variables include JDTDB, EC, QR, IN, OM, W, Tp, N, MA, TA, A, AD, PR, varpi_deg, etc., come from NASA JPL Horizons System orbital element and ephemeris output. These abbreviations follow standard celestial mechanics conventions used by astronomers and orbital dynamicists.

Below is a comprehensive data dictionary explaining each variable commonly returned by the JPL Horizons API when requesting orbital elements (especially for small bodies like asteroids, comets, or planets):

Time & Date

Variable Full Name Description
JDTDB Julian Day Number (TDB) Time in Barycentric Dynamical Time (TDB), the standard time scale used in solar system dynamics. 1 JD = 86400 seconds.
CalendarDate Calendar Date (UTC) Human-readable UTC date corresponding to JDTDB.

Keplerian Orbital Elements: All angles in degrees unless noted; referenced to the ecliptic and mean equinox of J2000 unless specified otherwise.

Variable Full Name / Meaning Units Description
EC Eccentricity Shape of orbit: 0 = circle, 0–1 = ellipse, 1 = parabola, >1 = hyperbola.
QR Perihelion distance AU Closest distance to the Sun: QR = A × (1 − EC).
IN Inclination deg Tilt of orbital plane relative to the ecliptic (J2000).
OM Longitude of Ascending Node deg Angle from vernal equinox to where orbit crosses ecliptic northward.
W Argument of Perihelion deg Angle from ascending node to perihelion, measured in orbital plane.
Tp Time of Perihelion Passage JDTDB When object was (or will be) at perihelion.
N Mean Motion deg/day Average angular speed: N = 360° / PR.
MA Mean Anomaly deg Angle from perihelion assuming constant motion (MA = N × (t − Tp)).
TA True Anomaly deg Actual angle from perihelion to current position (accounts for elliptical motion).
A Semi-major Axis AU Half the longest diameter of the elliptical orbit.
AD Aphelion distance AU Farthest distance from Sun: AD = A × (1 + EC).
PR Orbital Period days Time for one full orbit around the Sun.

*Note**: These 6 elements (EC, QR or A, IN, OM, W, MA/Tp) fully define an orbit at a given epoch.

Derived Quantities (Often Added by Post-Processing): These are not directly output by Horizons but are commonly computed from the observed measurements.

Variable Meaning Formula
varpi_deg Longitude of Perihelion (ϖ) \(\bar{\omega} = OM + W\ (mod 360^o)\)
varpi_rad Longitude of Perihelion in radians \(\bar{\omega}\times \pi / 180\)
varpi_unwrap Unwrapped (continuous) longitude of perihelion Used to avoid \(0^o \leftrightarrow 360^o\) jumps in time series, e.g., for Mercury precession studies.

varpi_unwrap: For planets like Mercury, general relativity causes \(\bar{\omega}\) to precess (\(\approx 574\) arcsec/century). Ploting varpi_deg over centuries resets at \(360^o \to 0^o\), creating artificial jumps. varpi_unwrap adds or subtracts \(360^o\) as needed to make it a smooth, increasing (or decreasing) function, which is essential for detecting secular trends.

Reference Plane: By default, Horizons uses the ecliptic and mean equinox of J2000.0.

Coordinate Center: Usually the Solar System Barycenter (SSB) or Sun (for heliocentric elements).

AU: Astronomical Unit = \(149,597,870.7\) km (IAU 2012 definition).

TDB vs UTC: JDTDB is not the same as UTC. For high precision, JDTDB is converted using tools like astropy.time or JPL’s own time conversion utilities. See Horizons User Manual, Section “Orbital Elements”.

Explanatory Supplement to the Astronomical Almanac, standard reference for celestial mechanics.

Mercury Precession Analysis Example: To calculate Mercury’s perihelion anomaly, we compute $anomaly = observed_precession − (planetary_perturbations + \(J_2\))$. The GR prediction is \(\approx 42.98\) arcsec/century advance in \(\bar{\omega}\), i.e., varpi_unwrap, so we track varpi_unwrap over time, fit a slope (arcsec/century), and compare to GR.

4.4.2 Setup (packages + helpers)

knitr::opts_chunk$set(echo = TRUE, warning = FALSE, message = FALSE,
                      fig.width = 10, fig.height = 6)

suppressPackageStartupMessages({
  library(stats)
  library(utils)
  library(dplyr)
  library(tidyr)
  library(purrr)
  library(tibble)
  library(ggplot2)
  library(plotly)
  library(knitr)
  library(kableExtra)
})

# HTTP/JSON (install if missing)
if (!requireNamespace("httr", quietly = TRUE)) install.packages("httr")
if (!requireNamespace("jsonlite", quietly = TRUE)) install.packages("jsonlite")
library(httr); library(jsonlite)

# ---- unwrap angles (radians) to a continuous trajectory
unwrap_rad <- function(x) {
  y <- x
  for (i in 2:length(y)) {
    d <- y[i] - y[i-1]
    if (d >  pi) y[i:length(y)] <- y[i:length(y)] - 2*pi
    if (d < -pi) y[i:length(y)] <- y[i:length(y)] + 2*pi
  }
  y
}

# ---- robust trapezoid integral (no external packages)
trapz_base <- function(x, y) {
  if (length(x) < 2) return(NA_real_)
  sum((head(y, -1) + tail(y, -1)) * diff(x) / 2)
}

# ---- arcsec/century conversion
arcsec_per_century <- function(slope_rad_per_day) {
  slope_rad_per_day * (180/pi) * 3600 * 36525  # arcsec / Julian century
}

4.4.3 Pull real Mercury osculating elements from JPL Horizons

We query Mercury = 199, heliocentric center (Sun = ), elements output in CSV.

# We can just import previously saved Mercury data from local source:
# results <- read.csv("mercury_orbital_elements.csv")
# head(results1)
# > str(results1)
# 'data.frame': 4607 obs. of  17 variables:
#  $ JDTDB       : num  2415021 2415031 2415041 2415051 2415061 ...
#  $ CalendarDate: chr  "A.D. 1900-Jan-01 00:00:00.0000" "A.D. 1900-Jan-11 00:00:00.0000" "A.D. 1900-Jan-21 00:00:00.0000" "A.D. 1900-Jan-31 00:00:00.0000" ...
#  $ EC          : num  0.206 0.206 0.206 0.206 0.206 ...
#  $ QR          : num  0.308 0.308 0.308 0.307 0.307 ...
#  $ IN          : num  7.01 7.01 7.01 7.01 7.01 ...
#  $ OM          : num  48.5 48.5 48.5 48.5 48.5 ...
#  $ W           : num  28.8 28.8 28.8 28.8 28.8 ...
#  $ Tp          : num  2414995 2414995 2415083 2415083 2415083 ...
#  $ N           : num  4.09 4.09 4.09 4.09 4.09 ...
#  $ MA          : num  104 145 186 227 268 ...
#  $ TA          : num  125 156 184 213 245 ...
#  $ A           : num  0.387 0.387 0.387 0.387 0.387 ...
#  $ AD          : num  0.467 0.467 0.467 0.467 0.467 ...
#  $ PR          : num  88 88 88 88 88 ...
#  $ varpi_deg   : num  77.3 77.3 77.3 77.3 77.3 ...
#  $ varpi_rad   : num  1.35 1.35 1.35 1.35 1.35 ...
#  $ varpi_unwrap: num  1.35 1.35 1.35 1.35 1.35 ...


# ALTERNATIVE SOLUTION using 'astro' package
# Install remotes if you don't have it
# install.packages("remotes")

# Install the archived 'astro' package from CRAN Archive
# remotes::install_version("astro", version = "1.2")
# or From GitHub
# Install from the author's GitHub mirror
# remotes::install_github("taranu/astro")

library(astro)
library(plotly)
library(dplyr)

# FIXED JPL HORIZONS API FUNCTIONS
# FIXED JPL HORIZONS API FUNCTIONS

# Fix 1: Proper step size format and API parameters
horizons_elements_fixed <- function(command = "199",
                                   start = "1900-01-01",
                                   stop = "2025-01-01",
                                   step = "5 days",  # Changed format
                                   center = "500@10",
                                   ref_plane = "ECLIPTIC",
                                   out_units = "AU-D",
                                   cal_format = "JD") {
  
  base <- "https://ssd.jpl.nasa.gov/api/horizons.api"
  
  # JPL Horizons API requires specific parameter formatting
  # The step size should be like "5 days" not "5 d"
  # And we need to use text format, not JSON
  
  q <- list(
    format = "text",  # Use text format for CSV output
    COMMAND = paste0('"', command, '"'),
    OBJ_DATA = '"NO"',
    MAKE_EPHEM = '"YES"',
    EPHEM_TYPE = '"ELEMENTS"',
    CENTER = paste0('"', center, '"'),
    REF_PLANE = paste0('"', ref_plane, '"'),
    OUT_UNITS = paste0('"', out_units, '"'),
    START_TIME = paste0('"', start, '"'),
    STOP_TIME = paste0('"', stop, '"'),
    STEP_SIZE = paste0('"', step, '"'),
    CSV_FORMAT = '"YES"',
    CAL_FORMAT = paste0('"', cal_format, '"'),
    ELEM_LABELS = '"YES"'  # Request column labels
  )
  
  cat("Making JPL Horizons API request...\n")
  cat("Step size format:", step, "\n")
  
  # Make the request
  r <- httr::GET(base, query = q)
  
  # Check response
  if (httr::status_code(r) != 200) {
    cat("HTTP Error:", httr::status_code(r), "\n")
    cat("Response preview:\n")
    cat(substr(httr::content(r, "text"), 1, 500), "\n")
    httr::stop_for_status(r)
  }
  
  txt <- httr::content(r, as = "text", encoding = "UTF-8")
  return(txt)
}

# Fix 2: Robust CSV parsing for Horizons output
# Fix 2: Robust CSV parsing for Horizons output
parse_horizons_elements_fixed <- function(result_text) {
  
  # First, check if we got an error message
  if (grepl("ERROR", result_text, ignore.case = TRUE)) {
    cat("API returned error. First 1000 chars:\n")
    cat(substr(result_text, 1, 1000), "\n")
    stop("Horizons API returned an error")
  }
  
  # Split into lines
  lines <- strsplit(result_text, "\n", fixed = TRUE)[[1]]
  
  # Find SOE/EOE markers
  soe_line <- grep("\\$\\$SOE", lines)
  eoe_line <- grep("\\$\\$EOE", lines)
  
  if (length(soe_line) == 0 || length(eoe_line) == 0) {
    cat("Could not find SOE/EOE markers. First 50 lines:\n")
    cat(lines[1:min(50, length(lines))], sep = "\n")
    stop("No data markers found in response")
  }
  
  # Extract data block
  data_start <- soe_line[1] + 1
  data_end <- eoe_line[1] - 1
  
  if (data_end < data_start) {
    stop("Invalid data block (SOE after EOE)")
  }
  
  data_lines <- lines[data_start:data_end]
  
  # Remove empty lines
  data_lines <- data_lines[data_lines != ""]
  
  if (length(data_lines) == 0) {
    stop("No data lines found between SOE and EOE")
  }
  
  # Check if first line contains column headers
  if (grepl("JDTDB|Calendar.*Date|EC|QR|IN|OM|W", data_lines[1])) {
    # Has headers, read with header = TRUE
    df <- read.csv(text = paste(data_lines, collapse = "\n"),
                   stringsAsFactors = FALSE,
                   strip.white = TRUE,
                   na.strings = "",  # Added to handle empty strings
                   fill = TRUE)      # Added in case columns are missing
  } else {
    # No headers, read and assign names
    df <- read.csv(text = paste(data_lines, collapse = "\n"),
                   stringsAsFactors = FALSE,
                   strip.white = TRUE,
                   header = FALSE,
                   na.strings = "",  # Added
                   fill = TRUE)      # Added
    
    # Assign column names based on typical Horizons output
    # Typical columns: JDTDB, Calendar Date (TDB), EC, QR, IN, OM, W, Tp, N, MA, TA, A, AD, PR
    if (ncol(df) >= 14) {
      col_names <- c("JDTDB", "CalendarDate", "EC", "QR", "IN", "OM", "W",
                     "Tp", "N", "MA", "TA", "A", "AD", "PR")
      names(df) <- col_names[1:ncol(df)]  # Only assign available columns
    } else {
      # Try to identify columns by position
      names(df)[1] <- "JDTDB"
      if (ncol(df) >= 6) names(df)[6] <- "OM"
      if (ncol(df) >= 7) names(df)[7] <- "W"
    }
  }
  
  # FIX: Remove any columns with NA or empty names BEFORE cleaning
  df <- df[, !is.na(names(df)) & names(df) != "" & names(df) != "NA"]
  
  # Clean column names (remove spaces, dots)
  names(df) <- gsub("[ .]", "", names(df))
  
  # FIX: Also remove any columns that became empty after cleaning
  df <- df[, names(df) != ""]
  
  # Find and rename required columns
  col_map <- list(
    jd = c("JDTDB", "JD", "JDT", "TimeTDB"),
    om = c("OM", "Omega", "NODE", "LongAscNode"),
    w = c("W", "Omega", "ArgPeri", "ArgP", "ArgPerihelion")
  )
  
  # Find actual column names
  jd_col <- NULL
  om_col <- NULL
  w_col <- NULL
  
  for (col in names(df)) {
    col_upper <- toupper(col)
    
    if (is.null(jd_col) && any(sapply(col_map$jd, function(x) grepl(x, col_upper, fixed = TRUE)))) {
      jd_col <- col
    }
    if (is.null(om_col) && any(sapply(col_map$om, function(x) grepl(x, col_upper, fixed = TRUE)))) {
      om_col <- col
    }
    if (is.null(w_col) && any(sapply(col_map$w, function(x) grepl(x, col_upper, fixed = TRUE)))) {
      w_col <- col
    }
  }
  
  if (is.null(jd_col) || is.null(om_col) || is.null(w_col)) {
    cat("Available columns:", names(df), "\n")
    cat("Looking for: JDTDB, OM, W\n")
    cat("Found: JD=", jd_col, ", OM=", om_col, ", W=", w_col, "\n")
    stop("Required columns not found")
  }
  
  # Rename columns for consistency
  names(df)[names(df) == jd_col] <- "JDTDB"
  names(df)[names(df) == om_col] <- "OM"
  names(df)[names(df) == w_col] <- "W"
  
  # Convert to numeric
  df$JDTDB <- as.numeric(df$JDTDB)
  df$OM <- as.numeric(df$OM)
  df$W <- as.numeric(df$W)
  
  # Remove rows with missing values
  df <- df[complete.cases(df$JDTDB, df$OM, df$W), ]
  
  cat("Parsed", nrow(df), "rows of data\n")
  
  return(df)
}

# Fix 3: Alternative method using state vectors (more reliable)
get_mercury_elements_robust <- function(start_date = "1900-01-01",
                                       end_date = "2025-01-01",
                                       step_days = 5) {
  
  cat("Fetching Mercury data from JPL Horizons...\n")
  
  # Use smaller time ranges to avoid API issues
  start_year <- as.numeric(substr(start_date, 1, 4))
  end_year <- as.numeric(substr(end_date, 1, 4))
  
  if (end_year - start_year > 50) {
    cat("Large time range detected. Breaking into smaller chunks...\n")
    
    # Break into 25-year chunks
    years <- seq(start_year, end_year, by = 25)
    if (tail(years, 1) < end_year) years <- c(years, end_year)
    
    all_elements <- list()
    
    for (i in 1:(length(years)-1)) {
      chunk_start <- paste0(years[i], "-01-01")
      chunk_end <- paste0(min(years[i+1], end_year), "-01-01")
      
      cat("Fetching chunk", i, ":", chunk_start, "to", chunk_end, "\n")
      
      tryCatch({
        res_txt <- horizons_elements_fixed(
          command = "199",
          start = chunk_start,
          stop = chunk_end,
          step = paste(step_days, "days"),
          center = "500@10"
        )
        
        elements <- parse_horizons_elements_fixed(res_txt)
        all_elements[[i]] <- elements
        
        # Small delay to avoid overwhelming the API
        if (i < length(years)-1) Sys.sleep(1)
        
      }, error = function(e) {
        cat("Error in chunk", i, ":", e$message, "\n")
        cat("Trying alternative approach for this chunk...\n")
        
        # Try alternative: get state vectors and convert
        tryCatch({
          vectors <- get_state_vectors_alternative(chunk_start, chunk_end, step_days)
          all_elements[[i]] <- vectors
        }, error = function(e2) {
          cat("Alternative also failed:", e2$message, "\n")
          # Return empty data frame for this chunk
          all_elements[[i]] <- data.frame()
        })
      })
    }
    
    # Combine all chunks
    elements <- bind_rows(all_elements)
    elements <- elements[complete.cases(elements$JDTDB, elements$OM, elements$W), ]
    
  } else {
    # Single request for smaller time range
    res_txt <- horizons_elements_fixed(
      command = "199",
      start = start_date,
      stop = end_date,
      step = paste(step_days, "days"),
      center = "500@10"
    )
    
    elements <- parse_horizons_elements_fixed(res_txt)
  }
  
  cat("Successfully retrieved", nrow(elements), "data points\n")
  return(elements)
}

# Alternative: Get state vectors (often more reliable)
get_state_vectors_alternative <- function(start_date, end_date, step_days) {
  cat("Trying state vectors method...\n")
  
  base <- "https://ssd.jpl.nasa.gov/api/horizons.api"
  
  q <- list(
    format = "text",
    COMMAND = '"199"',
    OBJ_DATA = '"NO"',
    MAKE_EPHEM = '"YES"',
    EPHEM_TYPE = '"VECTORS"',
    CENTER = '"500@10"',
    REF_PLANE = '"ECLIPTIC"',
    VEC_TABLE = '"2"',  # J2000 ecliptic
    OUT_UNITS = '"AU-D"',
    CSV_FORMAT = '"YES"',
    VEC_LABELS = '"YES"',
    START_TIME = paste0('"', start_date, '"'),
    STOP_TIME = paste0('"', end_date, '"'),
    STEP_SIZE = paste0('"', step_days, ' days"'),
    CAL_FORMAT = '"JD"'
  )
  
  r <- httr::GET(base, query = q)
  txt <- httr::content(r, "text")
  
  # Parse state vectors (you'd need to convert to elements)
  # This is a placeholder - you'd need to implement the conversion
  # or use an existing orbital mechanics library
  
  stop("State vectors method not fully implemented. Use elements method instead.")
}

# Fix 4: Test with a small request first
test_horizons_api <- function() {
  cat("=== TESTING JPL HORIZONS API ===\n")
  
  # Test 1: Small request
  cat("\nTest 1: Small request (5 days in 2000)\n")
  test_txt <- horizons_elements_fixed(
    command = "199",
    start = "2000-01-01",
    stop = "2000-01-06",
    step = "1 day",
    center = "500@10"
  )
  
  cat("API response (first 500 chars):\n")
  cat(substr(test_txt, 1, 500), "\n")
  
  # Test 2: Try to parse
  cat("\nTest 2: Parsing response...\n")
  tryCatch({
    test_df <- parse_horizons_elements_fixed(test_txt)
    cat("Success! Parsed", nrow(test_df), "rows\n")
    cat("Columns:", names(test_df), "\n")
    cat("First row:\n")
    head(test_df, 1)
    return(TRUE)
  }, error = function(e) {
    cat("Parse failed:", e$message, "\n")
    return(FALSE)
  })
}

# Fix 5: Main function with error handling
analyze_mercury_varpi_fixed <- function(start_date = "1900-01-01",
                                       end_date = "2025-01-01",
                                       step_days = 5) {
  
  cat("=== MERCURY PERIHELION PRECESSION ANALYSIS ===\n\n")
  
  # Test API first
  if (!test_horizons_api()) {
    cat("API test failed. Cannot proceed.\n")
    return(NULL)
  }
  
  cat("\n=== FETCHING MAIN DATASET ===\n")
  
  tryCatch({
    # Get the elements
    elements_raw <- get_mercury_elements_robust(start_date, end_date, step_days)
    
    # Process elements
    elements_processed <- elements_raw %>%
      arrange(JDTDB) %>%
      mutate(
        varpi_deg = (OM + W) %% 360,
        varpi_rad = varpi_deg * pi/180
      )
    
    # Unwrap the angles
    unwrap_rad <- function(rad_vec) {
      unwrapped <- rad_vec
      for (i in 2:length(rad_vec)) {
        diff <- rad_vec[i] - rad_vec[i-1]
        if (diff > pi) {
          unwrapped[i:length(unwrapped)] <- unwrapped[i:length(unwrapped)] - 2*pi
        } else if (diff < -pi) {
          unwrapped[i:length(unwrapped)] <- unwrapped[i:length(unwrapped)] + 2*pi
        }
      }
      unwrapped
    }
    
    elements_final <- elements_processed %>%
      mutate(varpi_unwrap = unwrap_rad(varpi_rad))
    
    cat("\n=== ANALYSIS COMPLETE ===\n")
    cat("Total data points:", nrow(elements_final), "\n")
    cat("Time range: JD", min(elements_final$JDTDB), "to", max(elements_final$JDTDB), "\n")
    cat("\nSummary of varpi_deg:\n")
    summary(elements_final$varpi_deg)
    
    # Save results
    write.csv(elements_final, "mercury_horizons_data.csv", row.names = FALSE)
    cat("\nData saved to mercury_horizons_data.csv\n")
    
    return(elements_final)
    
  }, error = function(e) {
    cat("Fatal error in analysis:", e$message, "\n")
    return(NULL)
  })
}

# Run the analysis
# results <- analyze_mercury_varpi_fixed(
#   start_date = "2000-01-01",  # Start with modern data (more reliable)
#   end_date = "2020-01-01",
#   step_days = 30  # Larger step to reduce data size
# )

# If successful, try larger range
# if (!is.null(results) && nrow(results) > 0) {
#   cat("\n=== EXPANDING TO FULL TIME RANGE ===\n")
  
results <- analyze_mercury_varpi_fixed(
    start_date = "1900-01-01",
    end_date = "2026-01-01",
    step_days = 10  # Medium step for full range
)
## === MERCURY PERIHELION PRECESSION ANALYSIS ===
## 
## === TESTING JPL HORIZONS API ===
## 
## Test 1: Small request (5 days in 2000)
## Making JPL Horizons API request...
## Step size format: 1 day 
## API response (first 500 chars):
## API VERSION: 1.2
## API SOURCE: NASA/JPL Horizons API
## 
## 
## 
## *******************************************************************************
## Ephemeris / API_USER Sat Jan 10 18:47:47 2026 Pasadena, USA      / Horizons
## *******************************************************************************
## Target body name: Mercury (199)                   {source: DE441}
## Center body name: Sun (10)                        {source: DE441}
## Center-site name: BODY CENTER
## ************************************************ 
## 
## Test 2: Parsing response...
## Parsed 6 rows of data
## Success! Parsed 6 rows
## Columns: JDTDB CalendarDate EC QR IN OM W Tp N MA TA A AD PR 
## First row:
## 
## === FETCHING MAIN DATASET ===
## Fetching Mercury data from JPL Horizons...
## Large time range detected. Breaking into smaller chunks...
## Fetching chunk 1 : 1900-01-01 to 1925-01-01 
## Making JPL Horizons API request...
## Step size format: 10 days 
## Parsed 914 rows of data
## Fetching chunk 2 : 1925-01-01 to 1950-01-01 
## Making JPL Horizons API request...
## Step size format: 10 days 
## Parsed 914 rows of data
## Fetching chunk 3 : 1950-01-01 to 1975-01-01 
## Making JPL Horizons API request...
## Step size format: 10 days 
## Parsed 914 rows of data
## Fetching chunk 4 : 1975-01-01 to 2000-01-01 
## Making JPL Horizons API request...
## Step size format: 10 days 
## Parsed 914 rows of data
## Fetching chunk 5 : 2000-01-01 to 2025-01-01 
## Making JPL Horizons API request...
## Step size format: 10 days 
## Parsed 914 rows of data
## Fetching chunk 6 : 2025-01-01 to 2026-01-01 
## Making JPL Horizons API request...
## Step size format: 10 days 
## Parsed 37 rows of data
## Successfully retrieved 4607 data points
## 
## === ANALYSIS COMPLETE ===
## Total data points: 4607 
## Time range: JD 2415021 to 2461037 
## 
## Summary of varpi_deg:
## 
## Data saved to mercury_horizons_data.csv
# Save the results
# write.csv(results, "mercury_orbital_elements.csv", row.names = FALSE)
# cat("Results saved to mercury_orbital_elements.csv\n")

# Display a sample of the results
cat("\nFirst few rows of results:\n")
## 
## First few rows of results:
head(results)
##     JDTDB                   CalendarDate        EC        QR       IN       OM
## 1 2415021 A.D. 1900-Jan-01 00:00:00.0000 0.2056246 0.3075009 7.010974 48.45668
## 2 2415031 A.D. 1900-Jan-11 00:00:00.0000 0.2056247 0.3075010 7.010971 48.45668
## 3 2415041 A.D. 1900-Jan-21 00:00:00.0000 0.2056254 0.3075006 7.010968 48.45667
## 4 2415051 A.D. 1900-Jan-31 00:00:00.0000 0.2056265 0.3074999 7.010965 48.45665
## 5 2415061 A.D. 1900-Feb-10 00:00:00.0000 0.2056271 0.3074994 7.010964 48.45662
## 6 2415071 A.D. 1900-Feb-20 00:00:00.0000 0.2056274 0.3074994 7.010964 48.45662
##          W      Tp        N       MA       TA         A        AD       PR
## 1 28.84040 2414995 4.092354 104.3230 125.3114 0.3870977 0.4666945 87.96894
## 2 28.84063 2414995 4.092350 145.2462 156.2591 0.3870979 0.4666948 87.96901
## 3 28.84084 2415083 4.092353 186.1694 184.1556 0.3870978 0.4666949 87.96895
## 4 28.84110 2415083 4.092360 227.0927 212.5681 0.3870974 0.4666948 87.96881
## 5 28.84131 2415083 4.092365 268.0161 245.2845 0.3870970 0.4666947 87.96870
## 6 28.84125 2415083 4.092363 308.9399 287.6255 0.3870972 0.4666950 87.96874
##   varpi_deg varpi_rad varpi_unwrap
## 1  77.29708  1.349089     1.349089
## 2  77.29731  1.349093     1.349093
## 3  77.29751  1.349096     1.349096
## 4  77.29774  1.349100     1.349100
## 5  77.29793  1.349103     1.349103
## 6  77.29787  1.349102     1.349102

4.4.4 Calculate Mercury’s Rate of Precession

Next, we calculate the rate of precession, using for example linear regression.

if (nrow(results) > 1) {
  # Convert JD to years for regression
  results$year <- 1968 + (results$JDTDB - 2440000) / 365.25

  # Linear regression of varpi_deg vs year
  lm_fit <- lm(varpi_deg ~ year, data = results)
  precession_rate <- coef(lm_fit)[2]  # degrees per year

  cat("\n=== PRECESSION RATE ESTIMATE ===\n")
  cat(sprintf("Linear precession rate: %.4f degrees/year\n", precession_rate))
  cat(sprintf("                          %.2f arcseconds/century\n", precession_rate * 3600 * 100))

  # Mercury's known perihelion precession is about 574 arcseconds/century
  # (after accounting for perturbations from other planets)
  known_rate <- 574  # arcseconds/century
  cat(sprintf("\nKnown relativistic contribution: ~43 arcseconds/century\n"))
  cat(sprintf("Total observed precession: ~%d arcseconds/century\n", known_rate))
}
## 
## === PRECESSION RATE ESTIMATE ===
## Linear precession rate: 0.0016 degrees/year
##                           571.76 arcseconds/century
## 
## Known relativistic contribution: ~43 arcseconds/century
## Total observed precession: ~574 arcseconds/century
# Replace the get_mercury_elements function with this working version:

# Function to fetch Mercury orbital elements from JPL Horizons
get_mercury_elements <- function(start_date = "1900-01-01",
                                 end_date = "2025-01-01",
                                 step_days = 5) {
  
  cat("Fetching Mercury orbital elements from", start_date, "to", end_date, "\n")
  cat("Step size:", step_days, "days\n")
  
  # Use the working horizons_elements function
  res_txt <- horizons_elements(
    command = "199",
    start = start_date,
    stop = end_date,
    step = paste(step_days, "d"),
    center = "500@10",
    ref_plane = "ECLIPTIC",
    out_units = "AU-D",
    cal_format = "JD"
  )
  
  # Parse the results
  elements <- parse_horizons_elements_csv(res_txt)
  
  cat("Retrieved", nrow(elements), "data points\n")
  
  return(elements)
}

# Also update the get_elements_alternative function:
get_elements_alternative <- function(start_date = "1900-01-01",
                                     end_date = "2025-01-01",
                                     step_days = 5) {
  
  cat("Trying alternative method...\n")
  
  # Use state vectors instead
  res_txt <- horizons_vectors(
    command = "199",
    start = start_date,
    stop = end_date,
    step = paste(step_days, "d"),
    center = "500@10"
  )
  
  # Parse state vectors
  vectors <- parse_horizons_vectors_csv(res_txt)
  
  cat("Retrieved state vectors with", nrow(vectors), "rows\n")
  
  # Convert state vectors to orbital elements
  elements_df <- state_vectors_to_elements(vectors)
  
  return(elements_df)
}

# Add a function to parse state vectors CSV
parse_horizons_vectors_csv <- function(result_text) {
  lines <- strsplit(result_text, "\n", fixed = TRUE)[[1]]
  
  # Find data block
  i0 <- which(grepl("\\$\\$SOE", lines))[1] + 1
  i1 <- which(grepl("\\$\\$EOE", lines))[1] - 1
  
  if (is.na(i0) || is.na(i1)) {
    cat("First 50 lines of response:\n")
    cat(lines[1:min(50, length(lines))], sep = "\n")
    stop("Could not find SOE/EOE markers in response.")
  }
  
  block <- lines[i0:i1]
  block <- block[nzchar(trimws(block))]
  
  # Read CSV
  df <- read.csv(text = paste(block, collapse = "\n"),
                 stringsAsFactors = FALSE,
                 strip.white = TRUE,
                 comment.char = "",
                 header = TRUE)
  
  # Clean column names
  names(df) <- gsub("\\s+|\\.", "", names(df))
  
  # Ensure we have required columns
  required_cols <- c("JDTDB", "X", "Y", "Z", "VX", "VY", "VZ")
  if (!all(required_cols %in% names(df))) {
    cat("Available columns:", names(df), "\n")
    stop("Missing required columns in state vectors data.")
  }
  
  # Convert to numeric
  df$JDTDB <- as.numeric(df$JDTDB)
  df$X <- as.numeric(df$X)
  df$Y <- as.numeric(df$Y)
  df$Z <- as.numeric(df$Z)
  df$VX <- as.numeric(df$VX)
  df$VY <- as.numeric(df$VY)
  df$VZ <- as.numeric(df$VZ)
  
  df
}

# Function to convert state vectors to orbital elements
state_vectors_to_elements <- function(vectors_df) {
  results <- list()
  
  # Constants
  GM <- 2.959122074e-4  # GM for Sun in AU^3/day^2
  
  for (i in 1:nrow(vectors_df)) {
    # Position and velocity vectors
    r <- c(vectors_df$X[i], vectors_df$Y[i], vectors_df$Z[i])
    v <- c(vectors_df$VX[i], vectors_df$VY[i], vectors_df$VZ[i])
    
    # Calculate orbital elements
    elem <- state_vector_to_elements(r, v, GM)
    
    # Add to results
    results[[i]] <- data.frame(
      JDTDB = vectors_df$JDTDB[i],
      OM = elem$OM,
      W = elem$W
    )
  }
  
  bind_rows(results)
}

4.4.5 Rolling-window estimate of perihelion precession rate (arcsec/century)

We regress unwrapped \(\varpi(t)\) on time within each window and convert to arcsec/century.

Note that time is measures in JD (Julian Date) on the \(x\)-axis.

### Rolling-window estimate of perihelion precession rate (arcsec/century)
### IMPROVED VERSION with better time handling and visualization

# Load required libraries if not already loaded
library(dplyr)
library(ggplot2)
library(plotly)
library(lubridate)

# Convert JD to Date for better readability
jd_to_date <- function(jd) {
  # Convert Julian Date to R Date
  # JD 0 = 12:00 January 1, 4713 BC (proleptic Julian calendar)
  # JD 2440587.5 = 12:00 January 1, 1970 (Unix epoch)
  return(as.Date(jd - 2440587.5, origin = "1970-01-01"))
}

jd_to_year <- function(jd) {
  # Convert JD to decimal year (e.g., 2000.5 = mid-year 2000)
  date <- jd_to_date(jd)
  year <- year(date)
  # Fraction of year
  year_start <- as.Date(paste0(year, "-01-01"))
  year_end <- as.Date(paste0(year + 1, "-01-01"))
  fraction <- as.numeric(difftime(date, year_start, units = "days")) / 
    as.numeric(difftime(year_end, year_start, units = "days"))
  return(year + fraction)
}

# If you want more precise conversion (accounting for leap years):
jd_to_decimal_year <- function(jd) {
  date <- jd_to_date(jd)
  year <- lubridate::year(date)
  start_of_year <- as.Date(paste0(year, "-01-01"))
  end_of_year <- as.Date(paste0(year + 1, "-01-01"))
  days_in_year <- as.numeric(difftime(end_of_year, start_of_year))
  day_of_year <- as.numeric(difftime(date, start_of_year, units = "days"))
  return(year + day_of_year / days_in_year)
}

rolling_slope_improved <- function(JD, y, window_years = 10, step_years = 1) {
  day_per_year <- 365.25
  w_days  <- window_years * day_per_year
  step_d  <- step_years   * day_per_year
  
  JD_min <- min(JD); JD_max <- max(JD)
  centers <- seq(JD_min + w_days/2, JD_max - w_days/2, by = step_d)
  
  out <- lapply(centers, function(cj) {
    idx <- which(JD >= (cj - w_days/2) & JD <= (cj + w_days/2))
    if (length(idx) < 30) return(NULL)
    x <- JD[idx] - cj
    fit <- lm(y[idx] ~ x)
    b1 <- coef(fit)[2]                   # rad/day
    se <- summary(fit)$coefficients[2,2]
    
    # Calculate date/year for the center
    center_date <- jd_to_date(cj)
    center_year <- jd_to_decimal_year(cj)
    
    tibble(
      JD_center = cj,
      date_center = center_date,
      year_center = center_year,
      slope_rad_day = b1,
      se_rad_day = se,
      n = length(idx)
    )
  })
  
  bind_rows(out) %>%
    mutate(
      slope_arcsec_cy = arcsec_per_century(slope_rad_day),
      se_arcsec_cy    = arcsec_per_century(se_rad_day)
    )
}

# Assuming 'results' contains your data with JDTDB and varpi_unwrap
# Let's create the rolling slope analysis with date conversion
sl <- rolling_slope_improved(results$JDTDB, results$varpi_unwrap, 
                             window_years = 10, step_years = 1)

# 1. Basic ggplot with Date
p1 <- ggplot(sl, aes(x = date_center, y = slope_arcsec_cy)) +
  geom_ribbon(aes(ymin = slope_arcsec_cy - 2*se_arcsec_cy,
                  ymax = slope_arcsec_cy + 2*se_arcsec_cy),
              alpha = 0.2, fill = "blue") +
  geom_line(linewidth = 0.7, color = "blue") +
  geom_point(size = 0.5, alpha = 0.5) +
  labs(title = "Mercury Perihelion Precession Rate",
       subtitle = "10-year rolling window, 1-year step (95% confidence band)",
       x = "Date (Window Center)",
       y = "Precession Rate (arcsec/century)",
       caption = paste("Data: JPL Horizons API |", 
                       "Time range:", min(sl$date_center), "to", max(sl$date_center))) +
  theme_minimal() +
  theme(
    plot.title = element_text(hjust = 0.5, face = "bold"),
    plot.subtitle = element_text(hjust = 0.5)
  )

p1

# 2. Interactive plot with plotly
p2 <- plot_ly(sl, x = ~date_center, y = ~slope_arcsec_cy,
              type = 'scatter', mode = 'lines+markers',
              name = 'Precession Rate',
              line = list(color = 'blue', width = 2),
              marker = list(size = 3, opacity = 0.5),
              hoverinfo = 'text',
              text = ~paste(
                sprintf("Date: %s", date_center),
                sprintf("Year: %.2f", year_center),
                sprintf("Rate: %.2f arcsec/cy", slope_arcsec_cy),
                sprintf("±%.2f (2σ)", 2*se_arcsec_cy),
                sprintf("N points: %d", n),
                sep = "<br>"
              )) %>%
  add_ribbons(x = ~date_center,
              ymin = ~slope_arcsec_cy - 2*se_arcsec_cy,
              ymax = ~slope_arcsec_cy + 2*se_arcsec_cy,
              name = '95% CI',
              fillcolor = 'rgba(0,100,255,0.2)',
              line = list(color = 'transparent')) %>%
  layout(
    title = list(
      text = "Mercury Perihelion Precession Rate (Interactive)",
      x = 0.5
    ),
    xaxis = list(
      title = "Date (Window Center)",
      tickformat = "%Y-%m",
      rangeslider = list(visible = TRUE)  # Add range slider for zooming
    ),
    yaxis = list(
      title = "Precession Rate (arcsec/century)",
      tickformat = ".1f"
    ),
    hovermode = 'x unified',
    showlegend = TRUE
  )

p2
# 3. Additional analysis: Long-term trend
# Let's also look at the overall linear trend
if (nrow(sl) > 0) {
  # Fit linear trend to the precession rates over time
  trend_fit <- lm(slope_arcsec_cy ~ year_center, data = sl)
  trend_summary <- summary(trend_fit)
  
  cat("\n=== LONG-TERM TREND ANALYSIS ===\n")
  cat("Linear trend of precession rate over time:\n")
  cat(sprintf("Slope: %.4f arcsec/cy per year\n", coef(trend_fit)[2]))
  cat(sprintf("P-value: %.4f\n", trend_summary$coefficients[2, 4]))
  cat(sprintf("R-squared: %.4f\n", trend_summary$r.squared))
  
  # Add trend line to plot
  p3 <- ggplot(sl, aes(x = year_center, y = slope_arcsec_cy)) +
    geom_point(alpha = 0.5) +
    geom_smooth(method = "lm", se = TRUE, color = "red", fill = "pink") +
    labs(title = "Long-term Trend of Mercury's Precession Rate",
         x = "Year (Decimal)",
         y = "Precession Rate (arcsec/century)",
         caption = sprintf("Trend: %.2f ± %.2f arcsec/cy per century",
                           coef(trend_fit)[2] * 100,
                           2 * summary(trend_fit)$coefficients[2, 2] * 100)) +
    theme_minimal()
  
  p3
  
  # 4. Distribution of rates
  # p4 <- ggplot(sl, aes(x = slope_arcsec_cy)) +
  #   geom_histogram(aes(y = ..density..), bins = 30, fill = "lightblue", 
  #                  color = "black", alpha = 0.7) +
  #   geom_density(color = "blue", linewidth = 1) +
  #   geom_vline(xintercept = mean(sl$slope_arcsec_cy), 
  #              color = "red", linetype = "dashed", linewidth = 1) +
  #   labs(title = "Distribution of Precession Rates",
  #        subtitle = paste("Mean:", round(mean(sl$slope_arcsec_cy), 2), 
  #                         "arcsec/cy, SD:", round(sd(sl$slope_arcsec_cy), 2)),
  #        x = "Precession Rate (arcsec/century)",
  #        y = "Density") +
  #   theme_minimal()
  # 
  # p4
  # Precompute density curve
  dens <- density(sl$slope_arcsec_cy, na.rm = TRUE)
  
  # Compute summary stats
  mean_slope <- mean(sl$slope_arcsec_cy, na.rm = TRUE)
  sd_slope   <- sd(sl$slope_arcsec_cy, na.rm = TRUE)
  
  # Build interactive plot
  p4_interactive <- plot_ly() %>%
    
    # Histogram (density-normalized)
    add_histogram(
      x = ~sl$slope_arcsec_cy,
      histnorm = "probability density",
      nbinsx = 30,
      marker = list(color = "lightblue", line = list(color = "black", width = 1)),
      opacity = 0.7,
      name = "Histogram"
    ) %>%
    
    # Kernel density estimate
    add_trace(
      x = dens$x,
      y = dens$y,
      type = "scatter",
      mode = "lines",
      line = list(color = "blue", width = 1),
      name = "Density"
    ) %>%
    
    # Layout with title, axes, and reference line
    layout(
      title = list(
        text = "Distribution of Precession Rates",
        x = 0.5,
        font = list(size = 16)
      ),
      xaxis = list(
        title = "Precession Rate (arcsec/century)"
      ),
      yaxis = list(
        title = "Density"
      ),
      shapes = list(
        list(
          type = "line",
          x0 = mean_slope,
          x1 = mean_slope,
          y0 = 0,
          y1 = 1,
          yref = "paper",
          line = list(color = "red", dash = "dash", width = 1)
        )
      ),
      annotations = list(
        list(
          x = mean_slope,
          y = 1,
          yref = "paper",
          text = sprintf("Mean: %.2f", mean_slope),
          showarrow = FALSE,
          font = list(color = "red"),
          xanchor = "center",
          yanchor = "bottom"
        )
      ),
      showlegend = TRUE,
      legend = list(x = 0.02, y = 0.98)
    )

  p4_interactive
  
  # 5. Time series decomposition
  # Convert to time series object
  if (nrow(sl) >= 24) {  # Need enough points for decomposition
    ts_data <- ts(sl$slope_arcsec_cy, 
                  start = min(sl$year_center),
                  frequency = 1)  # Annual data
    
    # Simple moving average for smoother visualization
    sl <- sl %>%
      mutate(
        ma_5 = zoo::rollmean(slope_arcsec_cy, 5, fill = NA, align = "center"),
        ma_10 = zoo::rollmean(slope_arcsec_cy, 10, fill = NA, align = "center")
      )
    
    p5 <- ggplot(sl, aes(x = date_center)) +
      geom_line(aes(y = slope_arcsec_cy, color = "Raw"), alpha = 0.3) +
      geom_line(aes(y = ma_5, color = "5-year MA"), linewidth = 1) +
      geom_line(aes(y = ma_10, color = "10-year MA"), linewidth = 1.2) +
      scale_color_manual(
        values = c("Raw" = "gray", "5-year MA" = "blue", "10-year MA" = "red")
      ) +
      labs(
        title = "Precession Rate with Moving Averages",
        x = "Date",
        y = "Precession Rate (arcsec/century)",
        color = "Series"
      ) +
      theme_minimal() +
      theme(legend.position = "bottom")
    
    p5
  }
}
## 
## === LONG-TERM TREND ANALYSIS ===
## Linear trend of precession rate over time:
## Slope: 0.0791 arcsec/cy per year
## P-value: 0.6248
## R-squared: 0.0021

# Save the rolling slope data for later use
write.csv(sl, "mercury_precession_rates.csv", row.names = FALSE)
cat("\nRolling slope data saved to: mercury_precession_rates.csv\n")
## 
## Rolling slope data saved to: mercury_precession_rates.csv

4.4.6 Define baselines (Newtonian vs GR) and compute the anomaly series

To compare with the classical Mercury anomaly framing, we subtract the dominant non-GR terms:

  • planetary perturbations \(\approx 532.3035\) arcsec/cy
  • solar oblateness \(J_2\) term \(\approx 0.0286\) arcsec/cy

and compare to GR \(\approx 42.9799\) arcsec/cy.

### Define baselines (Newtonian vs GR) and compute the anomaly series

# Load required libraries if not already loaded
library(plotly)
library(dplyr)
library(ggplot2)

# Reference values (from various sources)
A_planets <- 532.3035     # Planetary perturbations (arcsec/cy)
A_J2 <- 0.0286            # Solar oblateness J2 term (arcsec/cy)
A_GR <- 42.9799           # General Relativity prediction (arcsec/cy)
A_newton_nonGR <- A_planets + A_J2  # Total Newtonian non-GR

# Modern more precise values (if available)
A_GR_precise <- 42.9784   # More precise GR prediction
A_GR_error <- 0.0009      # Uncertainty in GR prediction

# Calculate anomaly
sl <- sl %>%
  mutate(
    anomaly_arcsec_cy = slope_arcsec_cy - A_newton_nonGR,
    anomaly_rel_to_GR = anomaly_arcsec_cy - A_GR,
    anomaly_percent_of_GR = (anomaly_arcsec_cy / A_GR) * 100,
    anomaly_sigma = ifelse(se_arcsec_cy > 0, 
                          abs(anomaly_arcsec_cy - A_GR) / se_arcsec_cy,
                          NA)
  )

# Summary statistics
cat("\n=== ANOMALY ANALYSIS SUMMARY ===\n")
## 
## === ANOMALY ANALYSIS SUMMARY ===
cat(sprintf("Newtonian baseline (planetary + J2): %.4f arcsec/cy\n", A_newton_nonGR))
## Newtonian baseline (planetary + J2): 532.3321 arcsec/cy
cat(sprintf("GR prediction: %.4f ± %.4f arcsec/cy\n", A_GR_precise, A_GR_error))
## GR prediction: 42.9784 ± 0.0009 arcsec/cy
cat(sprintf("\nAnomaly statistics (observed - Newtonian):\n"))
## 
## Anomaly statistics (observed - Newtonian):
cat(sprintf("Mean: %.4f arcsec/cy\n", mean(sl$anomaly_arcsec_cy, na.rm = TRUE)))
## Mean: 38.8214 arcsec/cy
cat(sprintf("Std Dev: %.4f arcsec/cy\n", sd(sl$anomaly_arcsec_cy, na.rm = TRUE)))
## Std Dev: 57.9517 arcsec/cy
cat(sprintf("Range: [%.4f, %.4f] arcsec/cy\n", 
            min(sl$anomaly_arcsec_cy, na.rm = TRUE), 
            max(sl$anomaly_arcsec_cy, na.rm = TRUE)))
## Range: [-102.5374, 167.5533] arcsec/cy
cat(sprintf("\nDeviation from GR (mean): %.4f arcsec/cy\n", 
            mean(sl$anomaly_arcsec_cy, na.rm = TRUE) - A_GR))
## 
## Deviation from GR (mean): -4.1585 arcsec/cy
cat(sprintf("Percent of GR prediction (mean): %.2f%%\n", 
            mean(sl$anomaly_percent_of_GR, na.rm = TRUE)))
## Percent of GR prediction (mean): 90.32%
# 1. Enhanced static plot
p_static <- ggplot(sl, aes(x = date_center)) +
  # Main anomaly line
  geom_line(aes(y = anomaly_arcsec_cy), color = "darkblue", linewidth = 1, alpha = 0.8) +
  
  # Confidence band (2 sigma)
  geom_ribbon(aes(ymin = anomaly_arcsec_cy - 2*se_arcsec_cy,
                  ymax = anomaly_arcsec_cy + 2*se_arcsec_cy),
              alpha = 0.2, fill = "blue") +
  
  # Reference lines
  geom_hline(yintercept = A_GR, linetype = "dashed", color = "red", linewidth = 1) +
  geom_hline(yintercept = A_GR_precise, linetype = "dotted", color = "darkred", linewidth = 0.8) +
  geom_hline(yintercept = 0, linetype = "solid", color = "gray50", linewidth = 0.5) +
  
  # GR uncertainty band
  annotate("rect", 
           xmin = min(sl$date_center), 
           xmax = max(sl$date_center),
           ymin = A_GR_precise - A_GR_error,
           ymax = A_GR_precise + A_GR_error,
           alpha = 0.1, fill = "red") +
  
  # Labels for reference lines
  annotate("text", 
           x = max(sl$date_center), 
           y = A_GR + 1, 
           label = "GR prediction", 
           color = "red", hjust = 1, vjust = 0) +
  annotate("text",
           x = max(sl$date_center),
           y = 0 - 1,
           label = "Zero anomaly",
           color = "gray50", hjust = 1, vjust = 1) +
  
  # Plot aesthetics
  labs(
    title = "Mercury Perihelion Precession Anomaly",
    subtitle = paste("Anomaly = Total Precession - (Planetary + J₂)",
                     "\nDashed red: GR = 42.9799 arcsec/cy",
                     "Dotted red: Modern GR = 42.9784 ± 0.0009 arcsec/cy"),
    x = "Date (Window Center)",
    y = "Anomaly (arcsec/century)",
    caption = paste("Data: JPL Horizons API |",
                    "Rolling window: 10 years |",
                    sprintf("Mean anomaly: %.2f ± %.2f arcsec/cy", 
                            mean(sl$anomaly_arcsec_cy), 
                            sd(sl$anomaly_arcsec_cy)))
  ) +
  theme_minimal() +
  theme(
    plot.title = element_text(hjust = 0.5, face = "bold", size = 14),
    plot.subtitle = element_text(hjust = 0.5, size = 10),
    panel.grid.minor = element_blank()
  )

p_static

# 2. Interactive plotly plot
p_interactive <- plot_ly(sl, x = ~date_center) %>%
  # Add anomaly line
  add_trace(y = ~anomaly_arcsec_cy,
            type = 'scatter',
            mode = 'lines+markers',
            name = 'Anomaly',
            line = list(color = 'blue', width = 2),
            marker = list(size = 4, opacity = 0.3),
            hoverinfo = 'text',
            text = ~paste(
              sprintf("Date: %s", date_center),
              sprintf("Year: %.2f", year_center),
              sprintf("Anomaly: %.3f arcsec/cy", anomaly_arcsec_cy),
              sprintf("±%.3f (2σ)", 2*se_arcsec_cy),
              sprintf("Deviation from GR: %.3f", anomaly_rel_to_GR),
              sprintf("%.1f%% of GR", anomaly_percent_of_GR),
              sep = "<br>"
            )) %>%
  
  # Add confidence band
  add_ribbons(ymin = ~anomaly_arcsec_cy - 2*se_arcsec_cy,
              ymax = ~anomaly_arcsec_cy + 2*se_arcsec_cy,
              name = '95% CI',
              fillcolor = 'rgba(0,100,255,0.2)',
              line = list(color = 'transparent')) %>%
  
  # Add GR reference line
  add_trace(y = rep(A_GR, nrow(sl)),
            type = 'scatter',
            mode = 'lines',
            name = 'GR Prediction',
            line = list(color = 'red', dash = 'dash', width = 2),
            hoverinfo = 'text',
            text = ~paste("GR prediction:", A_GR, "arcsec/cy")) %>%
  
  # Add zero line
  add_trace(y = rep(0, nrow(sl)),
            type = 'scatter',
            mode = 'lines',
            name = 'Zero Anomaly',
            line = list(color = 'gray', dash = 'solid', width = 1),
            hoverinfo = 'none') %>%
  
  # Layout configuration
  layout(
    title = list(
      text = "Mercury Perihelion Anomaly (Interactive)",
      x = 0.5,
      font = list(size = 16)
    ),
    xaxis = list(
      title = "Date (Window Center)",
      tickformat = "%Y-%m",
      rangeslider = list(visible = TRUE),
      gridcolor = 'lightgray'
    ),
    yaxis = list(
      title = "Anomaly (arcsec/century)",
      gridcolor = 'lightgray',
      zeroline = TRUE,
      zerolinecolor = 'gray',
      zerolinewidth = 1
    ),
    hovermode = 'x unified',
    showlegend = TRUE,
    legend = list(x = 0.02, y = 0.98, bgcolor = 'rgba(255,255,255,0.8)'),
    plot_bgcolor = 'white',
    paper_bgcolor = 'white'
  )

# Add horizontal GR uncertainty band as a shape
p_interactive <- p_interactive %>%
  layout(
    shapes = list(
      list(
        type = "rect",
        x0 = min(sl$date_center),
        x1 = max(sl$date_center),
        y0 = A_GR_precise - A_GR_error,
        y1 = A_GR_precise + A_GR_error,
        fillcolor = "rgba(255,0,0,0.1)",
        line = list(width = 0),
        layer = "below"
      )
    ),
    annotations = list(
      list(
        x = max(sl$date_center),
        y = A_GR_precise + A_GR_error + 0.5,
        text = "Modern GR with uncertainty",
        showarrow = FALSE,
        xref = "x",
        yref = "y",
        xanchor = "right",
        font = list(size = 10, color = "darkred")
      )
    )
  )

p_interactive
# 3. Histogram of anomalies
p_hist <- plot_ly(sl, x = ~anomaly_arcsec_cy,
                  type = 'histogram',
                  histnorm = 'probability density',
                  name = 'Anomaly Distribution',
                  nbinsx = 30,
                  marker = list(color = 'blue',
                                line = list(color = 'black', width = 0.5))) %>%
  layout(
    title = "Distribution of Mercury Anomaly Values",
    xaxis = list(title = "Anomaly (arcsec/century)"),
    yaxis = list(title = "Density"),
    showlegend = FALSE,
    shapes = list(
      # Vertical line for GR
      list(type = "line",
           x0 = A_GR, x1 = A_GR,
           y0 = 0, y1 = 1,
           yref = "paper",  # makes y0/y1 relative to the whole plot area (0 to 1)
           line = list(color = "red", dash = "dash", width = 2)),
      # Vertical line for mean
      list(type = "line",
           x0 = mean(sl$anomaly_arcsec_cy, na.rm = TRUE),
           x1 = mean(sl$anomaly_arcsec_cy, na.rm = TRUE),
           y0 = 0, y1 = 1,
           yref = "paper",
           line = list(color = "green", dash = "dot", width = 2))
    ),
    annotations = list(
      list(x = A_GR, y = 1,
           yref = "paper",
           text = "GR",
           showarrow = FALSE,
           font = list(color = "red"),
           xanchor = "center", yanchor = "bottom"),
      list(x = mean(sl$anomaly_arcsec_cy, na.rm = TRUE), y = 1,
           yref = "paper",
           text = "Mean",
           showarrow = FALSE,
           font = list(color = "green"),
           xanchor = "center", yanchor = "bottom")
    )
  )

p_hist
# 4. Time series of deviation from GR
p_deviation <- plot_ly(sl, x = ~date_center) %>%
  add_trace(y = ~anomaly_rel_to_GR,
            type = 'scatter',
            mode = 'lines+markers',
            name = 'Deviation from GR',
            line = list(color = 'purple', width = 2),
            marker = list(size = 4, opacity = 0.3),
            hoverinfo = 'text',
            text = ~paste(
              sprintf("Date: %s", date_center),
              sprintf("Deviation: %.3f arcsec/cy", anomaly_rel_to_GR),
              sprintf("%.1fσ from GR", anomaly_sigma),
              sep = "<br>"
            )) %>%
  layout(
    title = "Deviation from General Relativity Prediction",
    xaxis = list(title = "Date", rangeslider = list(visible = TRUE)),
    yaxis = list(title = "Deviation from GR (arcsec/century)"),
    hovermode = 'x unified',
    shapes = list(
      list(
        type = "line",
        x0 = 0, x1 = 1,
        xref = "paper",       # spans full width
        y0 = 0, y1 = 0,
        line = list(color = "gray", dash = "solid", width = 1)
      )
    ),
    annotations = list(
      list(
        x = 1, y = 0,
        xref = "paper", yref = "y",
        xanchor = "right", yanchor = "bottom",
        text = "GR exact match",
        showarrow = FALSE,
        font = list(color = "gray")
      )
    )
  )

p_deviation
# 5. Statistical test against GR
cat("\n=== STATISTICAL TEST AGAINST GR ===\n")
## 
## === STATISTICAL TEST AGAINST GR ===
# One-sample t-test against GR value
if (sum(!is.na(sl$anomaly_arcsec_cy)) > 1) {
  t_test_result <- t.test(sl$anomaly_arcsec_cy, mu = A_GR)
  cat("One-sample t-test (H₀: anomaly = GR prediction)\n")
  cat(sprintf("t = %.3f, df = %d\n", t_test_result$statistic, t_test_result$parameter))
  cat(sprintf("p-value = %.5f\n", t_test_result$p.value))
  cat(sprintf("95%% Confidence Interval: [%.4f, %.4f]\n", 
              t_test_result$conf.int[1], t_test_result$conf.int[2]))
  
  # Check if GR value is within confidence interval
  gr_in_ci <- t_test_result$conf.int[1] <= A_GR && A_GR <= t_test_result$conf.int[2]
  cat(sprintf("GR value within 95%% CI: %s\n", ifelse(gr_in_ci, "YES", "NO")))
}
## One-sample t-test (H₀: anomaly = GR prediction)
## t = -0.773, df = 115
## p-value = 0.44119
## 95% Confidence Interval: [28.1633, 49.4795]
## GR value within 95% CI: YES
# 6. Save anomaly data
# 6. Save anomaly data
sl_anomaly <- sl %>%
  dplyr::select(date_center, year_center, JD_center,
                slope_arcsec_cy, se_arcsec_cy,
                anomaly_arcsec_cy, anomaly_rel_to_GR,
                anomaly_percent_of_GR, anomaly_sigma)

# write.csv(sl_anomaly, "mercury_anomaly_analysis.csv", row.names = FALSE)
# cat("\nAnomaly analysis data saved to: mercury_anomaly_analysis.csv\n")

# 7. Create summary table for reporting
summary_table <- data.frame(
  Statistic = c("Newtonian baseline (planetary + J₂)",
                "GR prediction",
                "Mean observed anomaly",
                "Std. deviation of anomaly",
                "Deviation from GR (mean)",
                "Percent of GR prediction",
                "t-test p-value"),
  Value = c(
    sprintf("%.4f arcsec/cy", A_newton_nonGR),
    sprintf("%.4f ± %.4f arcsec/cy", A_GR_precise, A_GR_error),
    sprintf("%.4f ± %.4f arcsec/cy", 
            mean(sl$anomaly_arcsec_cy, na.rm = TRUE),
            sd(sl$anomaly_arcsec_cy, na.rm = TRUE)),
    sprintf("%.4f arcsec/cy", sd(sl$anomaly_arcsec_cy, na.rm = TRUE)),
    sprintf("%.4f arcsec/cy", 
            mean(sl$anomaly_arcsec_cy, na.rm = TRUE) - A_GR),
    sprintf("%.2f%%", mean(sl$anomaly_percent_of_GR, na.rm = TRUE)),
    ifelse(exists("t_test_result"), 
           sprintf("%.5f", t_test_result$p.value), "N/A")
  )
)

summary_table
##                             Statistic                       Value
## 1 Newtonian baseline (planetary + J₂)          532.3321 arcsec/cy
## 2                       GR prediction  42.9784 ± 0.0009 arcsec/cy
## 3               Mean observed anomaly 38.8214 ± 57.9517 arcsec/cy
## 4           Std. deviation of anomaly           57.9517 arcsec/cy
## 5            Deviation from GR (mean)           -4.1585 arcsec/cy
## 6            Percent of GR prediction                      90.32%
## 7                      t-test p-value                     0.44119

4.4.7 Construct repeated measurements at each time index (bootstrap within each window)

As KPT requires repeated observations per time index \(t_k\), we generate replicates by bootstrapping points inside each rolling window, refitting the slope each time, and converting to anomaly units.

bootstrap_slopes <- function(JD, y, centers_JD,
                             window_years = 10,
                             B = 200, seed = 7,
                             baseline_nonGR = A_newton_nonGR) {
  set.seed(seed)
  day_per_year <- 365.25
  w_days <- window_years * day_per_year

  K <- length(centers_JD)
  Y <- matrix(NA_real_, nrow = B, ncol = K)

  for (k in seq_len(K)) {
    cj  <- centers_JD[k]
    idx <- which(JD >= (cj - w_days/2) & JD <= (cj + w_days/2))
    if (length(idx) < 30) next

    x0 <- JD[idx] - cj
    y0 <- y[idx]
    n  <- length(idx)

    for (b in seq_len(B)) {
      ii <- sample.int(n, size = n, replace = TRUE)
      fit <- lm(y0[ii] ~ x0[ii])
      rate_total <- arcsec_per_century(coef(fit)[2])     # arcsec/cy (total)
      Y[b, k] <- rate_total - baseline_nonGR             # anomaly replicate
    }
  }
  list(Y = Y, t = centers_JD)
}

boot <- bootstrap_slopes(results$JDTDB, results$varpi_unwrap,
                         centers_JD = sl$JD_center,
                         window_years = 10, B = 200, seed = 7)

Y <- boot$Y          # [B x K] anomaly replicates (real-data-derived)
t_center <- boot$t

cat("Replicate matrix Y:", nrow(Y), "x", ncol(Y), "\n")
## Replicate matrix Y: 200 x 116
summary(as.vector(Y))
##     Min.  1st Qu.   Median     Mean  3rd Qu.     Max. 
## -137.276   -6.736   43.401   38.768   81.646  197.152

4.4.8 KPT Model: Comprehensive Physics-Constrained GEM Algorithm

The key insight of Kime-Phase Theory (KPT) is that repeated measurements at “the same” real time \(t\) correspond to different kime-phases \(\theta \in [-\pi, \pi]\). The complex time (kime) is \(\kappa = t \cdot e^{i\theta}\).

For the Mercury perihelion problem, we model observations as

\[Y_{i,k} = S_k(\Theta_{i,k}) + \varepsilon_{i,k}\]

where \(Y_{i,k}\) is the \(i\)-th replicate measurement in window \(k\), \(S_k(\theta)\) is the kime-surface at window \(k\), evaluated at kime-phase \(\theta\), \(\Theta_{i,k} \sim \varphi_k(\theta)\) is the kime-phase distribution for window \(k\), and \(\varepsilon_{i,k} \sim \mathcal{N}(0, \sigma^2)\) is the measurement noise.

Critical Physical Constraint: The expectation of the kime-surface over the kime-phase distribution must equal (or be close to) the GR prediction: \[\mathbb{E}_{\varphi_k}[S_k(\theta)] = \int_{-\pi}^{\pi} S_k(\theta) \varphi_k(\theta) d\theta \approx A_{\text{GR}}\]

This ensures that KPT refines our uncertainty quantification rather than replacing established physics.

To parameterize the KPT model, we use a constrained Fourier expansion for the kime-surface \[S_k(\theta) = \mu_0 + \delta_k + \sum_{j=1}^{J} \left[ a_{j,k} \cos(j\theta) + b_{j,k} \sin(j\theta) \right]\]

where \(\mu_0 = A_{\text{GR}}\) is the GR prediction (fixed anchor), \(\delta_k\) is a small time-varying offset with regularization \(\|\delta\|^2 \to 0\), and \(a_{j,k}, b_{j,k}\): Fourier coefficients capturing kime-phase-dependent variation.

The physical constraint is enforced via \[\mathbb{E}_{\varphi_k}[S_k(\theta)] = \mu_0 + \delta_k + \sum_j [a_{j,k} \mathbb{E}[\cos(j\theta)] + b_{j,k} \mathbb{E}[\sin(j\theta)]].\]

For a symmetric kime-phase distribution centered at \(\theta=0\): \(\mathbb{E}[\sin(j\theta)] = 0\).

# ==============================================================================
# COMPREHENSIVE KPT-GEM ALGORITHM
# Physics-constrained Generalized Expectation-Maximization for Kime-Phase Theory
# ==============================================================================

#' Theta grid for kime-phase discretization
#' @param L Number of grid points
theta_grid <- function(L) seq(-pi, pi, length.out = L + 1L)[1L:L]

#' Circular smoothing via FFT convolution with von Mises kernel
#' @param p Probability vector on grid
#' @param kappa Concentration parameter (higher = less smoothing)
smooth_circular <- function(p, kappa = 25) {
  L <- length(p)
  th <- theta_grid(L)
  ker <- exp(kappa * cos(th))
  ker <- ker / sum(ker)
  P <- fft(p)
  K <- fft(ker)
  Re(fft(P * K, inverse = TRUE)) / L
}

#' Comprehensive Physics-Constrained KPT-GEM
#' 
#' This implementation differs from the naive version by:
#' 1. Anchoring the mean to the GR prediction
#' 2. Using regularization to prevent overfitting
#' 3. Employing a shared (pooled) kime-phase distribution across windows
#' 4. Properly handling the constraint that E[S(θ)] ≈ A_GR
#' 
#' @param Y Matrix of observations (replicates x windows)
#' @param t Time centers for each window
#' @param A_GR The GR prediction for the anomaly (arcsec/century)
#' @param L_theta Number of theta grid points
#' @param n_harmonics Number of Fourier harmonics
#' @param lambda_delta Regularization for offset δ (higher = more constrained to A_GR)
#' @param lambda_coef Regularization for Fourier coefficients
#' @param pool_phi If TRUE, use a single shared φ(θ) across all windows
#' @param max_iter Maximum EM iterations
#' @param tol Convergence tolerance
#' @param smooth_kappa von Mises smoothing parameter
#' @param verbose Print progress
#' @param seed Random seed
kpt_gem_comprehensive <- function(Y, t, A_GR,
                                   L_theta = 256,
                                   n_harmonics = 2,
                                   lambda_delta = 10.0,
                                   lambda_coef = 1.0,
                                   pool_phi = TRUE,
                                   max_iter = 50,
                                   tol = 1e-5,
                                   smooth_kappa = 30,
                                   verbose = TRUE,
                                   seed = 42) {
  set.seed(seed)
  
  I <- nrow(Y)  # Number of replicates
  K <- ncol(Y)  # Number of windows
  
  # Theta grid
  theta <- theta_grid(L_theta)
  dtheta <- 2 * pi / L_theta  # Integration measure
  
  # Precompute Fourier basis on grid
  cos_basis <- matrix(0, nrow = L_theta, ncol = n_harmonics)
  sin_basis <- matrix(0, nrow = L_theta, ncol = n_harmonics)
  for (j in 1:n_harmonics) {
    cos_basis[, j] <- cos(j * theta)
    sin_basis[, j] <- sin(j * theta)
  }
  
  # --------------------------------------------------------------------------
  # INITIALIZATION
  # --------------------------------------------------------------------------
  
  # Anchor mean to GR prediction
  mu_0 <- A_GR
  
  # Initialize small offsets (regularized toward zero)
  delta <- rep(0, K)
  
  # Initialize Fourier coefficients (small random values)
  a_coef <- matrix(rnorm(n_harmonics * K, 0, 0.1), nrow = n_harmonics, ncol = K)
  b_coef <- matrix(rnorm(n_harmonics * K, 0, 0.1), nrow = n_harmonics, ncol = K)
  
  # Initialize kime-phase distribution (uniform)
  if (pool_phi) {
    phi <- matrix(1, nrow = 1, ncol = L_theta)  # Single shared φ
  } else {
    phi <- matrix(1, nrow = K, ncol = L_theta)  # Per-window φ
  }
  
  # Initialize noise variance from data
  sigma2 <- var(as.vector(Y - A_GR), na.rm = TRUE)
  if (!is.finite(sigma2) || sigma2 <= 0) sigma2 <- 1
  
  # Storage for convergence tracking
  log_lik_history <- numeric(max_iter)
  delta_history <- numeric(max_iter)
  
  # --------------------------------------------------------------------------
  # EM ITERATIONS
  # --------------------------------------------------------------------------
  
  for (it in 1:max_iter) {
    params_old <- c(delta, as.vector(a_coef), as.vector(b_coef))
    log_lik_total <- 0
    
    # Accumulator for pooled phi update
    if (pool_phi) {
      phi_accum <- rep(0, L_theta)
      n_accum <- 0
    }
    
    # ========================================================================
    # E-STEP: Compute posterior weights w_{i,l} for each observation
    # ========================================================================
    
    for (k in 1:K) {
      yk <- Y[, k]
      ok <- is.finite(yk)
      n_ok <- sum(ok)
      if (n_ok == 0) next
      
      # Compute kime-surface on grid: S_k(θ_l)
      S_l <- mu_0 + delta[k]
      for (j in 1:n_harmonics) {
        S_l <- S_l + a_coef[j, k] * cos_basis[, j] + b_coef[j, k] * sin_basis[, j]
      }
      
      # Get current phi for this window
      phi_k <- if (pool_phi) phi[1, ] else phi[k, ]
      
      # Log-likelihood: log p(y_i | θ_l, params)
      # y_i ~ N(S_k(θ_l), σ²)
      D <- outer(yk[ok], S_l, FUN = "-")  # [n_ok x L_theta]
      log_lik <- -D^2 / (2 * sigma2) - 0.5 * log(2 * pi * sigma2)
      
      # Log-prior: log φ_k(θ_l)
      log_prior <- log(pmax(phi_k, 1e-300))
      
      # Log-posterior (unnormalized): log w_{i,l} ∝ log p(y_i | θ_l) + log φ(θ_l)
      log_w <- sweep(log_lik, 2, log_prior, "+")
      
      # Normalize to get posterior weights
      log_w_max <- apply(log_w, 1, max)
      w <- exp(log_w - log_w_max)
      w <- w / rowSums(w)
      
      # Accumulate log-likelihood
      log_lik_i <- log_w_max + log(rowSums(exp(log_w - log_w_max)))
      log_lik_total <- log_lik_total + sum(log_lik_i)
      
      # ====================================================================
      # M-STEP (Part 1): Update phi_k
      # ====================================================================
      
      # Aggregate posterior: phi_k(θ_l) ∝ mean_i[w_{i,l}]
      phi_new <- colMeans(w)
      phi_new <- smooth_circular(phi_new, kappa = smooth_kappa)
      phi_new <- pmax(phi_new, 1e-12)
      phi_new <- phi_new / (sum(phi_new) * dtheta)  # Normalize as density
      
      if (pool_phi) {
        phi_accum <- phi_accum + phi_new * n_ok
        n_accum <- n_accum + n_ok
      } else {
        phi[k, ] <- phi_new
      }
      
      # ====================================================================
      # M-STEP (Part 2): Update delta_k and Fourier coefficients
      # ====================================================================
      
      # Weighted regression approach
      # Target: minimize Σ_i Σ_l w_{i,l} (y_i - S_l)² + regularization
      
      # Sufficient statistics
      w_l <- colSums(w)  # [L_theta] - total weight per theta
      y_l <- as.numeric(t(w) %*% yk[ok])  # [L_theta] - weighted sum of y
      
      # Design matrix: [L_theta x (1 + 2*n_harmonics)]
      # Columns: 1, cos(θ), sin(θ), cos(2θ), sin(2θ), ...
      X <- cbind(1, cos_basis, sin_basis)
      n_coef <- ncol(X)
      
      # Weighted least squares with ridge regularization
      # β = (X'WX + λI)^{-1} X'Wy
      W <- diag(pmax(w_l, 1e-12))
      
      # Regularization matrix: penalize deviations from [μ_0, 0, 0, ...]
      # The intercept should be μ_0 + δ_k, so we penalize δ_k
      reg_diag <- c(lambda_delta, rep(lambda_coef, 2 * n_harmonics))
      Reg <- diag(reg_diag)
      
      # Prior mean: [μ_0, 0, 0, ...]
      prior_mean <- c(mu_0, rep(0, 2 * n_harmonics))
      
      # Regularized WLS solution
      XtWX <- t(X) %*% W %*% X
      XtWy <- t(X) %*% W %*% (y_l / pmax(w_l, 1e-12))
      
      beta <- solve(XtWX + Reg, XtWy + Reg %*% prior_mean)
      
      # Extract parameters
      delta[k] <- beta[1] - mu_0
      a_coef[, k] <- beta[2:(1 + n_harmonics)]
      b_coef[, k] <- beta[(2 + n_harmonics):(1 + 2 * n_harmonics)]
    }
    
    # Finalize pooled phi
    if (pool_phi && n_accum > 0) {
      phi[1, ] <- phi_accum / n_accum
      phi[1, ] <- phi[1, ] / (sum(phi[1, ]) * dtheta)
    }
    
    # ========================================================================
    # M-STEP (Part 3): Update sigma²
    # ========================================================================
    
    ss_total <- 0
    n_total <- 0
    
    for (k in 1:K) {
      yk <- Y[, k]
      ok <- is.finite(yk)
      if (!any(ok)) next
      
      # Compute kime-surface
      S_l <- mu_0 + delta[k]
      for (j in 1:n_harmonics) {
        S_l <- S_l + a_coef[j, k] * cos_basis[, j] + b_coef[j, k] * sin_basis[, j]
      }
      
      phi_k <- if (pool_phi) phi[1, ] else phi[k, ]
      
      # Recompute posteriors
      D <- outer(yk[ok], S_l, FUN = "-")
      log_lik <- -D^2 / (2 * sigma2)
      log_w <- sweep(log_lik, 2, log(pmax(phi_k, 1e-300)), "+")
      log_w <- log_w - apply(log_w, 1, max)
      w <- exp(log_w); w <- w / rowSums(w)
      
      # Weighted sum of squared residuals
      ss_total <- ss_total + sum(w * D^2)
      n_total <- n_total + sum(ok)
    }
    
    sigma2 <- ss_total / max(n_total, 1)
    sigma2 <- max(sigma2, 1e-6)  # Floor to prevent collapse
    
    # ========================================================================
    # CONVERGENCE CHECK
    # ========================================================================
    
    params_new <- c(delta, as.vector(a_coef), as.vector(b_coef))
    delta_param <- max(abs(params_new - params_old))
    
    log_lik_history[it] <- log_lik_total
    delta_history[it] <- delta_param
    
    if (verbose) {
      cat(sprintf("Iter %02d: LL=%.2f  Δparam=%.3e  σ=%.3f  mean(δ)=%.3f\n",
                  it, log_lik_total, delta_param, sqrt(sigma2), mean(delta)))
    }
    
    if (delta_param < tol) {
      log_lik_history <- log_lik_history[1:it]
      delta_history <- delta_history[1:it]
      if (verbose) cat("Converged!\n")
      break
    }
  }
  
  # --------------------------------------------------------------------------
  # COMPUTE FINAL KIME-SURFACE GRID
  # --------------------------------------------------------------------------
  
  Sgrid <- matrix(0, nrow = K, ncol = L_theta)
  for (k in 1:K) {
    Sgrid[k, ] <- mu_0 + delta[k]
    for (j in 1:n_harmonics) {
      Sgrid[k, ] <- Sgrid[k, ] + a_coef[j, k] * cos_basis[, j] + b_coef[j, k] * sin_basis[, j]
    }
  }
  
  # Compute expected value E[S_k(θ)] for each window
  phi_final <- if (pool_phi) phi[1, ] else phi
  E_S <- numeric(K)
  for (k in 1:K) {
    phi_k <- if (pool_phi) phi[1, ] else phi[k, ]
    E_S[k] <- sum(Sgrid[k, ] * phi_k) * dtheta
  }
  
  list(
    # Grid
    theta = theta,
    t = t,
    
    # Parameters
    mu_0 = mu_0,
    delta = delta,
    a_coef = a_coef,
    b_coef = b_coef,
    sigma = sqrt(sigma2),
    
    # Kime-phase distribution
    varphi_theta = phi,
    pool_phi = pool_phi,
    
    # Kime-surface
    Sgrid = Sgrid,
    
    # Expected values
    E_S = E_S,
    
    # Diagnostics
    log_lik_history = log_lik_history,
    delta_history = delta_history,
    n_harmonics = n_harmonics,
    
    # Regularization used
    lambda_delta = lambda_delta,
    lambda_coef = lambda_coef
  )
}

#' Generate predictive samples from the KPT model
#' 
#' @param fit Output from kpt_gem_comprehensive
#' @param t_new New time points for prediction
#' @param n_draw Number of Monte Carlo draws
#' @param method Extrapolation method: "gp" (Gaussian process), "spline", or "constant"
kpt_predict <- function(fit, t_new, n_draw = 5000, method = "constant") {
  
  K_new <- length(t_new)
  theta <- fit$theta
  L_theta <- length(theta)
  dtheta <- 2 * pi / L_theta
  
  # Get phi (shared or last window)
  phi <- if (fit$pool_phi) fit$varphi_theta[1, ] else fit$varphi_theta[nrow(fit$varphi_theta), ]
  phi <- phi / (sum(phi) * dtheta)  # Ensure normalized
  
  # Extrapolate parameters to new times
  if (method == "spline" && length(fit$t) >= 4) {
    # Use smooth spline with strong smoothing (lower df)
    sp_delta <- smooth.spline(fit$t, fit$delta, df = min(4, length(fit$t)/2))
    delta_new <- predict(sp_delta, t_new)$y
    
    # Shrink extrapolated values toward zero
    in_range <- t_new >= min(fit$t) & t_new <= max(fit$t)
    shrink <- exp(-0.1 * pmax(0, t_new - max(fit$t)))
    delta_new <- delta_new * shrink
    
  } else if (method == "gp" && length(fit$t) >= 4) {
    # Gaussian process with fixed hyperparameters for stability
    # This is more sophisticated but requires additional packages
    delta_new <- rep(mean(fit$delta), K_new)  # Placeholder
    
  } else {
    # Constant: use mean of training deltas
    delta_new <- rep(mean(fit$delta), K_new)
  }
  
  # Similarly for Fourier coefficients - use mean
  a_mean <- rowMeans(fit$a_coef)
  b_mean <- rowMeans(fit$b_coef)
  
  # Generate predictions
  predictions <- matrix(0, nrow = n_draw, ncol = K_new)
  point_pred <- numeric(K_new)
  PI_low <- numeric(K_new)
  PI_high <- numeric(K_new)
  
  cos_basis <- matrix(0, nrow = L_theta, ncol = fit$n_harmonics)
  sin_basis <- matrix(0, nrow = L_theta, ncol = fit$n_harmonics)
  for (j in 1:fit$n_harmonics) {
    cos_basis[, j] <- cos(j * theta)
    sin_basis[, j] <- sin(j * theta)
  }
  
  for (k in 1:K_new) {
    # Compute kime-surface at this new time
    S_l <- fit$mu_0 + delta_new[k]
    for (j in 1:fit$n_harmonics) {
      S_l <- S_l + a_mean[j] * cos_basis[, j] + b_mean[j] * sin_basis[, j]
    }
    
    # Sample theta from phi
    theta_samples <- sample(theta, n_draw, replace = TRUE, prob = phi)
    
    # Evaluate S at sampled thetas (interpolate)
    S_samples <- approx(theta, S_l, xout = theta_samples, rule = 2)$y
    
    # Add noise
    predictions[, k] <- S_samples + rnorm(n_draw, 0, fit$sigma)
    
    # Summary statistics
    point_pred[k] <- mean(predictions[, k])
    PI_low[k] <- quantile(predictions[, k], 0.025)
    PI_high[k] <- quantile(predictions[, k], 0.975)
  }
  
  list(
    t = t_new,
    draws = predictions,
    point = point_pred,
    PI_low = PI_low,
    PI_high = PI_high,
    delta_used = delta_new
  )
}

4.4.9 Train/Test Split with Physics-Constrained KPT

# Use the same data preparation as before
ok_col <- which(colSums(is.finite(Y)) > 0.8 * nrow(Y))
Y2 <- Y[, ok_col, drop = FALSE]
t2 <- t_center[ok_col]

K2 <- ncol(Y2)
cut <- floor(0.8 * K2)
idx_tr <- 1:cut
idx_te <- (cut + 1):K2

Y_tr <- Y2[, idx_tr, drop = FALSE]
t_tr <- t2[idx_tr]
Y_te <- Y2[, idx_te, drop = FALSE]
t_te <- t2[idx_te]

# Baseline noise scale
sigma_base <- median(apply(Y_tr, 2, sd, na.rm = TRUE))
if (!is.finite(sigma_base) || sigma_base <= 0) {
  sigma_base <- sd(as.vector(Y_tr), na.rm = TRUE)
}

cat("\n========================================\n")
## 
## ========================================
cat("FITTING COMPREHENSIVE KPT-GEM MODEL\n")
## FITTING COMPREHENSIVE KPT-GEM MODEL
cat("========================================\n")
## ========================================
cat(sprintf("Training windows: %d\n", length(idx_tr)))
## Training windows: 92
cat(sprintf("Test windows: %d\n", length(idx_te)))
## Test windows: 24
cat(sprintf("GR anchor value: %.2f arcsec/century\n", A_GR))
## GR anchor value: 42.98 arcsec/century
cat("\n")
# Fit the comprehensive model
fit_kpt <- kpt_gem_comprehensive(
  Y = Y_tr,
  t = t_tr,
  A_GR = A_GR,
  L_theta = 256,
  n_harmonics = 2,
  lambda_delta = 10.0,    # Strong regularization toward GR
  lambda_coef = 5.0,      # Regularize Fourier coefficients
  pool_phi = TRUE,        # Shared kime-phase distribution
  max_iter = 50,
  tol = 1e-5,
  smooth_kappa = 30,
  verbose = TRUE
)
## Iter 01: LL=4.95  Δparam=1.379e+02  σ=11.846  mean(δ)=-5.280
## Iter 02: LL=-3378.03  Δparam=1.718e-02  σ=11.846  mean(δ)=-5.280
## Iter 03: LL=-3378.02  Δparam=2.509e-02  σ=11.846  mean(δ)=-5.280
## Iter 04: LL=-3378.02  Δparam=3.664e-02  σ=11.845  mean(δ)=-5.280
## Iter 05: LL=-3378.01  Δparam=5.348e-02  σ=11.845  mean(δ)=-5.280
## Iter 06: LL=-3378.01  Δparam=7.806e-02  σ=11.845  mean(δ)=-5.280
## Iter 07: LL=-3377.99  Δparam=1.190e-01  σ=11.845  mean(δ)=-5.280
## Iter 08: LL=-3377.94  Δparam=2.054e-01  σ=11.845  mean(δ)=-5.280
## Iter 09: LL=-3377.83  Δparam=3.538e-01  σ=11.844  mean(δ)=-5.280
## Iter 10: LL=-3377.54  Δparam=6.045e-01  σ=11.842  mean(δ)=-5.280
## Iter 11: LL=-3376.77  Δparam=1.010e+00  σ=11.837  mean(δ)=-5.281
## Iter 12: LL=-3374.80  Δparam=1.589e+00  σ=11.826  mean(δ)=-5.282
## Iter 13: LL=-3370.31  Δparam=2.153e+00  σ=11.807  mean(δ)=-5.285
## Iter 14: LL=-3362.60  Δparam=2.221e+00  σ=11.783  mean(δ)=-5.291
## Iter 15: LL=-3353.81  Δparam=1.737e+00  σ=11.756  mean(δ)=-5.300
## Iter 16: LL=-3346.31  Δparam=1.770e+00  σ=11.729  mean(δ)=-5.311
## Iter 17: LL=-3340.84  Δparam=1.553e+00  σ=11.702  mean(δ)=-5.324
## Iter 18: LL=-3337.29  Δparam=1.375e+00  σ=11.671  mean(δ)=-5.339
## Iter 19: LL=-3334.59  Δparam=1.345e+00  σ=11.636  mean(δ)=-5.356
## Iter 20: LL=-3331.85  Δparam=1.354e+00  σ=11.593  mean(δ)=-5.373
## Iter 21: LL=-3328.89  Δparam=1.417e+00  σ=11.543  mean(δ)=-5.390
## Iter 22: LL=-3326.01  Δparam=1.277e+00  σ=11.485  mean(δ)=-5.407
## Iter 23: LL=-3323.97  Δparam=1.483e+00  σ=11.418  mean(δ)=-5.423
## Iter 24: LL=-3323.82  Δparam=1.882e+00  σ=11.343  mean(δ)=-5.441
## Iter 25: LL=-3326.81  Δparam=2.573e+00  σ=11.258  mean(δ)=-5.462
## Iter 26: LL=-3334.00  Δparam=3.678e+00  σ=11.161  mean(δ)=-5.488
## Iter 27: LL=-3346.13  Δparam=4.947e+00  σ=11.049  mean(δ)=-5.519
## Iter 28: LL=-3363.75  Δparam=5.814e+00  σ=10.918  mean(δ)=-5.537
## Iter 29: LL=-3387.23  Δparam=9.221e+00  σ=10.768  mean(δ)=-5.505
## Iter 30: LL=-3417.31  Δparam=1.714e+01  σ=10.600  mean(δ)=-5.379
## Iter 31: LL=-3457.60  Δparam=1.682e+01  σ=10.423  mean(δ)=-5.190
## Iter 32: LL=-3507.65  Δparam=1.927e+01  σ=10.243  mean(δ)=-4.956
## Iter 33: LL=-3566.97  Δparam=2.223e+01  σ=10.067  mean(δ)=-4.702
## Iter 34: LL=-3632.53  Δparam=1.394e+01  σ=9.903  mean(δ)=-4.482
## Iter 35: LL=-3699.94  Δparam=1.090e+01  σ=9.757  mean(δ)=-4.204
## Iter 36: LL=-3775.32  Δparam=1.198e+01  σ=9.632  mean(δ)=-3.786
## Iter 37: LL=-3855.51  Δparam=1.264e+01  σ=9.529  mean(δ)=-3.277
## Iter 38: LL=-3927.51  Δparam=1.260e+01  σ=9.448  mean(δ)=-2.776
## Iter 39: LL=-3981.84  Δparam=1.352e+01  σ=9.390  mean(δ)=-2.353
## Iter 40: LL=-4011.77  Δparam=1.165e+01  σ=9.353  mean(δ)=-2.074
## Iter 41: LL=-4011.55  Δparam=7.816e+00  σ=9.335  mean(δ)=-1.947
## Iter 42: LL=-3984.24  Δparam=4.775e+00  σ=9.331  mean(δ)=-1.916
## Iter 43: LL=-3941.32  Δparam=2.928e+00  σ=9.336  mean(δ)=-1.925
## Iter 44: LL=-3894.62  Δparam=1.868e+00  σ=9.345  mean(δ)=-1.946
## Iter 45: LL=-3851.71  Δparam=1.246e+00  σ=9.354  mean(δ)=-1.968
## Iter 46: LL=-3815.67  Δparam=9.677e-01  σ=9.361  mean(δ)=-1.988
## Iter 47: LL=-3786.87  Δparam=8.847e-01  σ=9.364  mean(δ)=-2.006
## Iter 48: LL=-3764.42  Δparam=9.018e-01  σ=9.363  mean(δ)=-2.023
## Iter 49: LL=-3747.12  Δparam=9.104e-01  σ=9.359  mean(δ)=-2.038
## Iter 50: LL=-3733.79  Δparam=8.483e-01  σ=9.353  mean(δ)=-2.051
cat("\n=== MODEL SUMMARY ===\n")
## 
## === MODEL SUMMARY ===
cat(sprintf("Converged in %d iterations\n", length(fit_kpt$log_lik_history)))
## Converged in 50 iterations
cat(sprintf("Final σ: %.3f arcsec/century\n", fit_kpt$sigma))
## Final σ: 9.353 arcsec/century
cat(sprintf("Mean δ (offset from GR): %.3f\n", mean(fit_kpt$delta)))
## Mean δ (offset from GR): -2.051
cat(sprintf("SD of δ across windows: %.3f\n", sd(fit_kpt$delta)))
## SD of δ across windows: 26.041
cat(sprintf("Mean E[S_k(θ)]: %.3f (should be ≈ %.2f)\n", mean(fit_kpt$E_S), A_GR))
## Mean E[S_k(θ)]: 38.192 (should be ≈ 42.98)
# Generate predictions on test set
pred_kpt <- kpt_predict(fit_kpt, t_te, n_draw = 8000, method = "constant")

4.4.10 Comprehensive Scoring

# Scoring functions
crps_gaussian <- function(y, mu, sigma) {
  z <- (y - mu) / sigma
  sigma * (z * (2 * pnorm(z) - 1) + 2 * dnorm(z) - 1/sqrt(pi))
}

log_score_mc <- function(y, draws) {
  # KDE-based log-score
  n <- length(draws)
  bw <- 1.06 * sd(draws) * n^(-1/5)
  bw <- max(bw, 1e-6)
  dens <- mean(dnorm(y, mean = draws, sd = bw))
  log(max(dens, 1e-300))
}

crps_mc <- function(y, draws) {
  mean(abs(draws - y)) - 0.5 * mean(abs(draws - sample(draws, replace = TRUE)))
}

# Compute predictions and scores for each method
pred_newton_mean <- 0
pred_gr_mean <- A_GR

results_list <- lapply(seq_len(ncol(Y_te)), function(k) {
  yk <- Y_te[, k]
  yk <- yk[is.finite(yk)]
  if (length(yk) < 10) return(NULL)
  
  # KPT predictions
  kpt_draws <- pred_kpt$draws[, k]
  m_kpt <- mean(kpt_draws)
  pi_kpt <- quantile(kpt_draws, c(0.025, 0.975))
  
  # RMSE
  rmse_newton <- sqrt(mean((yk - pred_newton_mean)^2))
  rmse_gr <- sqrt(mean((yk - pred_gr_mean)^2))
  rmse_kpt <- sqrt(mean((yk - m_kpt)^2))
  
  # MLPD
  mlpd_newton <- mean(dnorm(yk, mean = pred_newton_mean, sd = sigma_base, log = TRUE))
  mlpd_gr <- mean(dnorm(yk, mean = pred_gr_mean, sd = sigma_base, log = TRUE))
  mlpd_kpt <- mean(vapply(yk, log_score_mc, numeric(1), draws = kpt_draws))
  
  # CRPS
  crps_newton <- mean(crps_gaussian(yk, pred_newton_mean, sigma_base))
  crps_gr <- mean(crps_gaussian(yk, pred_gr_mean, sigma_base))
  crps_kpt <- mean(vapply(yk, crps_mc, numeric(1), draws = kpt_draws))
  
  # Bias
  bias_newton <- mean(yk) - pred_newton_mean
  bias_gr <- mean(yk) - pred_gr_mean
  bias_kpt <- mean(yk) - m_kpt
  
  tibble(
    window = k,
    t_center = t_te[k],
    y_bar = mean(yk),
    n_obs = length(yk),
    
    # Newton
    pred_newton = pred_newton_mean,
    rmse_newton = rmse_newton,
    mlpd_newton = mlpd_newton,
    crps_newton = crps_newton,
    bias_newton = bias_newton,
    
    # GR
    pred_gr = pred_gr_mean,
    rmse_gr = rmse_gr,
    mlpd_gr = mlpd_gr,
    crps_gr = crps_gr,
    bias_gr = bias_gr,
    
    # KPT
    pred_kpt = m_kpt,
    pi_low_kpt = as.numeric(pi_kpt[1]),
    pi_high_kpt = as.numeric(pi_kpt[2]),
    rmse_kpt = rmse_kpt,
    mlpd_kpt = mlpd_kpt,
    crps_kpt = crps_kpt,
    bias_kpt = bias_kpt
  )
})

results_df <- bind_rows(results_list)

# Aggregate results
summary_stats <- results_df %>%
  summarise(
    # Newton
    Newton_RMSE_mean = mean(rmse_newton, na.rm = TRUE),
    Newton_RMSE_sd = sd(rmse_newton, na.rm = TRUE),
    Newton_MLPD_mean = mean(mlpd_newton, na.rm = TRUE),
    Newton_MLPD_sd = sd(mlpd_newton, na.rm = TRUE),
    Newton_CRPS_mean = mean(crps_newton, na.rm = TRUE),
    Newton_CRPS_sd = sd(crps_newton, na.rm = TRUE),
    Newton_Bias = mean(bias_newton, na.rm = TRUE),
    
    # GR
    GR_RMSE_mean = mean(rmse_gr, na.rm = TRUE),
    GR_RMSE_sd = sd(rmse_gr, na.rm = TRUE),
    GR_MLPD_mean = mean(mlpd_gr, na.rm = TRUE),
    GR_MLPD_sd = sd(mlpd_gr, na.rm = TRUE),
    GR_CRPS_mean = mean(crps_gr, na.rm = TRUE),
    GR_CRPS_sd = sd(crps_gr, na.rm = TRUE),
    GR_Bias = mean(bias_gr, na.rm = TRUE),
    
    # KPT
    KPT_Pred_mean = mean(pred_kpt, na.rm = TRUE),
    KPT_Pred_sd = sd(pred_kpt, na.rm = TRUE),
    KPT_RMSE_mean = mean(rmse_kpt, na.rm = TRUE),
    KPT_RMSE_sd = sd(rmse_kpt, na.rm = TRUE),
    KPT_MLPD_mean = mean(mlpd_kpt, na.rm = TRUE),
    KPT_MLPD_sd = sd(mlpd_kpt, na.rm = TRUE),
    KPT_CRPS_mean = mean(crps_kpt, na.rm = TRUE),
    KPT_CRPS_sd = sd(crps_kpt, na.rm = TRUE),
    KPT_Bias = mean(bias_kpt, na.rm = TRUE)
  )

# Build comparison table
comparison_table <- tibble(
  Method = c("Newtonian (no anomaly)", "Relativistic GR", "KPT (Comprehensive GEM)"),
  
  `Point Prediction` = c(
    "0.00",
    sprintf("%.2f", A_GR),
    sprintf("%.2f ± %.2f", summary_stats$KPT_Pred_mean, summary_stats$KPT_Pred_sd)
  ),
  
  `95% PI` = c(
    "—",
    "—",
    sprintf("[%.2f, %.2f]", 
            mean(results_df$pi_low_kpt, na.rm = TRUE),
            mean(results_df$pi_high_kpt, na.rm = TRUE))
  ),
  
  RMSE = c(
    sprintf("%.2f ± %.2f", summary_stats$Newton_RMSE_mean, summary_stats$Newton_RMSE_sd),
    sprintf("%.2f ± %.2f", summary_stats$GR_RMSE_mean, summary_stats$GR_RMSE_sd),
    sprintf("%.2f ± %.2f", summary_stats$KPT_RMSE_mean, summary_stats$KPT_RMSE_sd)
  ),
  
  MLPD = c(
    sprintf("%.2f ± %.2f", summary_stats$Newton_MLPD_mean, summary_stats$Newton_MLPD_sd),
    sprintf("%.2f ± %.2f", summary_stats$GR_MLPD_mean, summary_stats$GR_MLPD_sd),
    sprintf("%.2f ± %.2f", summary_stats$KPT_MLPD_mean, summary_stats$KPT_MLPD_sd)
  ),
  
  CRPS = c(
    sprintf("%.2f ± %.2f", summary_stats$Newton_CRPS_mean, summary_stats$Newton_CRPS_sd),
    sprintf("%.2f ± %.2f", summary_stats$GR_CRPS_mean, summary_stats$GR_CRPS_sd),
    sprintf("%.2f ± %.2f", summary_stats$KPT_CRPS_mean, summary_stats$KPT_CRPS_sd)
  ),
  
  `Mean Bias` = c(
    sprintf("%.2f", summary_stats$Newton_Bias),
    sprintf("%.2f", summary_stats$GR_Bias),
    sprintf("%.2f", summary_stats$KPT_Bias)
  )
)

cat("\n")
cat("==============================================================================\n")
## ==============================================================================
cat("MERCURY PERIHELION ANOMALY: NEWTON vs GR vs KPT (Comprehensive)\n")
## MERCURY PERIHELION ANOMALY: NEWTON vs GR vs KPT (Comprehensive)
cat("==============================================================================\n")
## ==============================================================================
knitr::kable(comparison_table, format = "simple", align = "c")
Method Point Prediction 95% PI RMSE MLPD CRPS Mean Bias
Newtonian (no anomaly) 0.00 53.44 ± 32.69 -17.90 ± 14.70 46.54 ± 32.27 43.87
Relativistic GR 42.98 39.32 ± 20.68 -10.69 ± 6.94 32.18 ± 20.14 0.89
KPT (Comprehensive GEM) 38.20 ± 0.10 [19.61, 56.62] 39.86 ± 20.43 -67.01 ± 106.75 33.68 ± 20.09 5.68
cat("\n")
cat("Note: RMSE = Root Mean Square Error (lower is better)\n")
## Note: RMSE = Root Mean Square Error (lower is better)
cat("      MLPD = Mean Log Predictive Density (higher is better)\n")
##       MLPD = Mean Log Predictive Density (higher is better)
cat("      CRPS = Continuous Ranked Probability Score (lower is better)\n")
##       CRPS = Continuous Ranked Probability Score (lower is better)
cat("      Bias = Mean(Observed - Predicted)\n")
##       Bias = Mean(Observed - Predicted)

4.4.11 Visualization: Model Comparison

# Plot comparison
library(ggplot2)

plot_df <- results_df %>%
  mutate(
    date = as.Date("2000-01-01") + (t_center - 2451545) # Convert JD to Date
  )

# Predictions over time
# p1 <- ggplot(plot_df, aes(x = date)) +
#   # Observed means
#   geom_point(aes(y = y_bar), color = "black", size = 2, alpha = 0.7) +
#   
#   # Newton (baseline)
#   geom_hline(yintercept = 0, linetype = "dotted", color = "blue", linewidth = 1) +
#   
#   # GR
#   geom_hline(yintercept = A_GR, linetype = "dashed", color = "red", linewidth = 1) +
#   
#   # KPT predictions with PI
#   geom_ribbon(aes(ymin = pi_low_kpt, ymax = pi_high_kpt), 
#               fill = "green", alpha = 0.2) +
#   geom_line(aes(y = pred_kpt), color = "darkgreen", linewidth = 1) +
#   
#   labs(
#     title = "Mercury Perihelion Anomaly Predictions: Newton vs GR vs KPT",
#     subtitle = "KPT provides uncertainty quantification while anchoring to GR",
#     x = "Date",
#     y = "Anomalous Precession (arcsec/century)"
#   ) +
#   theme_minimal() +
#   annotate("text", x = min(plot_df$date), y = 5, label = "Newton (0)", 
#            hjust = 0, color = "blue") +
#   annotate("text", x = min(plot_df$date), y = A_GR + 5, 
#            label = sprintf("GR (%.2f)", A_GR), hjust = 0, color = "red") +
#   annotate("text", x = min(plot_df$date), y = max(plot_df$pi_high_kpt) + 5, 
#            label = "KPT (with 95% PI)", hjust = 0, color = "darkgreen")
# 
# p1
# Ensure A_GR is defined, A_GR <- 42.9799  # example value

p1_interactive <- plot_ly(plot_df, x = ~date) %>%
  
  # Observed means (black points)
  add_markers(
    y = ~y_bar,
    marker = list(color = "black", size = 6, opacity = 0.7),
    name = "Observed Mean",
    hoverinfo = "text",
    text = ~paste("Date:", date, "<br>Observed:", round(y_bar, 3))
  ) %>%
  
  # KPT prediction interval (ribbon)
  add_ribbons(
    ymin = ~pi_low_kpt,
    ymax = ~pi_high_kpt,
    fillcolor = "rgba(0,128,0,0.2)",  # green with transparency
    line = list(color = "transparent"),
    name = "KPT 95% PI"
  ) %>%
  
  # KPT predicted mean
  add_trace(
    y = ~pred_kpt,
    type = "scatter",
    mode = "lines",
    line = list(color = "darkgreen", width = 2),
    name = "KPT Prediction",
    hoverinfo = "text",
    text = ~paste("Date:", date, "<br>KPT:", round(pred_kpt, 3))
  ) %>%
  
  # Layout with reference lines and labels
  layout(
    title = list(
      text = "Mercury Perihelion Anomaly Predictions: Newton vs GR vs KPT",
      x = 0.5,
      font = list(size = 16)
    ),
    xaxis = list(
      title = "Date",
      rangeslider = list(visible = TRUE)
    ),
    yaxis = list(
      title = "Anomalous Precession (arcsec/century)",
      zeroline = FALSE
    ),
    hovermode = "x unified",
    showlegend = TRUE,
    legend = list(x = 0.02, y = 0.98),
    
    # Horizontal reference lines (Newton = 0, GR = A_GR)
    shapes = list(
      # Newton baseline (y = 0)
      list(
        type = "line",
        x0 = 0, x1 = 1,
        xref = "paper",
        y0 = 0, y1 = 0,
        line = list(color = "blue", dash = "dot", width = 1)
      ),
      # GR prediction
      list(
        type = "line",
        x0 = 0, x1 = 1,
        xref = "paper",
        y0 = A_GR, y1 = A_GR,
        line = list(color = "red", dash = "dash", width = 1)
      )
    ),
    
    # Annotations (labels near left edge)
    annotations = list(
      list(
        x = min(plot_df$date),
        y = 5,
        text = "Newton (0)",
        showarrow = FALSE,
        xanchor = "left",
        font = list(color = "blue")
      ),
      list(
        x = min(plot_df$date),
        y = A_GR + 5,
        text = sprintf("GR (%.2f)", A_GR),
        showarrow = FALSE,
        xanchor = "left",
        font = list(color = "red")
      ),
      list(
        x = min(plot_df$date),
        y = max(plot_df$pi_high_kpt, na.rm = TRUE) + 5,
        text = "KPT (with 95% PI)",
        showarrow = FALSE,
        xanchor = "left",
        font = list(color = "darkgreen")
      )
    )
  )

p1_interactive
# RMSE comparison boxplot
rmse_long <- results_df %>%
  dplyr::select(window, rmse_newton, rmse_gr, rmse_kpt) %>%
  pivot_longer(cols = starts_with("rmse"), names_to = "Method", values_to = "RMSE") %>%
  mutate(Method = case_when(
    Method == "rmse_newton" ~ "Newton",
    Method == "rmse_gr" ~ "GR",
    Method == "rmse_kpt" ~ "KPT"
  ))

# p2 <- ggplot(rmse_long, aes(x = Method, y = RMSE, fill = Method)) +
#   geom_boxplot(alpha = 0.7) +
#   scale_fill_manual(values = c("Newton" = "blue", "GR" = "red", "KPT" = "green")) +
#   labs(
#     title = "RMSE Distribution Across Test Windows",
#     y = "RMSE (arcsec/century)"
#   ) +
#   theme_minimal() +
#   theme(legend.position = "none")
# 
# p2
p2_interactive <- plot_ly(
  rmse_long,
  x = ~Method,
  y = ~RMSE,
  type = "box",
  color = ~Method,
  colors = c("Newton" = "blue", "GR" = "red", "KPT" = "green"),
  alpha = 0.7,
  boxpoints = "outliers",  # show outliers (default); use "all" for all points
  hoverinfo = "y+name"
) %>%
  layout(
    title = "RMSE Distribution Across Test Windows",
    yaxis = list(title = "RMSE (arcsec/century)"),
    xaxis = list(title = "Method"),
    showlegend = FALSE,  # matches theme(legend.position = "none")
    boxmode = "group"    # not strictly needed for single series, but good practice
  )

p2_interactive

Note that the comprehensive KPT model works since it relies on:

  1. Physics Anchoring: \(\mu_0 = A_{\text{GR}}\) is fixed, not learned. The model only learns deviations from GR.

  2. Strong Regularization: \(\lambda_\delta\) penalizes offsets from GR, preventing the model from drifting to unrealistic values, e.g., \(1,100\) arcsec/century.

  3. Pooled Kime-Phase: Using a single shared \(\varphi(\theta)\) across windows prevents overfitting and ensures consistent behavior.

  4. Conservative Extrapolation: For test windows, we use the mean of training parameters rather than potentially unstable spline extrapolation.

  5. Physical Constraint Enforcement: We verify that \(\mathbb{E}[S_k(\theta)] \approx A_{\text{GR}}\) after training.

4.4.12 KPT Model Diagnostics

### KPT Model Diagnostics - Enhanced Version
### Interactive visualizations with proper date formatting

# Load required libraries
library(plotly)
library(dplyr)
library(ggplot2)
library(viridis)

# Ensure fit_kpt exists
if (!exists("fit_kpt")) {
  stop("fit_kpt object not found. Please run KPT fitting first.")
}

# Function to convert JD to proper date format
jd_to_proper_date <- function(jd) {
  # JD 0 = 12:00 January 1, 4713 BC
  # JD 2440587.5 = 12:00 January 1, 1970 (Unix epoch)
  return(as.Date(jd - 2440587.5, origin = "1970-01-01"))
}

# Convert time values to dates if they're Julian Dates
convert_time_to_date <- function(t_values, check_threshold = 2400000) {
  if (mean(t_values, na.rm = TRUE) > check_threshold) {
    # Likely Julian Dates
    return(jd_to_proper_date(t_values))
  } else {
    # Likely already in years or other units
    return(t_values)
  }
}

# ==============================================================================
# 1. ENHANCED: Kime-Phase Distribution φ(θ) with Interactive Analysis
# ==============================================================================

# Extract phi values
phi_final <- if (exists("fit_kpt$pool_phi") && fit_kpt$pool_phi) {
  fit_kpt$varphi_theta[1, ]
} else if (exists("fit_kpt$varphi_theta")) {
  colMeans(fit_kpt$varphi_theta, na.rm = TRUE)
} else {
  rep(NA, length(fit_kpt$theta))
}

# Create comprehensive phi analysis data
phi_data <- data.frame(
  theta = fit_kpt$theta,
  phi = phi_final,
  phi_normalized = phi_final / sum(phi_final, na.rm = TRUE),
  cumulative_phi = cumsum(phi_final / sum(phi_final, na.rm = TRUE))
)

# Statistics
phi_stats <- list(
  mean_theta = weighted.mean(phi_data$theta, phi_data$phi_normalized, na.rm = TRUE),
  sd_theta = sqrt(weighted.mean((phi_data$theta - weighted.mean(phi_data$theta, phi_data$phi_normalized))^2, 
                                 phi_data$phi_normalized, na.rm = TRUE)),
  entropy = -sum(phi_data$phi_normalized * log(phi_data$phi_normalized + 1e-10), na.rm = TRUE),
  max_phi = max(phi_data$phi, na.rm = TRUE),
  argmax_theta = phi_data$theta[which.max(phi_data$phi)]
)

# Interactive kime-phase distribution
p_phi_interactive <- plot_ly(phi_data, x = ~theta) %>%
  # Main phi distribution
  add_trace(y = ~phi,
            type = 'scatter',
            mode = 'lines',
            name = 'φ(θ)',
            line = list(color = 'purple', width = 3),
            fill = 'tozeroy',
            fillcolor = 'rgba(128, 0, 128, 0.3)',
            hoverinfo = 'text',
            text = ~paste(
              sprintf("θ = %.3f", theta),
              sprintf("φ(θ) = %.4f", phi),
              sprintf("Normalized: %.4f", phi_normalized),
              sep = "<br>"
            )) %>%
  
  layout(
    title = list(
      text = "Learned Kime-Phase Distribution φ(θ)",
      font = list(size = 16, color = 'darkblue')
    ),
    xaxis = list(
      title = "θ (Kime-Phase)",
      gridcolor = 'lightgray',
      zeroline = FALSE
    ),
    yaxis = list(
      title = "φ(θ)",
      gridcolor = 'lightgray'
    ),
    hovermode = 'x unified',
    showlegend = TRUE,
    legend = list(x = 0.02, y = 0.98),
    margin = list(t = 50),
    
    # Add vertical reference lines via shapes
    shapes = list(
      # Mean θ
      list(
        type = "line",
        x0 = phi_stats$mean_theta,
        x1 = phi_stats$mean_theta,
        y0 = 0,
        y1 = 1,
        yref = "paper",  # spans full height of plot
        line = list(color = "red", dash = "dash", width = 2)
      ),
      # Mode θ (argmax)
      list(
        type = "line",
        x0 = phi_stats$argmax_theta,
        x1 = phi_stats$argmax_theta,
        y0 = 0,
        y1 = 1,
        yref = "paper",
        line = list(color = "green", dash = "dot", width = 2)
      )
    ),
    
    # Optional: add legend-like annotations if needed (not required since traces are labeled)
    annotations = list(
      list(
        x = 0.02,
        y = 0.98,
        xref = "paper",
        yref = "paper",
        text = paste(
          sprintf("Mean θ: %.3f", phi_stats$mean_theta),
          sprintf("SD θ: %.3f", phi_stats$sd_theta),
          sprintf("Entropy: %.3f", phi_stats$entropy),
          sep = "<br>"
        ),
        showarrow = FALSE,
        bgcolor = "rgba(255,255,255,0.8)",
        bordercolor = "black",
        borderwidth = 1
      )
    )
  )

p_phi_interactive
# Additional: Cumulative distribution
p_phi_cumulative <- plot_ly(phi_data, x = ~theta) %>%
  add_trace(y = ~cumulative_phi,
            type = 'scatter',
            mode = 'lines',
            name = 'Cumulative φ(θ)',
            line = list(color = 'orange', width = 3),
            hoverinfo = 'text',
            text = ~paste(
              sprintf("θ = %.3f", theta),
              sprintf("Cumulative: %.3f", cumulative_phi),
              sep = "<br>"
            )) %>%
  layout(
    title = "Cumulative Kime-Phase Distribution",
    xaxis = list(title = "θ (Kime-Phase)"),
    yaxis = list(title = "Cumulative Probability"),
    showlegend = TRUE,
    
    # Add horizontal line at y = 0.5
    shapes = list(
      list(
        type = "line",
        x0 = 0, x1 = 1,
        xref = "paper",       # spans entire plot width
        y0 = 0.5, y1 = 0.5,
        line = list(color = "red", dash = "dash", width = 2)
      )
    ),
    
    # Optional: add annotation for the median line
    annotations = list(
      list(
        x = 1, y = 0.5,
        xref = "paper", yref = "y",
        xanchor = "right", yanchor = "bottom",
        text = "Median",
        showarrow = FALSE,
        font = list(color = "red")
      )
    )
  )

p_phi_cumulative
# ==============================================================================
# 2. ENHANCED: Offset δ over Time with Date Conversion
# ==============================================================================

# Prepare delta data with proper dates
delta_data <- data.frame(
  t_original = fit_kpt$t,
  delta = fit_kpt$delta
)

# Convert time to dates if they look like Julian Dates
delta_data$date <- convert_time_to_date(delta_data$t_original)
delta_data$year <- if (inherits(delta_data$date, "Date")) {
  as.numeric(format(delta_data$date, "%Y")) + 
    (as.numeric(format(delta_data$date, "%j")) - 1) / 365.25
} else {
  delta_data$t_original
}

# Calculate statistics
delta_stats <- list(
  mean_delta = mean(delta_data$delta, na.rm = TRUE),
  sd_delta = sd(delta_data$delta, na.rm = TRUE),
  min_delta = min(delta_data$delta, na.rm = TRUE),
  max_delta = max(delta_data$delta, na.rm = TRUE),
  trend = if (nrow(delta_data) > 1) {
    coef(lm(delta ~ year, data = delta_data))[2]
  } else 0
)

# Interactive delta plot
p_delta_interactive <- plot_ly(delta_data, x = ~date) %>%
  # Delta points
  add_trace(y = ~delta,
            type = 'scatter',
            mode = 'markers+lines',
            name = 'δ(t)',
            line = list(color = 'orange', width = 2),
            marker = list(size = 8, color = 'orange'),
            hoverinfo = 'text',
            text = ~paste(
              ifelse(inherits(date, "Date"), 
                     sprintf("Date: %s", format(date, "%Y-%m-%d")),
                     sprintf("Time: %.1f", t_original)),
              sprintf("δ = %.4f arcsec/cy", delta),
              sprintf("JD = %.1f", t_original),
              sep = "<br>"
            )) %>%

  # ±1 SD bands (ribbon)
  add_ribbons(
    ymin = rep(delta_stats$mean_delta - delta_stats$sd_delta, nrow(delta_data)),
    ymax = rep(delta_stats$mean_delta + delta_stats$sd_delta, nrow(delta_data)),
    name = '±1 SD',
    fillcolor = 'rgba(255,165,0,0.2)',
    line = list(color = 'transparent')
  ) %>%

  layout(
    title = list(
      text = "Offset δ from GR Prediction Over Training Windows",
      font = list(size = 16)
    ),
    xaxis = list(
      title = ifelse(inherits(delta_data$date, "Date"), 
                     "Date (Window Center)", 
                     "Window Center"),
      tickformat = ifelse(inherits(delta_data$date, "Date"), "%Y-%m", ""),
      rangeslider = list(visible = TRUE)
    ),
    yaxis = list(
      title = "δ (arcsec/century)",
      gridcolor = 'lightgray'
    ),
    hovermode = 'x unified',
    showlegend = TRUE,
    margin = list(t = 50),

    # Horizontal reference lines via shapes
    shapes = list(
      # Zero line
      list(
        type = "line",
        x0 = 0, x1 = 1,
        xref = "paper",
        y0 = 0, y1 = 0,
        line = list(color = "gray", dash = "dash", width = 1)
      ),
      # Mean delta line
      list(
        type = "line",
        x0 = 0, x1 = 1,
        xref = "paper",
        y0 = delta_stats$mean_delta,
        y1 = delta_stats$mean_delta,
        line = list(color = "red", dash = "dash", width = 2)
      )
    ),

    # Annotation box
    annotations = list(
      list(
        x = 0.98,
        y = 0.98,
        xref = "paper",
        yref = "paper",
        text = paste(
          sprintf("Mean δ: %.3f", delta_stats$mean_delta),
          sprintf("SD δ: %.3f", delta_stats$sd_delta),
          sprintf("Trend: %.3f/century", delta_stats$trend * 100),
          sep = "<br>"
        ),
        showarrow = FALSE,
        align = "right",
        bgcolor = "rgba(255,255,255,0.8)"
      )
    )
  )

p_delta_interactive
# Additional: Histogram of delta values
p_delta_hist <- plot_ly(
    x = ~delta_data$delta,
    type = 'histogram',
    nbinsx = 20,
    name = 'δ Distribution',
    marker = list(
      color = 'orange',
      line = list(color = 'darkorange', width = 1)
    )
  ) %>%
  layout(
    title = "Distribution of δ Values",
    xaxis = list(title = "δ (arcsec/century)"),
    yaxis = list(title = "Frequency"),
    showlegend = TRUE,
    
    # Add vertical lines via shapes
    shapes = list(
      # Mean δ
      list(
        type = "line",
        x0 = delta_stats$mean_delta,
        x1 = delta_stats$mean_delta,
        y0 = 0,
        y1 = 1,
        yref = "paper",  # spans full height of plot
        line = list(color = "red", dash = "dash", width = 2)
      ),
      # Zero line
      list(
        type = "line",
        x0 = 0,
        x1 = 0,
        y0 = 0,
        y1 = 1,
        yref = "paper",
        line = list(color = "gray", dash = "solid", width = 2)
      )
    ),
    
    # Optional: add labels near the top of the lines
    annotations = list(
      list(
        x = delta_stats$mean_delta, y = 1,
        yref = "paper", xref = "x",
        text = "Mean",
        showarrow = FALSE,
        font = list(color = "red"),
        xanchor = "center", yanchor = "bottom"
      ),
      list(
        x = 0, y = 1,
        yref = "paper", xref = "x",
        text = "Zero",
        showarrow = FALSE,
        font = list(color = "gray"),
        xanchor = "center", yanchor = "bottom"
      )
    )
  )

p_delta_hist
# ==============================================================================
# 3. ENHANCED: Convergence Analysis with Additional Metrics
# ==============================================================================

# Extract convergence history
conv_data <- data.frame(
  iteration = 1:length(fit_kpt$log_lik_history),
  log_likelihood = fit_kpt$log_lik_history
)

# Calculate convergence metrics
if (length(conv_data$log_likelihood) > 1) {
  conv_data$improvement <- c(NA, diff(conv_data$log_likelihood))
  conv_data$rel_improvement <- c(NA, diff(conv_data$log_likelihood) / 
                                  abs(conv_data$log_likelihood[-length(conv_data$log_likelihood)]))
}

# Convergence plot
p_conv_interactive <- plot_ly(conv_data, x = ~iteration) %>%
  # Log-likelihood trace
  add_trace(y = ~log_likelihood,
            type = 'scatter',
            mode = 'lines+markers',
            name = 'Log-Likelihood',
            line = list(color = 'navy', width = 3),
            marker = list(size = 8, color = 'navy'),
            yaxis = 'y',
            hoverinfo = 'text',
            text = ~paste(
              sprintf("Iteration: %d", iteration),
              sprintf("Log-Likelihood: %.2f", log_likelihood),
              ifelse(!is.na(improvement), 
                     sprintf("Improvement: %.4f", improvement),
                     ""),
              sep = "<br>"
            )) %>%
  
  # Improvement (secondary axis if available)
  add_trace(y = ~improvement,
            type = 'scatter',
            mode = 'lines',
            name = 'Improvement',
            line = list(color = 'red', width = 2, dash = 'dot'),
            yaxis = 'y2',
            visible = 'legendonly',
            hoverinfo = 'text',
            text = ~paste(
              sprintf("Iteration: %d", iteration),
              sprintf("Improvement: %.4f", improvement),
              sep = "<br>"
            )) %>%
  
  layout(
    title = list(
      text = "KPT-GEM Convergence History",
      font = list(size = 16)
    ),
    xaxis = list(
      title = "Iteration",
      gridcolor = 'lightgray'
    ),
    yaxis = list(
      title = "Log-Likelihood",
      gridcolor = 'lightgray'
    ),
    yaxis2 = list(
      title = "Improvement",
      overlaying = "y",
      side = "right",
      showgrid = FALSE
    ),
    hovermode = 'x unified',
    showlegend = TRUE,
    legend = list(x = 0.02, y = 0.98)
  )

p_conv_interactive
# Convergence diagnostics
if (nrow(conv_data) > 10) {
  final_ll <- tail(conv_data$log_likelihood, 1)
  initial_ll <- head(conv_data$log_likelihood, 1)
  total_improvement <- final_ll - initial_ll
  avg_improvement <- mean(diff(conv_data$log_likelihood), na.rm = TRUE)
  
  cat("\n=== CONVERGENCE DIAGNOSTICS ===\n")
  cat(sprintf("Initial log-likelihood: %.4f\n", initial_ll))
  cat(sprintf("Final log-likelihood: %.4f\n", final_ll))
  cat(sprintf("Total improvement: %.4f\n", total_improvement))
  cat(sprintf("Average improvement per iteration: %.4f\n", avg_improvement))
  cat(sprintf("Number of iterations: %d\n", nrow(conv_data)))
}
## 
## === CONVERGENCE DIAGNOSTICS ===
## Initial log-likelihood: 4.9477
## Final log-likelihood: -3733.7851
## Total improvement: -3738.7328
## Average improvement per iteration: -76.3007
## Number of iterations: 50
# ==============================================================================
# 4. ENHANCED: Expected Value E[S_k(θ)] vs GR with Date Conversion
# ==============================================================================

# Prepare E[S] data
ES_data <- data.frame(
  t_original = fit_kpt$t,
  E_S = fit_kpt$E_S
)

# Convert time to dates
ES_data$date <- convert_time_to_date(ES_data$t_original)

# Calculate deviation from GR
ES_data$deviation_from_GR <- ES_data$E_S - A_GR
ES_data$percent_of_GR <- (ES_data$E_S / A_GR) * 100

# Statistics
ES_stats <- list(
  mean_E_S = mean(ES_data$E_S, na.rm = TRUE),
  sd_E_S = sd(ES_data$E_S, na.rm = TRUE),
  mean_deviation = mean(ES_data$deviation_from_GR, na.rm = TRUE),
  rms_deviation = sqrt(mean(ES_data$deviation_from_GR^2, na.rm = TRUE))
)

# Interactive E[S] plot
p_ES_interactive <- plot_ly(ES_data, x = ~date) %>%
  # E[S] values
  add_trace(
    y = ~E_S,
    type = 'scatter',
    mode = 'markers+lines',
    name = 'E[S(θ)]',
    line = list(color = 'darkgreen', width = 3),
    marker = list(size = 10, color = 'darkgreen', symbol = 'circle'),
    hoverinfo = 'text',
    text = ~paste(
      ifelse(inherits(date, "Date"), 
             sprintf("Date: %s", format(date, "%Y-%m-%d")),
             sprintf("Time: %.1f", t_original)),
      sprintf("E[S(θ)] = %.4f", E_S),
      sprintf("GR = %.4f", A_GR),
      sprintf("Deviation: %.4f", deviation_from_GR),
      sprintf("%.2f%% of GR", percent_of_GR),
      sep = "<br>"
    )
  ) %>%
  
  # ±1 SD band around GR (ribbon)
  add_ribbons(
    ymin = rep(A_GR - ES_stats$sd_E_S, nrow(ES_data)),
    ymax = rep(A_GR + ES_stats$sd_E_S, nrow(ES_data)),
    name = '±1 SD from GR',
    fillcolor = 'rgba(255,0,0,0.1)',
    line = list(color = 'transparent')
  ) %>%
  
  layout(
    title = list(
      text = "Expected Value E[S(θ)] vs GR Prediction",
      font = list(size = 16)
    ),
    xaxis = list(
      title = ifelse(inherits(ES_data$date, "Date"), 
                     "Date (Window Center)", 
                     "Window Center"),
      tickformat = ifelse(inherits(ES_data$date, "Date"), "%Y-%m", ""),
      rangeslider = list(visible = TRUE)
    ),
    yaxis = list(
      title = "E[S(θ)] (arcsec/century)",
      gridcolor = 'lightgray'
    ),
    hovermode = 'x unified',
    showlegend = TRUE,
    margin = list(t = 50),
    
    # Horizontal reference lines via shapes
    shapes = list(
      # GR prediction line
      list(
        type = "line",
        x0 = 0, x1 = 1,
        xref = "paper",
        y0 = A_GR, y1 = A_GR,
        line = list(color = "red", dash = "dash", width = 3)
      ),
      # Mean E[S] line
      list(
        type = "line",
        x0 = 0, x1 = 1,
        xref = "paper",
        y0 = ES_stats$mean_E_S, y1 = ES_stats$mean_E_S,
        line = list(color = "blue", dash = "dot", width = 2)
      )
    ),
    
    # Annotation box (bottom-right)
    annotations = list(
      list(
        x = 0.98,
        y = 0.02,
        xref = "paper",
        yref = "paper",
        text = paste(
          sprintf("Mean E[S]: %.4f", ES_stats$mean_E_S),
          sprintf("SD: %.4f", ES_stats$sd_E_S),
          sprintf("RMS deviation from GR: %.4f", ES_stats$rms_deviation),
          sep = "<br>"
        ),
        showarrow = FALSE,
        align = "right",
        bgcolor = "rgba(255,255,255,0.8)"
      )
    )
  )

p_ES_interactive
# ==============================================================================
# 5. ADDITIONAL DIAGNOSTICS: Model Parameter Correlations
# ==============================================================================

if (exists("fit_kpt$delta") && exists("fit_kpt$E_S") && 
    length(fit_kpt$delta) == length(fit_kpt$E_S)) {
  
  corr_data <- data.frame(
    delta = fit_kpt$delta,
    E_S = fit_kpt$E_S
  )
  
  correlation <- cor(corr_data$delta, corr_data$E_S, use = "complete.obs")
  
  p_correlation <- plot_ly(corr_data, x = ~delta, y = ~E_S,
                           type = 'scatter',
                           mode = 'markers',
                           marker = list(size = 10, 
                                         color = ~E_S,
                                         colorscale = 'Viridis',
                                         showscale = TRUE),
                           name = 'Window',
                           hoverinfo = 'text',
                           text = ~paste(
                             sprintf("δ = %.4f", delta),
                             sprintf("E[S] = %.4f", E_S),
                             sep = "<br>"
                           )) %>%
    add_lines(x = range(corr_data$delta, na.rm = TRUE),
              y = predict(lm(E_S ~ delta, data = corr_data),
                         newdata = data.frame(delta = range(corr_data$delta, na.rm = TRUE))),
              line = list(color = 'red', width = 2),
              name = 'Regression') %>%
    layout(
      title = sprintf("Correlation between δ and E[S]: r = %.3f", correlation),
      xaxis = list(title = "δ (arcsec/century)"),
      yaxis = list(title = "E[S(θ)] (arcsec/century)"),
      showlegend = TRUE
    )
  
  p_correlation
}

# ==============================================================================
# 6. COMPOSITE DASHBOARD VIEW
# ==============================================================================

# Create a summary dashboard
cat("\n=== KPT MODEL DIAGNOSTICS SUMMARY ===\n")
## 
## === KPT MODEL DIAGNOSTICS SUMMARY ===
cat(sprintf("\n1. Kime-Phase Distribution:\n"))
## 
## 1. Kime-Phase Distribution:
cat(sprintf("   Mean θ: %.3f, SD θ: %.3f\n", phi_stats$mean_theta, phi_stats$sd_theta))
##    Mean θ: NA, SD θ: NaN
cat(sprintf("   Entropy: %.3f, Max φ: %.4f at θ = %.3f\n", 
            phi_stats$entropy, phi_stats$max_phi, phi_stats$argmax_theta))

cat(sprintf("\n2. Offset δ Statistics:\n"))
## 
## 2. Offset δ Statistics:
cat(sprintf("   Mean δ: %.4f ± %.4f arcsec/cy\n", delta_stats$mean_delta, delta_stats$sd_delta))
##    Mean δ: -2.0508 ± 26.0409 arcsec/cy
cat(sprintf("   Range: [%.4f, %.4f]\n", delta_stats$min_delta, delta_stats$max_delta))
##    Range: [-54.3838, 47.3894]
cat(sprintf("   Trend: %.4f arcsec/cy per century\n", delta_stats$trend * 100))
##    Trend: 2.2421 arcsec/cy per century
cat(sprintf("\n3. Expected Value E[S(θ)]:\n"))
## 
## 3. Expected Value E[S(θ)]:
cat(sprintf("   Mean: %.4f ± %.4f arcsec/cy\n", ES_stats$mean_E_S, ES_stats$sd_E_S))
##    Mean: 38.1922 ± 55.3321 arcsec/cy
cat(sprintf("   Deviation from GR: %.4f ± %.4f arcsec/cy\n", 
            ES_stats$mean_deviation, ES_stats$rms_deviation))
##    Deviation from GR: -4.7877 ± 55.2384 arcsec/cy
if (exists("correlation")) {
  cat(sprintf("\n4. Parameter Correlations:\n"))
  cat(sprintf("   δ vs E[S]: r = %.3f\n", correlation))
}

# ==============================================================================
# 7. SAVE DIAGNOSTICS DATA
# ==============================================================================

# Save all diagnostics data
diagnostics_list <- list(
  phi_data = phi_data,
  phi_stats = phi_stats,
  delta_data = delta_data,
  delta_stats = delta_stats,
  conv_data = conv_data,
  ES_data = ES_data,
  ES_stats = ES_stats,
  timestamp = Sys.time(),
  model_version = ifelse(exists("fit_kpt$version"), fit_kpt$version, "unknown")
)

saveRDS(diagnostics_list, "kpt_diagnostics_data.rds")
cat("\nDiagnostics data saved to: kpt_diagnostics_data.rds\n")
## 
## Diagnostics data saved to: kpt_diagnostics_data.rds
# Create a summary HTML report
if (requireNamespace("htmltools", quietly = TRUE)) {
  library(htmltools)
  
  summary_html <- tags$div(
    tags$h2("KPT Model Diagnostics Summary"),
    tags$h3("Model Parameters"),
    tags$ul(
      tags$li(sprintf("Kime-phase mean θ: %.3f", phi_stats$mean_theta)),
      tags$li(sprintf("Kime-phase SD θ: %.3f", phi_stats$sd_theta)),
      tags$li(sprintf("Mean offset δ: %.4f ± %.4f", delta_stats$mean_delta, delta_stats$sd_delta)),
      tags$li(sprintf("Mean E[S]: %.4f ± %.4f", ES_stats$mean_E_S, ES_stats$sd_E_S))
    ),
    tags$h3("Convergence"),
    tags$ul(
      tags$li(sprintf("Iterations: %d", nrow(conv_data))),
      tags$li(sprintf("Final log-likelihood: %.2f", tail(conv_data$log_likelihood, 1)))
    )
  )
  
  # You could save this to an HTML file
  # save_html(summary_html, "kpt_diagnostics_summary.html")
}

cat("\n=== KPT DIAGNOSTICS COMPLETE ===\n")
## 
## === KPT DIAGNOSTICS COMPLETE ===
cat(sprintf("Created %d interactive visualizations\n", 5))
## Created 5 interactive visualizations

4.4.13 Enhanced Visual Diagnostics for Comprehensive KPT Model

This section provides additional visualizations for the physics-constrained KPT-GEM model comparing it to Newton and GR baselines.

# ==============================================================================
# ENHANCED VISUAL DIAGNOSTICS FOR COMPREHENSIVE KPT MODEL
# ==============================================================================

# Load required libraries
library(plotly)
library(ggplot2)
library(dplyr)
library(tidyr)
library(viridis)
library(patchwork)
library(scales)

# Define color scheme for consistency
colors <- list(

  observed = "#2C3E50",      # Dark blue-gray
  newton = "#3498DB",        # Blue
  gr = "#E74C3C",           # Red
  kpt = "#27AE60",          # Green
  kpt_light = "rgba(39, 174, 96, 0.2)",
  uncertainty = "rgba(39, 174, 96, 0.15)",
  highlight = "#F39C12",    # Orange for highlights
  grid = "#ECF0F1"          # Light gray
)

# Verify required objects exist
required_objects <- c("results_df", "fit_kpt", "pred_kpt", "results_df", "A_GR")
missing <- required_objects[!sapply(required_objects, exists)]
if (length(missing) > 0) {
  stop(paste("Missing required objects:", paste(missing, collapse = ", ")))
}

4.4.13.1 Main Results Comparison

# ==============================================================================
# (A) COMPREHENSIVE TIME SERIES COMPARISON
# ==============================================================================

# Prepare plotting data with enhanced metrics
df_plot <- results_df %>%
  mutate(
    # Time conversions
    t_year = (t_center - min(t_center)) / 365.25,
    t_date = as.Date("2000-01-01") + (t_center - 2451545),
    
    # Predictions
    GR_pred = A_GR,
    Newton_pred = 0,
    
    # Residuals
    Residual_Newton = y_bar - 0,
    Residual_GR = y_bar - A_GR,
    Residual_KPT = y_bar - pred_kpt,
    
    # Standardized residuals (for diagnostics)
    Std_Residual_Newton = Residual_Newton / sd(Residual_Newton, na.rm = TRUE),
    Std_Residual_GR = Residual_GR / sd(Residual_GR, na.rm = TRUE),
    Std_Residual_KPT = Residual_KPT / ((pi_high_kpt - pi_low_kpt) / 3.92),  # Approx SD from 95% PI
    
    # Coverage indicators
    KPT_coverage = (y_bar >= pi_low_kpt & y_bar <= pi_high_kpt),
    
    # PI width
    PI_width = pi_high_kpt - pi_low_kpt
  )

# Calculate comprehensive coverage statistics
coverage_stats <- df_plot %>%
  summarise(
    KPT_95_coverage = mean(KPT_coverage, na.rm = TRUE) * 100,
    Mean_PI_width = mean(PI_width, na.rm = TRUE),
    Median_PI_width = median(PI_width, na.rm = TRUE),
    N_windows = n(),
    
    # Mean absolute errors
    MAE_Newton = mean(abs(Residual_Newton), na.rm = TRUE),
    MAE_GR = mean(abs(Residual_GR), na.rm = TRUE),
    MAE_KPT = mean(abs(Residual_KPT), na.rm = TRUE),
    
    # RMSE
    RMSE_Newton = sqrt(mean(Residual_Newton^2, na.rm = TRUE)),
    RMSE_GR = sqrt(mean(Residual_GR^2, na.rm = TRUE)),
    RMSE_KPT = sqrt(mean(Residual_KPT^2, na.rm = TRUE)),
    
    # Bias
    Bias_Newton = mean(Residual_Newton, na.rm = TRUE),
    Bias_GR = mean(Residual_GR, na.rm = TRUE),
    Bias_KPT = mean(Residual_KPT, na.rm = TRUE)
  )

cat("\n")
cat("╔══════════════════════════════════════════════════════════════╗\n")
## ╔══════════════════════════════════════════════════════════════╗
cat("║         COMPREHENSIVE KPT MODEL - SUMMARY STATISTICS         ║\n")
## ║         COMPREHENSIVE KPT MODEL - SUMMARY STATISTICS         ║
cat("╠══════════════════════════════════════════════════════════════╣\n")
## ╠══════════════════════════════════════════════════════════════╣
cat(sprintf("║  Test Windows:              %4d                             ║\n", coverage_stats$N_windows))
## ║  Test Windows:                24                             ║
cat(sprintf("║  KPT 95%% PI Coverage:       %5.1f%%                           ║\n", coverage_stats$KPT_95_coverage))
## ║  KPT 95% PI Coverage:        16.7%                           ║
cat(sprintf("║  Mean PI Width:             %6.3f arcsec/cy                 ║\n", coverage_stats$Mean_PI_width))
## ║  Mean PI Width:             37.011 arcsec/cy                 ║
cat("╠══════════════════════════════════════════════════════════════╣\n")
## ╠══════════════════════════════════════════════════════════════╣
cat("║  Method          RMSE        MAE         Bias                ║\n")
## ║  Method          RMSE        MAE         Bias                ║
cat("╠══════════════════════════════════════════════════════════════╣\n")
## ╠══════════════════════════════════════════════════════════════╣
cat(sprintf("║  Newton:       %7.3f    %7.3f    %+7.3f                 ║\n", 
            coverage_stats$RMSE_Newton, coverage_stats$MAE_Newton, coverage_stats$Bias_Newton))
## ║  Newton:        61.470     51.255    +43.872                 ║
cat(sprintf("║  GR:           %7.3f    %7.3f    %+7.3f                 ║\n", 
            coverage_stats$RMSE_GR, coverage_stats$MAE_GR, coverage_stats$Bias_GR))
## ║  GR:            43.065     37.246     +0.892                 ║
cat(sprintf("║  KPT:          %7.3f    %7.3f    %+7.3f                 ║\n", 
            coverage_stats$RMSE_KPT, coverage_stats$MAE_KPT, coverage_stats$Bias_KPT))
## ║  KPT:           43.440     37.701     +5.677                 ║
cat("╚══════════════════════════════════════════════════════════════╝\n")
## ╚══════════════════════════════════════════════════════════════╝
# 1. Static ggplot version
p1_static <- ggplot(df_plot, aes(x = t_date)) +
  # KPT prediction interval (background)
  geom_ribbon(aes(ymin = pi_low_kpt, ymax = pi_high_kpt),
              fill = colors$kpt, alpha = 0.15) +
  
  # Reference lines
  geom_hline(yintercept = A_GR, linetype = "dashed", 
             color = colors$gr, linewidth = 1.2) +
  geom_hline(yintercept = 0, linetype = "dotted", 
             color = colors$newton, linewidth = 1) +
  
  # KPT prediction line
  geom_line(aes(y = pred_kpt), color = colors$kpt, linewidth = 1.2) +
  
  # Observed values
  geom_line(aes(y = y_bar), color = colors$observed, linewidth = 0.8, alpha = 0.7) +
  geom_point(aes(y = y_bar), color = colors$observed, size = 2.5) +
  
  # Highlight points outside coverage

  geom_point(data = df_plot %>% filter(!KPT_coverage),
             aes(x = t_date, y = y_bar),
             color = colors$highlight, size = 4, shape = 21, 
             fill = "white", stroke = 2) +
  
  # Annotations
  annotate("text", x = min(df_plot$t_date) + 30, y = A_GR + 2,
           label = sprintf("GR = %.2f''", A_GR), 
           color = colors$gr, hjust = 0, fontface = "bold", size = 4) +
  annotate("text", x = min(df_plot$t_date) + 30, y = -3,
           label = "Newton = 0''", 
           color = colors$newton, hjust = 0, fontface = "bold", size = 4) +
  
  # Coverage annotation
  annotate("label", x = max(df_plot$t_date) - 100, y = max(df_plot$y_bar, na.rm = TRUE) + 3,
           label = sprintf("95%% PI Coverage: %.1f%%\nMean PI Width: %.2f''",
                           coverage_stats$KPT_95_coverage, coverage_stats$Mean_PI_width),
           fill = "white", alpha = 0.9, color = colors$kpt,
           hjust = 1, size = 3.5, fontface = "bold",
           label.padding = unit(0.5, "lines")) +
  
  # Theme
  labs(
    title = "Mercury Perihelion Anomaly: Comprehensive KPT Model Predictions",
    subtitle = "Physics-constrained kime-surface anchored to GR prediction with learned uncertainty",
    x = "Date (Window Center)",
    y = "Anomalous Precession (arcsec/century)",
    caption = sprintf("Black: Observed | Green: KPT (95%% PI) | Red dashed: GR (%.2f'') | Blue dotted: Newton (0'')\nOrange circles: Observations outside 95%% prediction interval", A_GR)
  ) +
  theme_minimal(base_size = 12) +
  theme(
    plot.title = element_text(hjust = 0.5, face = "bold", size = 14),
    plot.subtitle = element_text(hjust = 0.5, size = 11, color = "gray40"),
    plot.caption = element_text(hjust = 0.5, size = 9, color = "gray50"),
    panel.grid.minor = element_blank(),
    legend.position = "none"
  )

p1_static

# 2. Interactive plotly version
p1_interactive <- plot_ly(df_plot, x = ~t_date) %>%
  
  # KPT prediction interval
  add_ribbons(ymin = ~pi_low_kpt, ymax = ~pi_high_kpt,
              name = 'KPT 95% PI',
              fillcolor = colors$kpt_light,
              line = list(color = 'transparent'),
              hoverinfo = 'none') %>%
  
  # Newton baseline
  add_trace(y = rep(0, nrow(df_plot)),
            type = 'scatter', mode = 'lines',
            name = 'Newton (0)',
            line = list(color = colors$newton, dash = 'dot', width = 2),
            hoverinfo = 'name') %>%
  
  # GR reference
  add_trace(y = rep(A_GR, nrow(df_plot)),
            type = 'scatter', mode = 'lines',
            name = sprintf('GR (%.2f)', A_GR),
            line = list(color = colors$gr, dash = 'dash', width = 2),
            hoverinfo = 'name') %>%
  
  # KPT prediction
  add_trace(y = ~pred_kpt,
            type = 'scatter', mode = 'lines',
            name = 'KPT Prediction',
            line = list(color = colors$kpt, width = 2.5),
            hovertemplate = paste(
              "<b>KPT Prediction</b><br>",
              "Value: %{y:.3f} arcsec/cy<br>",
              "<extra></extra>"
            )) %>%
  
  # Observed values
  add_trace(y = ~y_bar,
            type = 'scatter', mode = 'lines+markers',
            name = 'Observed Mean',
            line = list(color = colors$observed, width = 1.5),
            marker = list(size = 8, color = colors$observed),
            hovertemplate = paste(
              "<b>Observed</b><br>",
              "Date: %{x}<br>",
              "Mean: %{y:.3f} arcsec/cy<br>",
              "KPT: %{customdata[0]:.3f}<br>",
              "95%% PI: [%{customdata[1]:.2f}, %{customdata[2]:.2f}]<br>",
              "Residual: %{customdata[3]:.3f}<br>",
              "<extra></extra>"
            ),
            customdata = ~cbind(pred_kpt, pi_low_kpt, pi_high_kpt, Residual_KPT)) %>%
  
  # Highlight non-coverage points
  add_trace(data = df_plot %>% filter(!KPT_coverage),
            x = ~t_date, y = ~y_bar,
            type = 'scatter', mode = 'markers',
            name = 'Outside 95% PI',
            marker = list(size = 12, color = 'white', 
                          line = list(color = colors$highlight, width = 3)),
            hovertemplate = "<b>Outside PI</b><br>Observed: %{y:.3f}<extra></extra>") %>%
  
  layout(
    title = list(
      text = "<b>Mercury Perihelion Anomaly: Comprehensive KPT Predictions</b><br><sup>Interactive visualization with range slider</sup>",
      font = list(size = 16)
    ),
    xaxis = list(
      title = "Date",
      tickformat = "%Y-%m",
      rangeslider = list(visible = TRUE, thickness = 0.05)
    ),
    yaxis = list(
      title = "Anomalous Precession (arcsec/century)",
      zeroline = TRUE,
      zerolinecolor = colors$grid
    ),
    hovermode = 'x unified',
    legend = list(
      orientation = "h",
      yanchor = "bottom",
      y = 1.02,
      xanchor = "center",
      x = 0.5
    ),
    margin = list(t = 100)
  )

p1_interactive

4.4.13.2 Physics-Constrained Kime-Surface Analysis

# ==============================================================================
# (B) KIME-SURFACE VISUALIZATION (Physics-Constrained)
# ==============================================================================

if (exists("fit_kpt") && !is.null(fit_kpt)) {
  
  cat("\n=== PHYSICS-CONSTRAINED KIME-SURFACE ANALYSIS ===\n")
  cat(sprintf("Anchor value (A_GR): %.2f arcsec/cy\n", fit_kpt$mu_0))
  cat(sprintf("Number of training windows: %d\n", length(fit_kpt$t)))
  cat(sprintf("Number of harmonics: %d\n", fit_kpt$n_harmonics))
  cat(sprintf("Learned noise σ: %.3f arcsec/cy\n", fit_kpt$sigma))
  cat(sprintf("Mean offset δ from GR: %.4f arcsec/cy\n", mean(fit_kpt$delta)))
  cat(sprintf("SD of offsets: %.4f arcsec/cy\n", sd(fit_kpt$delta)))
  
  # Extract data
  kime_data <- fit_kpt$Sgrid
  n_slices <- nrow(kime_data)
  n_theta <- ncol(kime_data)
  theta <- fit_kpt$theta
  
  # 1. Kime-Surface Slices with GR Reference
  n_slices_to_plot <- min(8, n_slices)
  slice_indices <- round(seq(1, n_slices, length.out = n_slices_to_plot))
  
  surf_df <- bind_rows(lapply(slice_indices, function(k) {
    tibble(
      theta = theta,
      S = kime_data[k, ],
      k_index = k,
      t_value = fit_kpt$t[k],
      delta_k = fit_kpt$delta[k],
      E_S_k = fit_kpt$E_S[k],
      label = sprintf("Window %d (δ=%.3f)", k, fit_kpt$delta[k])
    )
  }))
  
  p_slices <- ggplot(surf_df, aes(x = theta, y = S, group = label, color = delta_k)) +
    geom_hline(yintercept = fit_kpt$mu_0, linetype = "dashed", 
               color = colors$gr, linewidth = 1.2) +
    geom_line(linewidth = 1.1, alpha = 0.8) +
    scale_color_gradient2(low = "blue", mid = "gray50", high = "red", midpoint = 0,
                          name = expression(delta[k])) +
    scale_x_continuous(
      breaks = c(-pi, -pi/2, 0, pi/2, pi),
      labels = c(expression(-pi), expression(-pi/2), "0", 
                 expression(pi/2), expression(pi))
    ) +
    annotate("text", x = pi - 0.3, y = fit_kpt$mu_0 + 0.5,
             label = sprintf("GR = %.2f", fit_kpt$mu_0),
             color = colors$gr, hjust = 1, fontface = "bold") +
    labs(
      title = expression("Physics-Constrained Kime-Surface Slices " * S[k](theta)),
      subtitle = sprintf("Anchored to GR prediction; color indicates offset δ from anchor (mean δ = %.4f)", 
                         mean(fit_kpt$delta)),
      x = expression("Kime-Phase " * theta),
      y = "S (arcsec/century)"
    ) +
    theme_minimal(base_size = 12) +
    theme(
      plot.title = element_text(hjust = 0.5, face = "bold"),
      plot.subtitle = element_text(hjust = 0.5, color = "gray40"),
      legend.position = "right"
    )
  
  p_slices
  
  # 2. Interactive 3D Kime-Surface
  if (n_slices >= 10) {
    # Subsample for smooth 3D rendering
    slice_idx_3d <- round(seq(1, n_slices, length.out = min(50, n_slices)))
    theta_idx_3d <- round(seq(1, n_theta, length.out = min(100, n_theta)))
    
    z_matrix <- kime_data[slice_idx_3d, theta_idx_3d]
    x_vals <- theta[theta_idx_3d]
    y_vals <- slice_idx_3d
    
    p_3d <- plot_ly() %>%
      add_surface(
        z = z_matrix,
        x = x_vals,
        y = y_vals,
        colorscale = list(
          list(0, "blue"),
          list(0.5, "white"),
          list(1, "red")
        ),
        cmin = fit_kpt$mu_0 - 2,
        cmax = fit_kpt$mu_0 + 2,
        contours = list(
          z = list(show = TRUE, usecolormap = TRUE, 
                   highlightcolor = "#ff0000", project = list(z = TRUE))
        ),
        colorbar = list(title = "S(θ)")
      ) %>%
      # Add GR reference plane
      add_surface(
        z = matrix(fit_kpt$mu_0, nrow = length(y_vals), ncol = length(x_vals)),
        x = x_vals,
        y = y_vals,
        colorscale = list(list(0, colors$gr), list(1, colors$gr)),
        opacity = 0.3,
        showscale = FALSE,
        name = "GR Reference"
      ) %>%
      layout(
        title = list(
          text = "<b>3D Kime-Surface Visualization</b><br><sup>Red plane: GR reference level</sup>",
          font = list(size = 14)
        ),
        scene = list(
          xaxis = list(title = "θ (kime-phase)"),
          yaxis = list(title = "Window Index"),
          zaxis = list(title = "S(θ) arcsec/cy"),
          camera = list(eye = list(x = 1.8, y = 1.8, z = 1.2))
        )
      )
    
    p_3d
  }
  
  # 3. Heatmap with GR reference
  p_heatmap <- plot_ly() %>%
    add_heatmap(
      z = kime_data - fit_kpt$mu_0,  # Show deviation from GR
      x = theta,
      y = 1:n_slices,
      colorscale = list(
        list(0, "blue"),
        list(0.5, "white"),
        list(1, "red")
      ),
      zmid = 0,
      colorbar = list(title = "S(θ) - A_GR"),
      hovertemplate = paste(
        "Window: %{y}<br>",
        "θ: %{x:.2f}<br>",
        "Deviation from GR: %{z:.4f}<br>",
        "<extra></extra>"
      )
    ) %>%
    layout(
      title = list(
        text = "<b>Kime-Surface Deviation from GR</b><br><sup>Blue: below GR | White: at GR | Red: above GR</sup>",
        font = list(size = 14)
      ),
      xaxis = list(
        title = "θ (kime-phase)",
        tickvals = c(-pi, -pi/2, 0, pi/2, pi),
        ticktext = c("-π", "-π/2", "0", "π/2", "π")
      ),
      yaxis = list(title = "Window Index")
    )
  
  p_heatmap
  
  # 4. Expected Value E[S_k(θ)] vs GR
  es_df <- tibble(
    window = 1:length(fit_kpt$E_S),
    t = fit_kpt$t,
    E_S = fit_kpt$E_S,
    delta = fit_kpt$delta,
    deviation = E_S - fit_kpt$mu_0
  )
  
  p_expected <- plot_ly(es_df, x = ~window) %>%
    add_trace(
      y = rep(fit_kpt$mu_0, nrow(es_df)),
      type = 'scatter', mode = 'lines',
      name = sprintf('GR Reference (%.2f)', fit_kpt$mu_0),
      line = list(color = colors$gr, dash = 'dash', width = 2)
    ) %>%
    add_trace(
      y = ~E_S,
      type = 'scatter', mode = 'lines+markers',
      name = 'E[S_k(θ)]',
      line = list(color = colors$kpt, width = 2),
      marker = list(size = 6, color = colors$kpt),
      hovertemplate = paste(
        "Window: %{x}<br>",
        "E[S(θ)]: %{y:.4f}<br>",
        "Deviation: %{customdata:.4f}<br>",
        "<extra></extra>"
      ),
      customdata = ~deviation
    ) %>%
    layout(
      title = list(
        text = "<b>Physical Constraint Check: E[S_k(θ)] ≈ A_GR</b><br><sup>Expected kime-surface value should match GR prediction</sup>",
        font = list(size = 14)
      ),
      xaxis = list(title = "Training Window Index"),
      yaxis = list(
        title = "Expected Value (arcsec/cy)",
        range = c(fit_kpt$mu_0 - 1, fit_kpt$mu_0 + 1)
      ),
      shapes = list(
        list(
          type = "rect",
          x0 = 0, x1 = nrow(es_df),
          y0 = fit_kpt$mu_0 - 0.1, y1 = fit_kpt$mu_0 + 0.1,
          fillcolor = colors$kpt_light,
          line = list(width = 0),
          layer = "below"
        )
      ),
      annotations = list(
        list(
          x = nrow(es_df) * 0.5,
          y = fit_kpt$mu_0 + 0.8,
          text = sprintf("Mean E[S(θ)] = %.4f (target: %.2f)", 
                         mean(fit_kpt$E_S), fit_kpt$mu_0),
          showarrow = FALSE,
          font = list(size = 12, color = colors$kpt)
        )
      )
    )
  
  p_expected
}
## 
## === PHYSICS-CONSTRAINED KIME-SURFACE ANALYSIS ===
## Anchor value (A_GR): 42.98 arcsec/cy
## Number of training windows: 92
## Number of harmonics: 2
## Learned noise σ: 9.353 arcsec/cy
## Mean offset δ from GR: -2.0508 arcsec/cy
## SD of offsets: 26.0409 arcsec/cy

4.4.13.3 Learned Kime-Phase Distribution

# ==============================================================================
# (C) KIME-PHASE DISTRIBUTION φ(θ) VISUALIZATION
# ==============================================================================

if (exists("fit_kpt") && !is.null(fit_kpt)) {
  
  # Get the learned kime-phase distribution
  phi <- if (fit_kpt$pool_phi) {
    fit_kpt$varphi_theta[1, ]
  } else {
    colMeans(fit_kpt$varphi_theta)
  }
  
  # Normalize for display
  dtheta <- 2 * pi / length(phi)
  phi_normalized <- phi / (sum(phi) * dtheta)
  
  phi_df <- tibble(
    theta = fit_kpt$theta,
    phi = phi_normalized,
    phi_cumulative = cumsum(phi_normalized * dtheta)
  )
  
  # Calculate circular statistics
  # Circular mean
  sin_mean <- sum(phi_normalized * sin(fit_kpt$theta)) * dtheta
  cos_mean <- sum(phi_normalized * cos(fit_kpt$theta)) * dtheta
  circular_mean <- atan2(sin_mean, cos_mean)
  circular_concentration <- sqrt(sin_mean^2 + cos_mean^2)
  
  # Entropy (relative to uniform)
  uniform_phi <- 1 / (2 * pi)
  entropy <- -sum(phi_normalized * log(phi_normalized + 1e-10)) * dtheta
  max_entropy <- log(2 * pi)
  relative_entropy <- entropy / max_entropy
  
  cat("\n=== KIME-PHASE DISTRIBUTION ANALYSIS ===\n")
  cat(sprintf("Pooled distribution: %s\n", ifelse(fit_kpt$pool_phi, "Yes", "No")))
  cat(sprintf("Circular mean: %.4f radians (%.1f°)\n", circular_mean, circular_mean * 180 / pi))
  cat(sprintf("Concentration (R): %.4f (0=uniform, 1=concentrated)\n", circular_concentration))
  cat(sprintf("Relative entropy: %.3f (1=uniform)\n", relative_entropy))
  
  # 1. Density plot
  p_phi_density <- ggplot(phi_df, aes(x = theta, y = phi)) +
    geom_area(fill = colors$kpt, alpha = 0.3) +
    geom_line(color = colors$kpt, linewidth = 1.5) +
    geom_hline(yintercept = 1/(2*pi), linetype = "dashed", color = "gray50") +
    geom_vline(xintercept = circular_mean, linetype = "dotted", 
               color = colors$highlight, linewidth = 1) +
    scale_x_continuous(
      breaks = c(-pi, -pi/2, 0, pi/2, pi),
      labels = c(expression(-pi), expression(-pi/2), "0", 
                 expression(pi/2), expression(pi))
    ) +
    annotate("text", x = circular_mean + 0.3, y = max(phi_normalized) * 0.9,
             label = sprintf("Circular mean\n= %.2f rad", circular_mean),
             color = colors$highlight, hjust = 0, size = 3.5) +
    annotate("text", x = pi - 0.2, y = 1/(2*pi) + 0.02,
             label = "Uniform", color = "gray50", hjust = 1, size = 3) +
    labs(
      title = expression("Learned Kime-Phase Distribution " * varphi(theta)),
      subtitle = sprintf("Captures temporal uncertainty in measurements (Rel. Entropy = %.2f)", 
                         relative_entropy),
      x = expression("Kime-Phase " * theta),
      y = expression(varphi(theta))
    ) +
    theme_minimal(base_size = 12) +
    theme(
      plot.title = element_text(hjust = 0.5, face = "bold"),
      plot.subtitle = element_text(hjust = 0.5, color = "gray40")
    )
  
  p_phi_density
  
  # 2. Interactive polar plot
  p_phi_polar <- plot_ly(type = 'scatterpolar', mode = 'lines') %>%
    add_trace(
      r = c(phi_normalized, phi_normalized[1]),  # Close the loop
      theta = c(fit_kpt$theta * 180 / pi, fit_kpt$theta[1] * 180 / pi),
      fill = 'toself',
      fillcolor = colors$kpt_light,
      line = list(color = colors$kpt, width = 2),
      name = 'φ(θ)'
    ) %>%
    add_trace(
      r = rep(1/(2*pi), length(fit_kpt$theta) + 1),
      theta = c(fit_kpt$theta * 180 / pi, fit_kpt$theta[1] * 180 / pi),
      line = list(color = 'gray', dash = 'dash', width = 1),
      name = 'Uniform'
    ) %>%
    layout(
      title = list(
        text = "<b>Kime-Phase Distribution (Polar)</b>",
        font = list(size = 14)
      ),
      polar = list(
        radialaxis = list(
          visible = TRUE,
          range = c(0, max(phi_normalized) * 1.1)
        ),
        angularaxis = list(
          tickmode = "array",
          tickvals = c(-180, -90, 0, 90, 180),
          ticktext = c("-π", "-π/2", "0", "π/2", "π")
        )
      ),
      showlegend = TRUE,
      legend = list(x = 0.85, y = 0.95)
    )
  
  p_phi_polar
}
## 
## === KIME-PHASE DISTRIBUTION ANALYSIS ===
## Pooled distribution: Yes
## Circular mean: 2.2338 radians (128.0°)
## Concentration (R): 0.0006 (0=uniform, 1=concentrated)
## Relative entropy: 0.614 (1=uniform)

4.4.13.4 Offset δ Analysis

# ==============================================================================
# (D) OFFSET δ_k FROM GR ANALYSIS
# ==============================================================================

if (exists("fit_kpt") && !is.null(fit_kpt)) {
  
  delta_df <- tibble(
    window = 1:length(fit_kpt$delta),
    t = fit_kpt$t,
    t_date = as.Date("2000-01-01") + (fit_kpt$t - 2451545),
    delta = fit_kpt$delta
  )
  
  # Test if deltas are significantly different from zero
  t_test_delta <- t.test(fit_kpt$delta, mu = 0)
  
  cat("\n=== OFFSET δ FROM GR ANALYSIS ===\n")
  cat(sprintf("Mean δ: %.6f arcsec/cy\n", mean(fit_kpt$delta)))
  cat(sprintf("SD δ: %.6f arcsec/cy\n", sd(fit_kpt$delta)))
  cat(sprintf("Range: [%.6f, %.6f]\n", min(fit_kpt$delta), max(fit_kpt$delta)))
  cat(sprintf("t-test vs 0: t=%.3f, p=%.4f\n", t_test_delta$statistic, t_test_delta$p.value))
  cat(sprintf("Regularization λ_δ used: %.1f\n", fit_kpt$lambda_delta))
  
  # Time series of deltas
  p_delta_ts <- plot_ly(delta_df, x = ~t_date) %>%
    add_trace(
      y = ~delta,
      type = 'scatter', mode = 'lines+markers',
      name = 'δ_k',
      line = list(color = colors$highlight, width = 2),
      marker = list(size = 8, color = colors$highlight),
      hovertemplate = paste(
        "Date: %{x}<br>",
        "δ: %{y:.6f} arcsec/cy<br>",
        "<extra></extra>"
      )
    ) %>%
    add_trace(
      y = rep(0, nrow(delta_df)),
      type = 'scatter', mode = 'lines',
      name = 'Zero (GR)',
      line = list(color = 'gray', dash = 'dash', width = 1)
    ) %>%
    add_trace(
      y = rep(mean(fit_kpt$delta), nrow(delta_df)),
      type = 'scatter', mode = 'lines',
      name = sprintf('Mean δ = %.4f', mean(fit_kpt$delta)),
      line = list(color = colors$kpt, dash = 'dot', width = 2)
    ) %>%
    layout(
      title = list(
        text = sprintf("<b>Offset δ_k from GR Prediction Over Training Windows</b><br><sup>Regularization forces δ toward 0 (λ_δ = %.1f)</sup>", 
                       fit_kpt$lambda_delta),
        font = list(size = 14)
      ),
      xaxis = list(title = "Date"),
      yaxis = list(
        title = "δ (arcsec/century)",
        zeroline = TRUE
      ),
      shapes = list(
        list(
          type = "rect",
          x0 = min(delta_df$t_date), x1 = max(delta_df$t_date),
          y0 = -2*sd(fit_kpt$delta), y1 = 2*sd(fit_kpt$delta),
          fillcolor = "rgba(200,200,200,0.2)",
          line = list(width = 0),
          layer = "below"
        )
      ),
      annotations = list(
        list(
          x = max(delta_df$t_date),
          y = 2*sd(fit_kpt$delta),
          text = "±2σ band",
          showarrow = FALSE,
          xanchor = "right",
          font = list(size = 10, color = "gray50")
        )
      )
    )
  
  p_delta_ts
  
  # Histogram of deltas
  p_delta_hist <- ggplot(delta_df, aes(x = delta)) +
    geom_histogram(aes(y = after_stat(density)), bins = 20, 
                   fill = colors$highlight, alpha = 0.7, color = "white") +
    geom_density(color = colors$observed, linewidth = 1.2) +
    geom_vline(xintercept = 0, linetype = "dashed", color = colors$gr, linewidth = 1) +
    geom_vline(xintercept = mean(fit_kpt$delta), linetype = "solid", 
               color = colors$kpt, linewidth = 1) +
    annotate("text", x = 0, y = Inf, label = "GR (δ=0)", 
             color = colors$gr, vjust = 2, hjust = -0.1) +
    labs(
      title = expression("Distribution of Offsets " * delta[k]),
      subtitle = sprintf("Mean = %.4f, SD = %.4f (t-test p = %.3f)", 
                         mean(fit_kpt$delta), sd(fit_kpt$delta), t_test_delta$p.value),
      x = expression(delta[k] ~ "(arcsec/century)"),
      y = "Density"
    ) +
    theme_minimal(base_size = 12) +
    theme(
      plot.title = element_text(hjust = 0.5, face = "bold"),
      plot.subtitle = element_text(hjust = 0.5, color = "gray40")
    )
  
  p_delta_hist
}
## 
## === OFFSET δ FROM GR ANALYSIS ===
## Mean δ: -2.050818 arcsec/cy
## SD δ: 26.040934 arcsec/cy
## Range: [-54.383802, 47.389376]
## t-test vs 0: t=-0.755, p=0.4520
## Regularization λ_δ used: 10.0

4.4.13.5 Residual Diagnostics

# ==============================================================================
# (E) COMPREHENSIVE RESIDUAL ANALYSIS
# ==============================================================================

# 1. Residuals over time (all methods)
p_residuals_time <- plot_ly(df_plot, x = ~t_date) %>%
  add_trace(
    y = ~Residual_Newton,
    type = 'scatter', mode = 'lines+markers',
    name = sprintf('Newton (MAE=%.2f)', coverage_stats$MAE_Newton),
    line = list(color = colors$newton, width = 1.5),
    marker = list(size = 5, color = colors$newton),
    opacity = 0.7
  ) %>%
  add_trace(
    y = ~Residual_GR,
    type = 'scatter', mode = 'lines+markers',
    name = sprintf('GR (MAE=%.2f)', coverage_stats$MAE_GR),
    line = list(color = colors$gr, width = 1.5),
    marker = list(size = 5, color = colors$gr),
    opacity = 0.7
  ) %>%
  add_trace(
    y = ~Residual_KPT,
    type = 'scatter', mode = 'lines+markers',
    name = sprintf('KPT (MAE=%.2f)', coverage_stats$MAE_KPT),
    line = list(color = colors$kpt, width = 2),
    marker = list(size = 6, color = colors$kpt)
  ) %>%
  layout(
    title = list(
      text = "<b>Prediction Residuals Over Time</b><br><sup>Observed - Predicted</sup>",
      font = list(size = 14)
    ),
    xaxis = list(
      title = "Date",
      rangeslider = list(visible = TRUE, thickness = 0.05)
    ),
    yaxis = list(title = "Residual (arcsec/century)"),
    hovermode = 'x unified',
    shapes = list(
      list(
        type = "line",
        x0 = 0, x1 = 1, xref = "paper",
        y0 = 0, y1 = 0,
        line = list(color = "gray", dash = "dash", width = 1)
      )
    )
  )

p_residuals_time
# 2. Residual boxplots
residuals_long <- df_plot %>%
  dplyr::select(t_date, Residual_Newton, Residual_GR, Residual_KPT) %>%
  pivot_longer(
    cols = starts_with("Residual"),
    names_to = "Method",
    values_to = "Residual",
    names_prefix = "Residual_"
  ) %>%
  mutate(Method = factor(Method, levels = c("Newton", "GR", "KPT")))

# p_residual_box <- ggplot(residuals_long, aes(x = Method, y = Residual, fill = Method)) +
#   geom_hline(yintercept = 0, linetype = "dashed", color = "gray50") +
#   geom_boxplot(alpha = 0.7, outlier.shape = 21) +
#   geom_jitter(width = 0.2, alpha = 0.3, size = 1.5) +
#   scale_fill_manual(values = c("Newton" = colors$newton, 
#                                 "GR" = colors$gr, 
#                                 "KPT" = colors$kpt)) +
#   stat_summary(fun = mean, geom = "point", shape = 23, size = 4, 
#                fill = "white", color = "black") +
#   labs(
#     title = "Residual Distributions by Method",
#     subtitle = "Diamond = mean; Box = IQR; Points = individual windows",
#     x = "",
#     y = "Residual (arcsec/century)"
#   ) +
#   theme_minimal(base_size = 12) +
#   theme(
#     plot.title = element_text(hjust = 0.5, face = "bold"),
#     legend.position = "none"
#   )
# 
# p_residual_box
# 1. Compute mean residuals by method
mean_residuals <- aggregate(Residual ~ Method, data = residuals_long, FUN = mean)

# 2. Define color mapping (ensure it matches your 'colors' object)
method_colors <- c(
  "Newton" = colors$newton,
  "GR"     = colors$gr,
  "KPT"    = colors$kpt
)

# 3. Build interactive plot
p_residual_box_interactive <- plot_ly() %>%
  
  # --- Jittered points (individual residuals) ---
  add_trace(
    data = residuals_long,
    x = ~Method,
    y = ~Residual,
    type = "scatter",
    mode = "markers",
    marker = list(
      color = ~Method,
      colors = method_colors,
      size = 6,
      opacity = 0.3,
      symbol = "circle"
    ),
    hoverinfo = "text",
    text = ~paste("Method:", Method, "<br>Residual:", round(Residual, 4)),
    name = "Residuals",
    showlegend = FALSE
  ) %>%
  
  # --- Boxplots (IQR, median, whiskers) ---
  add_trace(
    data = residuals_long,
    x = ~Method,
    y = ~Residual,
    type = "box",
    boxpoints = FALSE,  # hide default points (we already have jitter)
    marker = list(color = "black"),
    line = list(color = "black"),
    fillcolor = ~Method,
    colors = method_colors,
    opacity = 0.7,
    name = "Boxplot",
    showlegend = FALSE
  ) %>%
  
  # --- Mean markers (diamonds) ---
  add_trace(
    data = mean_residuals,
    x = ~Method,
    y = ~Residual,
    type = "scatter",
    mode = "markers",
    marker = list(
      symbol = "diamond",   # diamond shape
      size = 10,
      color = "black",
      line = list(color = "black", width = 1),
      fillcolor = "white"
    ),
    hoverinfo = "text",
    text = ~paste("Mean residual (", Method, "): ", round(Residual, 4)),
    name = "Mean",
    showlegend = TRUE
  ) %>%
  
  # --- Layout with reference line and styling ---
  layout(
    title = list(
      text = "Residual Distributions by Method",
      x = 0.5,
      font = list(size = 16, family = "Arial")
    ),
    xaxis = list(
      title = "",
      showgrid = FALSE
    ),
    yaxis = list(
      title = "Residual (arcsec/century)",
      zeroline = FALSE
    ),
    shapes = list(
      # Horizontal dashed line at y = 0
      list(
        type = "line",
        x0 = 0, x1 = 1,
        xref = "paper",
        y0 = 0, y1 = 0,
        line = list(color = "gray", dash = "dash", width = 1)
      )
    ),
    annotations = list(
      list(
        x = 0.5, y = -0.1,
        xref = "paper", yref = "paper",
        xanchor = "center", yanchor = "top",
        text = "Diamond = mean; Box = IQR; Points = individual windows",
        showarrow = FALSE,
        font = list(size = 12, color = "gray50")
      )
    ),
    showlegend = FALSE,  # matches theme(legend.position = "none")
    hovermode = "closest"
  )

p_residual_box_interactive
# 3. Q-Q plots
qq_data <- residuals_long %>%
  group_by(Method) %>%
  arrange(Residual) %>%
  mutate(
    theoretical = qnorm(ppoints(n())),
    sample = Residual
  ) %>%
  ungroup()

# Fit lines for each method
qq_fits <- qq_data %>%
  group_by(Method) %>%
  summarise(
    slope = sd(sample, na.rm = TRUE),
    intercept = mean(sample, na.rm = TRUE)
  )

# p_qq <- ggplot(qq_data, aes(x = theoretical, y = sample, color = Method)) +
#   geom_abline(slope = 1, intercept = 0, linetype = "dashed", color = "gray50") +
#   geom_point(size = 2.5, alpha = 0.7) +
#   geom_smooth(method = "lm", se = FALSE, linewidth = 1) +
#   scale_color_manual(values = c("Newton" = colors$newton, 
#                                  "GR" = colors$gr, 
#                                  "KPT" = colors$kpt)) +
#   facet_wrap(~Method, scales = "free_y") +
#   labs(
#     title = "Q-Q Plots: Residual Normality Assessment",
#     subtitle = "Points should follow diagonal line if normally distributed",
#     x = "Theoretical Quantiles (Normal)",
#     y = "Sample Quantiles"
#   ) +
#   theme_minimal(base_size = 12) +
#   theme(
#     plot.title = element_text(hjust = 0.5, face = "bold"),
#     legend.position = "none",
#     strip.text = element_text(face = "bold", size = 11)
#   )
# 
# p_qq
# Ensure colors are defined
method_colors <- c(
  "Newton" = colors$newton,
  "GR"     = colors$gr,
  "KPT"    = colors$kpt
)

# Split data by method
qq_list <- split(qq_data, qq_data$Method)

# Function to create one Q-Q subplot
make_qq_plot <- function(df, method) {
  col <- method_colors[method]
  
  # Fit linear model: sample ~ theoretical
  lm_fit <- lm(sample ~ theoretical, data = df)
  df$fit <- predict(lm_fit)
  
  # Sort for smooth line
  df_sorted <- df[order(df$theoretical), ]
  
  plot_ly() %>%
    # Diagonal reference (y = x)
    add_lines(
      x = range(df$theoretical),
      y = range(df$theoretical),
      line = list(color = "gray50", dash = "dash"),
      showlegend = FALSE,
      hoverinfo = "skip"
    ) %>%
    # Sample points
    add_markers(
      data = df,
      x = ~theoretical,
      y = ~sample,
      color = I(col),
      size = 6,
      opacity = 0.7,
      hoverinfo = "text",
      text = ~paste("Theoretical:", round(theoretical, 3), "<br>Sample:", round(sample, 3)),
      showlegend = FALSE
    ) %>%
    # Regression line (not y=x, but best-fit)
    add_lines(
      data = df_sorted,
      x = ~theoretical,
      y = ~fit,
      color = I(col),
      width = 2,
      hoverinfo = "skip",
      showlegend = FALSE
    ) %>%
    layout(
      title = method,
      xaxis = list(title = "Theoretical Quantiles (Normal)"),
      yaxis = list(title = "Sample Quantiles"),
      margin = list(b = 40, t = 40, l = 60, r = 20)
    )
}

# Generate list of plots
qq_plots <- lapply(names(qq_list), function(m) {
  make_qq_plot(qq_list[[m]], m)
})

# Combine into a single row (1x3 grid)
p_qq_interactive <- subplot(
  qq_plots[[1]], qq_plots[[2]], qq_plots[[3]],
  nrows = 1, shareX = TRUE, shareY = FALSE,
  titleX = TRUE, titleY = TRUE
) %>%
  layout(
    title = list(
      text = "Q-Q Plots: Residual Normality Assessment",
      x = 0.5,
      font = list(size = 18, family = "Arial")
    ),
    annotations = list(
      list(
        x = 0.5, y = -0.1,
        xref = "paper", yref = "paper",
        text = "Points should follow diagonal line if normally distributed",
        showarrow = FALSE,
        font = list(size = 13, color = "gray50")
      )
    )
  )

p_qq_interactive
# 4. Standardized residuals
p_std_resid <- plot_ly(df_plot, x = ~window) %>%
  add_trace(
    y = ~Std_Residual_KPT,
    type = 'scatter', mode = 'markers',
    name = 'KPT Std. Residual',
    marker = list(
      size = 10,
      color = ~Std_Residual_KPT,
      colorscale = list(list(0, "blue"), list(0.5, "white"), list(1, "red")),
      cmin = -3, cmax = 3,
      colorbar = list(title = "Std. Resid."),
      line = list(color = 'black', width = 1)
    ),
    hovertemplate = paste(
      "Window: %{x}<br>",
      "Std. Residual: %{y:.2f}<br>",
      "<extra></extra>"
    )
  ) %>%
  layout(
    title = list(
      text = "<b>Standardized KPT Residuals</b><br><sup>Values beyond ±2 indicate potential outliers</sup>",
      font = list(size = 14)
    ),
    xaxis = list(title = "Window Index"),
    yaxis = list(
      title = "Standardized Residual",
      zeroline = TRUE
    ),
    shapes = list(
      list(type = "line", x0 = 0, x1 = max(df_plot$window),
           y0 = 2, y1 = 2, line = list(color = "red", dash = "dash")),
      list(type = "line", x0 = 0, x1 = max(df_plot$window),
           y0 = -2, y1 = -2, line = list(color = "red", dash = "dash"))
    )
  )

p_std_resid

4.4.13.6 Model Convergence and Diagnostics

# ==============================================================================
# (F) MODEL CONVERGENCE AND FITTING DIAGNOSTICS
# ==============================================================================

if (exists("fit_kpt") && !is.null(fit_kpt)) {
  
  # 1. Log-likelihood convergence
  conv_df <- tibble(
    iteration = 1:length(fit_kpt$log_lik_history),
    log_lik = fit_kpt$log_lik_history,
    delta_param = fit_kpt$delta_history
  )
  
  p_convergence <- plot_ly(conv_df) %>%
    add_trace(
      x = ~iteration, y = ~log_lik,
      type = 'scatter', mode = 'lines+markers',
      name = 'Log-Likelihood',
      yaxis = 'y1',
      line = list(color = colors$kpt, width = 2),
      marker = list(size = 6, color = colors$kpt)
    ) %>%
    add_trace(
      x = ~iteration, y = ~delta_param,
      type = 'scatter', mode = 'lines+markers',
      name = 'Δ Parameters',
      yaxis = 'y2',
      line = list(color = colors$highlight, width = 2),
      marker = list(size = 6, color = colors$highlight)
    ) %>%
    layout(
      title = list(
        text = "<b>KPT-GEM Convergence Diagnostics</b>",
        font = list(size = 14)
      ),
      xaxis = list(title = "Iteration"),
      yaxis = list(
        title = "Log-Likelihood",
        titlefont = list(color = colors$kpt),
        tickfont = list(color = colors$kpt)
      ),
      yaxis2 = list(
        title = "Max |Δ Parameters|",
        titlefont = list(color = colors$highlight),
        tickfont = list(color = colors$highlight),
        overlaying = "y",
        side = "right",
        type = "log"
      ),
      legend = list(x = 0.7, y = 0.95)
    )
  
  p_convergence
  
  # 2. Summary statistics panel
  cat("\n")
  cat("╔══════════════════════════════════════════════════════════════╗\n")
  cat("║              KPT-GEM MODEL FITTING SUMMARY                   ║\n")
  cat("╠══════════════════════════════════════════════════════════════╣\n")
  cat(sprintf("║  Iterations to convergence:  %3d                             ║\n", 
              length(fit_kpt$log_lik_history)))
  cat(sprintf("║  Final log-likelihood:       %10.2f                      ║\n", 
              tail(fit_kpt$log_lik_history, 1)))
  cat(sprintf("║  Final Δ parameters:         %10.2e                      ║\n", 
              tail(fit_kpt$delta_history, 1)))
  cat("╠══════════════════════════════════════════════════════════════╣\n")
  cat(sprintf("║  Physics anchor (A_GR):      %7.2f arcsec/cy              ║\n", fit_kpt$mu_0))
  cat(sprintf("║  Learned noise σ:            %7.3f arcsec/cy              ║\n", fit_kpt$sigma))
  cat(sprintf("║  Number of harmonics:        %3d                             ║\n", fit_kpt$n_harmonics))
  cat(sprintf("║  λ_δ (offset reg.):          %6.1f                          ║\n", fit_kpt$lambda_delta))
  cat(sprintf("║  λ_coef (Fourier reg.):      %6.1f                          ║\n", fit_kpt$lambda_coef))
  cat(sprintf("║  Pooled φ(θ):                %s                            ║\n", 
              ifelse(fit_kpt$pool_phi, "Yes", "No ")))
  cat("╚══════════════════════════════════════════════════════════════╝\n")
}
## 
## ╔══════════════════════════════════════════════════════════════╗
## ║              KPT-GEM MODEL FITTING SUMMARY                   ║
## ╠══════════════════════════════════════════════════════════════╣
## ║  Iterations to convergence:   50                             ║
## ║  Final log-likelihood:         -3733.79                      ║
## ║  Final Δ parameters:           8.48e-01                      ║
## ╠══════════════════════════════════════════════════════════════╣
## ║  Physics anchor (A_GR):        42.98 arcsec/cy              ║
## ║  Learned noise σ:              9.353 arcsec/cy              ║
## ║  Number of harmonics:          2                             ║
## ║  λ_δ (offset reg.):            10.0                          ║
## ║  λ_coef (Fourier reg.):         5.0                          ║
## ║  Pooled φ(θ):                Yes                            ║
## ╚══════════════════════════════════════════════════════════════╝

4.4.13.7 Final Summary Table

# ==============================================================================
# (G) COMPREHENSIVE RESULTS TABLE
# ==============================================================================

# Compute all metrics
final_metrics <- df_plot %>%
  summarise(
    # Newton
    Newton_Point = 0,
    Newton_RMSE = sqrt(mean(Residual_Newton^2, na.rm = TRUE)),
    Newton_MAE = mean(abs(Residual_Newton), na.rm = TRUE),
    Newton_Bias = mean(Residual_Newton, na.rm = TRUE),
    
    # GR
    GR_Point = A_GR,
    GR_RMSE = sqrt(mean(Residual_GR^2, na.rm = TRUE)),
    GR_MAE = mean(abs(Residual_GR), na.rm = TRUE),
    GR_Bias = mean(Residual_GR, na.rm = TRUE),
    
    # KPT
    KPT_Point_Mean = mean(pred_kpt, na.rm = TRUE),
    KPT_Point_SD = sd(pred_kpt, na.rm = TRUE),
    KPT_RMSE = sqrt(mean(Residual_KPT^2, na.rm = TRUE)),
    KPT_MAE = mean(abs(Residual_KPT), na.rm = TRUE),
    KPT_Bias = mean(Residual_KPT, na.rm = TRUE),
    KPT_Coverage = mean(KPT_coverage, na.rm = TRUE) * 100,
    KPT_PI_Width = mean(PI_width, na.rm = TRUE)
  )

# Build formatted table
summary_table <- tibble(
  Method = c("Newtonian (no anomaly)", "General Relativity", "KPT (Comprehensive GEM)"),
  
  `Point Prediction (arcsec/cy)` = c(
    "0.00",
    sprintf("%.2f", A_GR),
    sprintf("%.2f ± %.2f", final_metrics$KPT_Point_Mean, final_metrics$KPT_Point_SD)
  ),
  
  `95% PI` = c(
    "—",
    "—",
    sprintf("[%.2f, %.2f]", 
            mean(df_plot$pi_low_kpt, na.rm = TRUE),
            mean(df_plot$pi_high_kpt, na.rm = TRUE))
  ),
  
  `RMSE` = c(
    sprintf("%.3f", final_metrics$Newton_RMSE),
    sprintf("%.3f", final_metrics$GR_RMSE),
    sprintf("%.3f", final_metrics$KPT_RMSE)
  ),
  
  `MAE` = c(
    sprintf("%.3f", final_metrics$Newton_MAE),
    sprintf("%.3f", final_metrics$GR_MAE),
    sprintf("%.3f", final_metrics$KPT_MAE)
  ),
  
  `Bias` = c(
    sprintf("%+.3f", final_metrics$Newton_Bias),
    sprintf("%+.3f", final_metrics$GR_Bias),
    sprintf("%+.3f", final_metrics$KPT_Bias)
  ),
  
  `95% Coverage` = c(
    "—",
    "—",
    sprintf("%.1f%%", final_metrics$KPT_Coverage)
  ),
  
  `Mean PI Width` = c(
    "—",
    "—",
    sprintf("%.2f", final_metrics$KPT_PI_Width)
  )
)

# Display with kableExtra
cat("\n")
cat("═══════════════════════════════════════════════════════════════════════════════\n")
## ═══════════════════════════════════════════════════════════════════════════════
cat("            MERCURY PERIHELION ANOMALY: FINAL MODEL COMPARISON                 \n")
##             MERCURY PERIHELION ANOMALY: FINAL MODEL COMPARISON
cat("═══════════════════════════════════════════════════════════════════════════════\n\n")
## ═══════════════════════════════════════════════════════════════════════════════
kable(summary_table, 
      format = "html",
      align = c('l', 'c', 'c', 'c', 'c', 'c', 'c', 'c'),
      escape = FALSE) %>%
  kable_styling(
    bootstrap_options = c("striped", "hover", "condensed"),
    full_width = FALSE,
    position = "center"
  ) %>%
  row_spec(0, bold = TRUE, background = "#2C3E50", color = "white") %>%
  row_spec(1, background = "#EBF5FB") %>%
  row_spec(2, background = "#FDEDEC") %>%
  row_spec(3, background = "#E9F7EF", bold = TRUE) %>%
  column_spec(1, bold = TRUE) %>%
  add_header_above(c(" " = 1, "Prediction" = 2, "Error Metrics" = 3, "Uncertainty" = 2))
Prediction
Error Metrics
Uncertainty
Method Point Prediction (arcsec/cy) 95% PI RMSE MAE Bias 95% Coverage Mean PI Width
Newtonian (no anomaly) 0.00 61.470 51.255 +43.872
General Relativity 42.98 43.065 37.246 +0.892
KPT (Comprehensive GEM) 38.20 ± 0.10 [19.61, 56.62] 43.440 37.701 +5.677 16.7% 37.01

4.4.13.8 Save Outputs

# ==============================================================================
# (H) SAVE VISUALIZATION DATA AND EXPORTS
# ==============================================================================

# Save comprehensive results
results_export <- list(
  # Evaluation data
  eval_df = df_plot,
  
  # KPT model fit
  kpt_fit = if (exists("fit_kpt")) fit_kpt else NULL,
  
  # Summary statistics
  coverage_stats = coverage_stats,
  final_metrics = final_metrics,
  
  # Model parameters
  A_GR = A_GR,
  
  # Metadata
  timestamp = Sys.time(),
  n_test_windows = nrow(df_plot)
)

# Optionally save to file
# saveRDS(results_export, "kpt_comprehensive_results.rds")

cat("\n=== VISUALIZATION COMPLETE ===\n")
## 
## === VISUALIZATION COMPLETE ===
cat(sprintf("Processed %d test windows\n", nrow(df_plot)))
## Processed 24 test windows
cat(sprintf("KPT 95%% PI Coverage: %.1f%%\n", final_metrics$KPT_Coverage))
## KPT 95% PI Coverage: 16.7%
cat(sprintf("KPT vs GR RMSE improvement: %.1f%%\n", 
            (final_metrics$GR_RMSE - final_metrics$KPT_RMSE) / final_metrics$GR_RMSE * 100))
## KPT vs GR RMSE improvement: -0.9%

4.4.14 Interpretation

Next, we explicate what constitutes a KPT win in this experiment:

  • GR should remain best for the mean anomaly (near ~43 arcsec/century).
  • KPT can improve the prediction problem if:
    • its predictive intervals are better calibrated (coverage),
    • it yields improved MLPD / CRPS on held-out windows,
    • it robustly models heavy tails / time-varying uncertainty (encoded through \(\varphi(\theta)\) and \(S(t,\theta)\)).

If KPT is not better than GR on scoring rules, that’s still a meaningful result: the anomaly behaves nearly constant in this ephemeris-derived series, and the simplest model is best.

5 Open Mathematical Problems

While spacekime representation offers novel insights, it also introduces several mathematical challenges that need to be addressed for the framework to be fully developed and applied effectively. Below is a summary of these challenges, along with the pros and cons of using complex time representation in mathematical terms.

5.1 Analyticity of Induced Kimesurfaces

Challenge: Determining the conditions under which kimesurfaces (surfaces in spacekime formed by complex time trajectories) are analytic functions is crucial for ensuring that the mathematical descriptions are well-behaved and that techniques from complex analysis can be applied.

  • Mathematical Implications:
    • Holomorphic Functions: Requires that the functions defining kimesurfaces are holomorphic (complex differentiable) in their domain.
    • Cauchy-Riemann Equations: Must satisfy these equations to ensure analyticity, which can be challenging when dealing with complex time-dependent phenomena.
    • Singularities and Branch Cuts: Identifying and handling singularities that may arise on kimesurfaces is essential to prevent undefined behavior.
  • Consequences:
    • Mathematical Rigor: Without ensuring analyticity, the mathematical framework may lack rigor, leading to potential inconsistencies.
    • Application of Theorems: Many powerful theorems in complex analysis (e.g., Cauchy’s integral theorem) require analyticity, limiting the tools available if this condition is not met.

5.2 Space-Kime Ergodicity

Challenge: Extending the concept of ergodicity to spacekime involves determining whether time averages over complex time trajectories are equivalent to ensemble averages across the spacekime manifold.

  • Mathematical Implications:
    • Definition of Ergodicity in Complex Time: Requires redefining ergodicity to account for the complex nature of time.
    • Invariant Measures: Identifying appropriate invariant measures on kimesurfaces to ensure that statistical properties are consistent over time and across the ensemble.
    • Mixing Properties: Analyzing whether the system exhibits mixing (strong or weak), which is necessary for ergodicity.
  • Consequences:
    • Statistical Mechanics Foundations: Ergodicity is foundational in statistical mechanics; lack thereof in spacekime can hinder the development of statistical descriptions of complex time systems.
    • Predictability: Non-ergodic systems may exhibit unpredictable behavior, complicating modeling efforts.

5.3 Measurability and Observability of the Enigmatic Kime-Phase

Challenge: The kime-phase \(\phi = \omega t\) (with \(\omega\) being a characteristic frequency) becomes complex in this framework. Determining how to measure and observe this complex phase in physical systems is non-trivial.

  • Mathematical Implications:
    • Definition of Observables: Need to define what constitutes an observable in spacekime, especially when traditional measurement operators may not apply.
    • Complex Probability Distributions: Handling probability distributions over complex variables introduces mathematical complexities, as standard probability theory is based on real-valued variables.
  • Consequences:
    • Experimental Validation: Without clear methods to measure the kime-phase, it’s challenging to validate the theoretical models experimentally.
    • Interpretation of Results: The physical meaning of measurements associated with the imaginary component of time is ambiguous, complicating the interpretation.

5.4 Causality

Challenge: Maintaining causality in systems where time is complex is essential, as causality is a cornerstone of physical laws. Complex time can introduce ambiguities in the sequence of events.

  • Mathematical Implications:
    • Time Ordering: Defining a consistent time-ordering when time has both real and imaginary components.
    • Signal Propagation: Ensuring that signals or influences do not propagate backward in the real-time component.
    • Analytic Continuation: Using analytic continuation carefully to avoid violating causality, especially in solutions to differential equations.
  • Consequences:
    • Physical Consistency: Violations of causality can lead to paradoxes and conflict with established physical theories.
    • Mathematical Models: Models must be constructed to inherently respect causality, which may limit the forms they can take.

5.5 Consistency Between Observed Sample Estimates and Theoretical Spacekime Models

Challenge: Aligning empirical data with theoretical predictions from spacekime models requires that the models accurately reflect the underlying processes generating the data.

  • Mathematical Implications:
    • Statistical Estimation: Developing estimators that can handle complex-valued data and provide unbiased, consistent estimates.
    • Model Identification: Determining whether the models are identifiable from data, especially when the imaginary component may not be directly observable.
    • Parameter Estimation: Estimating parameters in the presence of complex noise and ensuring convergence of estimation algorithms.
  • Consequences:
    • Model Validation: Difficulty in validating models may hinder their acceptance and application.
    • Predictive Accuracy: Inconsistencies can lead to poor predictive performance, limiting the utility of the models.

5.6 Pros and Cons of the Complex Time Representation

5.6.1 Pros

  1. Richer Mathematical Framework:
    • Advantage: Complex time allows for the use of complex analysis, providing a richer set of mathematical tools (e.g., contour integration, residue calculus) to solve problems.
    • Mathematical Benefit: Can potentially simplify solutions to differential equations and facilitate the use of integral transforms (e.g., Laplace and Fourier transforms in complex domains).
  2. Modeling Oscillatory and Wave Phenomena:
    • Advantage: Naturally incorporates oscillations and wave-like behavior, as complex exponentials can represent sinusoidal functions.
    • Mathematical Benefit: Facilitates the analysis of systems where phase and frequency are crucial, such as quantum mechanics and signal processing.
  3. Handling Uncertainty and Fluctuations:
    • Advantage: The imaginary component can model fluctuations, damping, or growth phenomena not captured by real time alone.
    • Mathematical Benefit: Provides a framework for incorporating stochastic processes and random perturbations in time evolution.

5.6.2 Cons

  1. Mathematical Complexity and Analytical Challenges:
    • Disadvantage: Increases the complexity of mathematical models, making them more difficult to analyze and solve.
    • Mathematical Drawback: Complex differential equations and integrals may not have closed-form solutions and may require advanced numerical methods.
  2. Ambiguity in Physical Interpretation:
    • Disadvantage: The physical meaning of the imaginary component of time is not clear, leading to potential confusion or misinterpretation.
    • Mathematical Drawback: Mapping mathematical results back to physical phenomena can be challenging, especially when dealing with non-observable quantities.
  3. Potential Violations of Fundamental Principles:
    • Disadvantage: Risk of violating causality and other fundamental physical laws if not carefully constrained.
    • Mathematical Drawback: Requires additional constraints or conditions to ensure that solutions are physically meaningful.
  4. Measurement and Validation Difficulties:
    • Disadvantage: Difficulty in measuring or observing the imaginary component of time makes experimental validation problematic.
    • Mathematical Drawback: Limits the ability to test and refine models based on empirical data.
  5. Challenges in Statistical Mechanics and Thermodynamics:
    • Disadvantage: Traditional statistical mechanics frameworks are based on real time; extending these to complex time is non-trivial.
    • Mathematical Drawback: May require rederivation of foundational results, such as fluctuation-dissipation theorems, in the context of complex time.

The complex time representation introduces a novel mathematical framework with the potential to provide deeper insights into systems exhibiting complex temporal behavior. However, several open mathematical challenges must be addressed:

  • Analyticity of Kimesurfaces: Ensuring that induced kimesurfaces are analytic is crucial for applying complex analysis tools effectively.
  • Space-Kime Ergodicity: Defining and establishing ergodicity in spacekime is necessary for the validity of statistical methods.
  • Measurability and Observability of the Kime-Phase: Developing methods to measure or infer the kime-phase is essential for connecting theory with experiment.
  • Causality: Maintaining causality within complex time frameworks is imperative to ensure physical consistency.
  • Consistency with Observations: Aligning theoretical models with empirical data requires robust statistical and mathematical techniques.

Pros of the complex time representation include a richer mathematical framework, better modeling of oscillatory phenomena, and the ability to handle uncertainties. Cons involve increased mathematical complexity, ambiguities in physical interpretation, potential violations of fundamental principles, and difficulties in measurement and validation.

5.7 Future R&D Directions

To address these challenges, future research in the following directions may be fruitful.

  • Developing Mathematical Methods: Creating new mathematical tools or adapting existing ones to handle complex time variables, ensuring analyticity, and preserving causality.
  • Experimental Techniques: Innovating methods to measure or infer the imaginary component of time or its effects indirectly through observable quantities.
  • Theoretical Refinements: Refining the theoretical framework to clarify the physical interpretation of complex time and ensure consistency with established physical laws.
  • Numerical Simulations: Employing computational methods to simulate complex time systems, providing insights that may not be accessible analytically.
  • See also previous section in TCIU Chapter 3 on Radon-Nikodym Derivatives, Kimemeasures, and Kime Operator.

6 References

  • Einstein, A. (1915). Explanation of the perihelion motion of Mercury from the general theory of relativity.
  • Misner, C. W., Thorne, K. S., & Wheeler, J. A. (1973). Gravitation. W.H. Freeman and Company.
SOCR Resource Visitor number Web Analytics SOCR Email