RMM

RMM#

import io
from datetime import datetime
import numpy as np
import pandas as pd
import plotly.graph_objects as go
import plotly.io as pio

# Ensure figures embed in static HTML builds (e.g., Jupyter Book)
pio.renderers.default = "plotly_mimetype"
pio.templates.default = "plotly_white"
def load_rmm_from_file(path: str) -> pd.DataFrame:
    """
    Parse a local BoM RMM text file.

    Returns DataFrame with ['rmm1','rmm2','phase','amplitude'] indexed by datetime.
    Skips headers and drops missing (1.E36 or 999).
    """
    rows = []
    with open(path, "r", encoding="utf-8", errors="ignore") as f:
        for raw in f:
            line = raw.strip()
            if not line:
                continue
            c0 = line[0]
            # Keep only lines beginning with a year (numeric or leading minus)
            if not (c0.isdigit() or (c0 == "-" and len(line) > 1 and line[1].isdigit())):
                continue
            parts = line.split()
            if len(parts) < 7:
                continue
            try:
                y = int(parts[0]); m = int(parts[1]); d = int(parts[2])
                r1 = float(parts[3]); r2 = float(parts[4]); ph = int(parts[5]); amp = float(parts[6])
            except ValueError:
                continue
            # Drop missing per header: 1.E36 or 999
            if any(abs(v) >= 1e35 for v in (r1, r2, amp)) or any(v == 999 for v in (r1, r2, amp)):
                continue
            rows.append({
                "date": datetime(y, m, d),
                "rmm1": r1, "rmm2": r2, "phase": ph, "amplitude": amp
            })
    if not rows:
        raise ValueError("No RMM rows parsed from file.")
    return pd.DataFrame(rows).set_index("date").sort_index()


def plot_rmm_phase_space(df: pd.DataFrame, *, highlight_days: int = 60) -> go.Figure:
    if df.empty:
        raise ValueError("Empty RMM DataFrame provided.")

    fig = go.Figure()

    # Full track (muted)
    fig.add_trace(go.Scatter(
        x=df["rmm1"], y=df["rmm2"],
        mode="lines", line=dict(color="rgba(150,150,150,0.35)", width=1),
        name="Full track",
    ))

    # Highlight recent segment
    cutoff = df.index.max() - pd.Timedelta(days=highlight_days)
    recent = df[df.index >= cutoff]
    if len(recent) > 1:
        t = (recent.index - recent.index.min()).days.astype(float)
        fig.add_trace(go.Scatter(
            x=recent["rmm1"], y=recent["rmm2"],
            mode="lines+markers",
            line=dict(color="#1f77b4", width=2),
            marker=dict(size=6, color=t, colorscale="Viridis", showscale=True,
                        colorbar=dict(title="Days from start")),
            name=f"Last {highlight_days} days",
        ))

    # Unit circle (amplitude = 1)
    theta = np.linspace(0, 2*np.pi, 361)
    fig.add_trace(go.Scatter(
        x=np.cos(theta), y=np.sin(theta),
        mode="lines", line=dict(color="black", width=1, dash="dash"),
        name="Amplitude = 1",
    ))

    # Axes at zero
    fig.add_shape(type="line", x0=-5, x1=5, y0=0, y1=0, line=dict(color="gray", width=1, dash="dot"))
    fig.add_shape(type="line", x0=0, x1=0, y0=-5, y1=5, line=dict(color="gray", width=1, dash="dot"))

    # Phase labels
    labels = {
        1: (0.35, 1.05), 2: (1.05, 0.35), 3: (1.05, -0.35), 4: (0.35, -1.05),
        5: (-0.35, -1.05), 6: (-1.05, -0.35), 7: (-1.05, 0.35), 8: (-0.35, 1.05),
    }
    for ph, (x, y) in labels.items():
        fig.add_annotation(x=x, y=y, text=str(ph), showarrow=False, font=dict(size=12))

    # Latest point
    latest = df.iloc[-1]
    fig.add_trace(go.Scatter(
        x=[latest["rmm1"]], y=[latest["rmm2"]],
        mode="markers",
        marker=dict(color="crimson", size=10, line=dict(color="white", width=1)),
        name=f"Latest: {df.index[-1].date()} (phase {latest['phase']}, amp {latest['amplitude']:.2f})",
    ))

    fig.update_layout(
        title="RMM Phase Space (RMM2 vs RMM1)",
        xaxis_title="RMM1", yaxis_title="RMM2",
        xaxis=dict(constrain="domain", scaleanchor="y", scaleratio=1, range=[-4, 4], zeroline=False),
        yaxis=dict(range=[-4, 4], zeroline=False),
        legend=dict(orientation="h", yanchor="bottom", y=1.02, xanchor="left", x=0),
        margin=dict(l=40, r=10, t=60, b=40),
        hovermode="closest",
    )
    return fig
# Use the relative path from the notebook location
rmm_path = "data/rmm.74toRealtime.txt"  # absolute also works if you prefer
rmm = load_rmm_from_file(rmm_path)

fig = plot_rmm_phase_space(rmm, highlight_days=60)
fig.show()

# Optional: inspect last few days
rmm.tail()
rmm1 rmm2 phase amplitude
date
2024-02-20 -0.305918 0.305419 8 0.432281
2024-02-21 0.103033 0.071931 5 0.125658
2024-02-22 0.236122 0.248544 6 0.342823
2024-02-23 0.201856 0.325106 6 0.382675
2024-02-24 -0.003146 0.128460 7 0.128498