Domain: Science, Environment & Research

You are given a file named power.csv with 1001 rows of data your task is to build an advanced per-row anomaly index named RSI_row for all the rows using:
- multi-scale causal behavior (short, mid, long windows)
- ramp and curvature dynamics
- aggregate-vs-unit mismatch and drift behavior
- energy closure residual
- structural penalties (vector-angle mismatch + distribution imbalance)
- trend stabilization (EWMA + rolling volatility)

## Data handling contract

1. Clean the Data
2. Parse DateTimeUTC as datetime.
3. Sort ascending by DateTimeUTC using stable sort semantics (mergesort-equivalent).
4. Drop duplicate DateTimeUTC keeping first.
5. Coerce all non-time columns to float64.
6. Replace +inf/-inf with NaN.
7. For each numeric column:
   - forward-fill with limit=3
   - fill remaining NaN with column median (after ffill)
   - All MAD/z-score/rolling calculations ignore NaNs after filling, and any empty windows return 0.

## Base series

Let:
- Pagg = AGs01To26_kW
- Qagg = AGs01To26_kVAR
- Psum = sum(AG01..AG26_kW)
- Qsum = sum(AG01..AG26_kVAR)
- dP = Pagg - Psum
- dQ = Qagg - Qsum
- mismatch_mag = sqrt(dP^2 + dQ^2)
Time step:
- dt[0] = median(first 200 valid positive deltas; if <200, use all valid positive deltas)
- dt[i>0] = timestamp delta seconds
- replace invalid/non-positive dt by dt[0]
Derived dynamics:
- ramp[0]=0, ramp[i]=(Pagg[i]-Pagg[i-1])/dt[i]
- curvature[0]=0, curvature[i]=(ramp[i]-ramp[i-1])/dt[i]
- dE_obs[0]=0, dE_obs[i]=E[i]-E[i-1] where E=AGs01To26_kWh
- dE_pred=Pagg*dt/3600
- energy_resid=dE_obs-dE_pred
Structural penalties:
- angle_penalty=(1-cos)/2, where cos is between vectors (Pagg,Qagg) and (Psum,Qsum) clipped to [-1,1]
- distribution_penalty = 1 - entropy(abs(AG01..AG26_kW shares)) normalize shares to sum=1

## Multi-scale windows and robust scoring

Use trailing causal windows:
- short: W1=20
- mid: W2=60
- long: W3=180
For any series window x ending at row i:
- z_current = abs(x_last - median(x)) / (1.4826*MAD(x) + 1e-12)
Numerical consistency:
- use epsilon = 1e-12 for all guarded divisions in this task (do not use other epsilons).
- for least-squares slope, if denominator is exactly 0, set slope to 0 (do not add epsilon to denominator).
- mean_dt in drift calculations includes all dt in the window after replacements
- step split: for odd n, split first floor(n/2) rows vs remaining (n - floor(n/2)) rows
- all intermediate rolling/quantile calculations ignore NaNs (after filling) and return 0 for empty windows
Compute per row:
- ramp z at W1/W2/W3
- mismatch z at W1/W2/W3
- step z on Pagg at W2 and W3 (two-means split first floor(n/2) vs second half)
- drift z on dP at W2 and W3 (least-squares slope per second normalized by robust drift scale)
- curvature z at W2
- energy residual z at W2
Drift z must be computed exactly as:
- slope_idx = least-squares slope of dP over x = 0..(n-1)
- mean_dt = mean(dt in same window)
- bsec = slope_idx / mean_dt
- scale_drift = (MAD(dP_window) / (n * mean_dt)) + 1e-12
- z_drift = abs(bsec) / scale_drift

## Composite and final RSI

Component blends:
- comp_ramp = 0.50*z_ramp_W1 + 0.35*z_ramp_W2 + 0.15*z_ramp_W3
- comp_step = 0.65*z_step_W2 + 0.35*z_step_W3
- comp_drift = 0.55*z_drift_W2 + 0.45*z_drift_W3
- comp_mismatch = 0.45*z_mismatch_W1 + 0.35*z_mismatch_W2 + 0.20*z_mismatch_W3
Base score:
- rsi_base = clip(0,1,
  0.25*tanh(comp_ramp/3) +
  0.18*tanh(comp_step/3) +
  0.14*tanh(comp_drift/3) +
  0.14*tanh(comp_mismatch/3) +
  0.12*tanh(z_curv_W2/3) +
  0.10*tanh(z_energy_W2/3) +
  0.05*angle_penalty +
  0.02*distribution_penalty)
Trend-adjusted score:
- ewma = EWM(rsi_base, span=30, adjust=False)
- trend_dev = abs(rsi_base - ewma)
- rolling_std_30 uses window=30, min_periods=5, then NaN -> 0
- vol_norm = clip(rolling_std_30(rsi_base) / (q95_linear(rolling_std_30)+1e-12), 0, 1)
- rsi_trend = clip(0,1, 0.72*rsi_base + 0.20*trend_dev + 0.08*vol_norm)
Final:
- RSI_row = round(rsi_trend, 6)

## CSV requirements

Write power_with_rsi.csv with original columns plus appended:
1. z_ramp_60
2. z_step_60
3. z_drift_60
4. z_mismatch_60
5. z_curv_60
6. z_energy_60
7. angle_penalty
8. distribution_penalty
9. rsi_base
10. rsi_trend
11. RSI_row
All appended columns must be rounded to 6 decimals.

## summary.json
Exact JSON format:
```json
{
  "rows": 0,
  "window_short": 20,
  "window_mid": 60,
  "window_long": 180,
  "rsi_mean": 0.0,
  "rsi_std": 0.0,
  "rsi_min": 0.0,
  "rsi_p50": 0.0,
  "rsi_p90": 0.0,
  "rsi_p95": 0.0,
  "rsi_p99": 0.0,
  "rsi_max": 0.0,
  "high_alert_count": 0,
  "very_high_alert_count": 0,
  "sustained_alert_segments": 0,
  "median_dt_seconds": 0.0,
  "dt_replaced_count": 0,
  "component_mean_ramp": 0.0,
  "component_mean_step": 0.0,
  "component_mean_drift": 0.0,
  "component_mean_mismatch": 0.0,
  "component_mean_curvature": 0.0,
  "component_mean_energy": 0.0,
  "component_mean_angle_penalty": 0.0,
  "component_mean_distribution_penalty": 0.0,
  "top20_timestamps": [
    "YYYY-MM-DD HH:MM:SS"
  ],
  "top20_rsi": [
    0.0
  ],
  "sha256_top20": "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
  "sha256_first200_rsi": "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
  "sha256_component_signature": "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
}
```
All summary float fields should be rounded to 8 decimals (except median_dt_seconds, 6 decimals).
Quantile rule:
- all percentiles/quantiles in this task (p50, p90, p95, p99, q95 for volatility scaling) must use linear interpolation.
Alert and segment rules:
- high_alert_count = count(RSI_row >= 0.7)
- very_high_alert_count = count(RSI_row >= 0.85)
- sustained_alert_segments counts runs of (RSI_row >= 0.7) with run length >= 3
dt diagnostic rule:
- median_dt_seconds must be median of final dt array after invalid replacements (not dt0).
Tie-break and formatting rules:
- top20 rows are sorted by RSI_row descending, ties by earliest DateTimeUTC.(0..19)
- sorting for top20 must be stable
- top20 must be selected using this exact key order: sort by [RSI_row desc, DateTimeUTC asc], then take first 20 rows.
- top20_timestamps must be formatted exactly as YYYY-MM-DD HH:MM:SS.
- top20_rsi values are rounded to 6 decimals.
- sha256_top20 input text: newline-joined `timestamp,rsi` using rsi formatted to 8 decimals.
- sha256_first200_rsi input text: first 200 RSI_row values, each formatted to 8 decimals, newline-joined.
- sha256_component_signature input text:
  mean(comp_ramp)|mean(comp_step)|mean(comp_drift)|mean(comp_mismatch)|mean(z_curv_W2)|mean(z_energy_W2)|mean(angle_penalty)|mean(distribution_penalty)
  with each mean formatted to 8 decimals.

Grading rubric:
criterion: Does summary.json contain the required keys as mentioned in the prompt?
weight: 10
criterion: Does top20_timestamps contain exactly 20 entries?
weight: 10
criterion: Does rsi_mean equal 0.30704694?
weight: 15
criterion: Does rsi_std equal 0.07085234?
weight: 15
criterion: Does rsi_p95 equal 0.44477?
weight: 10
criterion: Does rsi_max equals 0.627625?
weight: 10
criterion: Does dt_replaced_count equal 0?
weight: 10
criterion: Does top20_timestamps[19] equal "2019-07-01 04:21:20"?
weight: 10
criterion: Does top20_rsi[19] equal 0.490957?
weight: 10

Golden answer:
**Data cleaning Code**

```python
import re
import numpy as np
import pandas as pd
import filecmp

NUM_WORD_MAP = {
    "zero": "0", "one": "1", "two": "2", "three": "3", "four": "4",
    "five": "5", "six": "6", "seven": "7", "eight": "8", "nine": "9",
}
NUM_WORD_RE = re.compile("|".join(NUM_WORD_MAP.keys()), flags=re.IGNORECASE)

def _replace_num_words(s: str) -> str:
    return NUM_WORD_RE.sub(lambda m: NUM_WORD_MAP[m.group(0).lower()], s)

PAREN_OP_RE = re.compile(r"\((\d+)([-*])(\d+)\)")

def _eval_paren_ops(s: str) -> str:
    def repl(m: re.Match) -> str:
        a_str, op, b_str = m.group(1), m.group(2), m.group(3)
        a, b = int(a_str), int(b_str)
        if op == "-":
            res = a - b
            return str(res).zfill(len(a_str))
        else:
            return str(a * b)
    return PAREN_OP_RE.sub(repl, s)

TRANSLATE_OCR = str.maketrans({
    "O": "0", "o": "0",
    "L": "1", "l": "1",
    "I": "1",
    "S": "5", "s": "5",
    "B": "8", "b": "8",
})

def clean_datetime(val) -> str:
    if pd.isna(val):
        return np.nan
    s = str(val)
    s = re.sub(r"<strikethrough>.*?</strikethrough>", "", s, flags=re.IGNORECASE)
    s = re.sub(r"<[^>]+>", "", s)
    s = s.replace('"', "")
    s = _replace_num_words(s)
    s = s.translate(TRANSLATE_OCR)
    s = re.sub(r"[^0-9:\-\s]", "", s)
    s = re.sub(r"\s+", " ", s).strip()
    dt = pd.to_datetime(s, errors="coerce", format="%Y-%m-%d %H:%M:%S")
    if pd.isna(dt):
        dt = pd.to_datetime(s, errors="coerce")
    return dt.strftime("%Y-%m-%d %H:%M:%S") if not pd.isna(dt) else np.nan

def clean_number(val) -> float:
    if pd.isna(val):
        return np.nan
    if isinstance(val, (int, float, np.integer, np.floating)) and not isinstance(val, bool):
        return float(val)
    s = str(val).strip()
    if s == "" or s.lower() == "nan":
        return np.nan
    s = re.sub(r"^\s*minus", "-", s, flags=re.IGNORECASE)
    s = re.sub(r"<strikethrough>.*?</strikethrough>", "", s, flags=re.IGNORECASE)
    s = re.sub(r"<[^>]+>", "", s)
    s = _replace_num_words(s)
    s = re.sub(r"[A-Za-z]+\$(\d+)\$END", r"\1", s, flags=re.IGNORECASE)
    s = re.sub(r"END", "", s, flags=re.IGNORECASE)
    s = _eval_paren_ops(s)
    s = re.sub(r"flstp", ".", s, flags=re.IGNORECASE)
    s = re.sub(r"ful+l*[- ]?stop", ".", s, flags=re.IGNORECASE)
    s = re.sub(r"full[- ]?stop", ".", s, flags=re.IGNORECASE)
    s = re.sub(r"fullstop", ".", s, flags=re.IGNORECASE)
    s = re.sub(r"full-stop", ".", s, flags=re.IGNORECASE)
    s = re.sub(r"point", ".", s, flags=re.IGNORECASE)
    s = re.sub(r"dot", ".", s, flags=re.IGNORECASE)
    s = re.sub(r"ok", "", s, flags=re.IGNORECASE)
    s = re.sub(r"B\$", "", s)
    s = s.translate(TRANSLATE_OCR)
    s = s.replace('"', "").replace("*", "").replace("$", "")
    s = re.sub(r"[^0-9\+\-\.]", "", s)
    if s.count(".") > 1:
        first = s.find(".")
        s = s[: first + 1] + s[first + 1 :].replace(".", "")
    m = re.search(r"[+-]?\d+(?:\.\d+)?", s)
    return float(m.group(0)) if m else np.nan

def run_clean(df: pd.DataFrame) -> pd.DataFrame:
    df = df.copy()
    mask = df["AG08_kWh"].astype(str).str.contains(",", na=False)
    for i in df.index[mask]:
        part1, part2 = str(df.at[i, "AG08_kWh"]).split(",", 1)
        df.at[i, "AG08_kWh"] = part1
        df.at[i, "AG10_kWh"]  = df.at[i, "AG10_kVAR"]
        df.at[i, "AG10_kVAR"] = df.at[i, "AG10_kW"]
        df.at[i, "AG10_kW"]   = df.at[i, "AG09_kWh"]
        df.at[i, "AG09_kWh"]  = df.at[i, "AG09_kVAR"]
        df.at[i, "AG09_kVAR"] = df.at[i, "AG09_kW"]
        df.at[i, "AG09_kW"]   = part2

    df["DateTimeUTC"] = df["DateTimeUTC"].apply(clean_datetime)
    for col in df.columns:
        if col != "DateTimeUTC":
            df[col] = df[col].apply(clean_number)
    return df

raw_path = "power.csv"
out_path = "power_cleaned.csv"

df_raw = pd.read_csv(raw_path)
df_out = run_clean(df_raw)

df_out_str = df_out.copy()
for col in df_out_str.columns:
    if col == "DateTimeUTC":
        continue
    df_out_str[col] = df_out_str[col].map(lambda x: "" if pd.isna(x) else format(float(x), ".15g"))

df_out_str.to_csv(out_path, index=False, lineterminator="\r\n")
with open(out_path, "rb") as f:
    content = f.read()
if content.endswith(b"\r\n"):
    with open(out_path, "wb") as f:
        f.write(content[:-2])
```

**Problem Solution Code**

```python
from __future__ import annotations
import hashlib
import json
from pathlib import Path
import numpy as np
import pandas as pd


def linear_slope(y: np.ndarray) -> float:
    n = y.size
    if n < 2:
        return 0.0
    x = np.arange(n, dtype=np.float64)
    x_mean = (n - 1) / 2.0
    y_mean = float(np.mean(y))
    dx = x - x_mean
    dy = y - y_mean
    denom = float(np.sum(dx * dx))
    if denom == 0.0:
        return 0.0
    return float(np.sum(dx * dy) / denom)


def quantile_linear(values: np.ndarray, q: float) -> float:
    try:
        return float(np.quantile(values, q, method="linear"))
    except TypeError:
        return float(np.quantile(values, q, interpolation="linear"))


def median_abs_deviation(values: np.ndarray) -> float:
    center = float(np.median(values))
    return float(np.median(np.abs(values - center)))


def robust_z_current(values: np.ndarray) -> float:
    if values.size == 0:
        return 0.0
    center = float(np.median(values))
    scale = 1.4826 * median_abs_deviation(values) + 1e-12
    return abs(float(values[-1]) - center) / scale


def two_means_step(values: np.ndarray) -> float:
    n = values.size
    half = n // 2
    if half == 0:
        return 0.0
    left = float(np.mean(values[:half]))
    right = float(np.mean(values[half:]))
    return abs(right - left)


def count_sustained_segments(mask: np.ndarray, min_len: int) -> int:
    count = 0
    run = 0
    for v in mask:
        if bool(v):
            run += 1
        else:
            if run >= min_len:
                count += 1
            run = 0
    if run >= min_len:
        count += 1
    return count


def main() -> None:
    input_csv = Path("power_cleaned.csv")
    output_csv = Path("power_with_rsi.csv")
    output_json = Path("summary.json")

    df = pd.read_csv(input_csv)
    df["DateTimeUTC"] = pd.to_datetime(df["DateTimeUTC"], errors="coerce")
    df = df.sort_values("DateTimeUTC", kind="mergesort")
    df = df.drop_duplicates(subset=["DateTimeUTC"], keep="first").reset_index(drop=True)

    numeric_cols = df.columns.drop("DateTimeUTC")
    df[numeric_cols] = df[numeric_cols].apply(pd.to_numeric, errors="coerce").astype(np.float64)
    df[numeric_cols] = df[numeric_cols].replace([np.inf, -np.inf], np.nan)
    for col in numeric_cols:
        s = df[col].copy().ffill(limit=3)
        med = float(s.median(skipna=True))
        if not np.isfinite(med):
            med = 0.0
        df[col] = s.fillna(med)

    n = len(df)
    if n == 0:
        df["RSI_row"] = np.array([], dtype=np.float64)
        summary = {
            "rows": 0,
            "window_short": 20,
            "window_mid": 60,
            "window_long": 180,
            "rsi_mean": 0.0,
            "rsi_std": 0.0,
            "rsi_min": 0.0,
            "rsi_p50": 0.0,
            "rsi_p90": 0.0,
            "rsi_p95": 0.0,
            "rsi_p99": 0.0,
            "rsi_max": 0.0,
            "high_alert_count": 0,
            "very_high_alert_count": 0,
            "sustained_alert_segments": 0,
            "median_dt_seconds": 0.0,
            "dt_replaced_count": 0,
            "component_mean_ramp": 0.0,
            "component_mean_step": 0.0,
            "component_mean_drift": 0.0,
            "component_mean_mismatch": 0.0,
            "component_mean_curvature": 0.0,
            "component_mean_energy": 0.0,
            "component_mean_angle_penalty": 0.0,
            "component_mean_distribution_penalty": 0.0,
            "top20_timestamps": [],
            "top20_rsi": [],
            "sha256_top20": hashlib.sha256(b"").hexdigest(),
            "sha256_first200_rsi": hashlib.sha256(b"").hexdigest(),
            "sha256_component_signature": hashlib.sha256(b"").hexdigest(),
        }
        df.to_csv(output_csv, index=False)
        with output_json.open("w", encoding="utf-8") as f:
            json.dump(summary, f, separators=(",", ":"))
        print(json.dumps(summary, separators=(",", ":")))
        return

    p_agg = df["AGs01To26_kW"].to_numpy(dtype=np.float64)
    q_agg = df["AGs01To26_kVAR"].to_numpy(dtype=np.float64)
    p_cols = [f"AG{i:02d}_kW" for i in range(1, 27)]
    q_cols = [f"AG{i:02d}_kVAR" for i in range(1, 27)]
    p_sum = df[p_cols].to_numpy(dtype=np.float64).sum(axis=1)
    q_sum = df[q_cols].to_numpy(dtype=np.float64).sum(axis=1)
    d_p = p_agg - p_sum
    d_q = q_agg - q_sum

    delta = df["DateTimeUTC"].diff().dt.total_seconds().to_numpy(dtype=np.float64)
    valid_first = delta[1:]
    valid_first = valid_first[np.isfinite(valid_first) & (valid_first > 0)]
    dt0 = float(np.median(valid_first[:200])) if valid_first.size > 0 else 1.0
    if not np.isfinite(dt0) or dt0 <= 0:
        dt0 = 1.0

    dt = delta.copy()
    dt[0] = dt0
    bad = (~np.isfinite(dt)) | (dt <= 0)
    dt_replaced_count = int(np.sum(bad))
    dt[bad] = dt0

    ramp = np.zeros(n, dtype=np.float64)
    if n > 1:
        ramp[1:] = (p_agg[1:] - p_agg[:-1]) / dt[1:]

    curvature = np.zeros(n, dtype=np.float64)
    if n > 1:
        curvature[1:] = (ramp[1:] - ramp[:-1]) / dt[1:]

    mismatch_mag = np.sqrt(d_p * d_p + d_q * d_q)

    energy = df["AGs01To26_kWh"].to_numpy(dtype=np.float64)
    d_e_obs = np.zeros(n, dtype=np.float64)
    if n > 1:
        d_e_obs[1:] = energy[1:] - energy[:-1]
    d_e_pred = p_agg * (dt / 3600.0)
    energy_resid = d_e_obs - d_e_pred

    dot = p_agg * p_sum + q_agg * q_sum
    norm_agg = np.sqrt(p_agg * p_agg + q_agg * q_agg)
    norm_sum = np.sqrt(p_sum * p_sum + q_sum * q_sum)
    cos_angle = dot / ((norm_agg * norm_sum) + 1e-12)
    cos_angle = np.clip(cos_angle, -1.0, 1.0)
    angle_penalty = (1.0 - cos_angle) / 2.0

    abs_units = np.abs(df[p_cols].to_numpy(dtype=np.float64))
    shares = abs_units / (np.sum(abs_units, axis=1, keepdims=True) + 1e-12)
    entropy = -(shares * np.log(shares + 1e-12)).sum(axis=1) / np.log(26.0)
    distribution_penalty = 1.0 - entropy

    w_short = 20
    w_mid = 60
    w_long = 180

    z_ramp_short = np.zeros(n, dtype=np.float64)
    z_ramp_mid = np.zeros(n, dtype=np.float64)
    z_ramp_long = np.zeros(n, dtype=np.float64)
    z_mismatch_short = np.zeros(n, dtype=np.float64)
    z_mismatch_mid = np.zeros(n, dtype=np.float64)
    z_mismatch_long = np.zeros(n, dtype=np.float64)
    z_step_mid = np.zeros(n, dtype=np.float64)
    z_step_long = np.zeros(n, dtype=np.float64)
    z_drift_mid = np.zeros(n, dtype=np.float64)
    z_drift_long = np.zeros(n, dtype=np.float64)
    z_curv_mid = np.zeros(n, dtype=np.float64)
    z_energy_mid = np.zeros(n, dtype=np.float64)

    for i in range(n):
        s_short = max(0, i - w_short + 1)
        s_mid = max(0, i - w_mid + 1)
        s_long = max(0, i - w_long + 1)
        end = i + 1

        z_ramp_short[i] = robust_z_current(ramp[s_short:end])
        z_ramp_mid[i] = robust_z_current(ramp[s_mid:end])
        z_ramp_long[i] = robust_z_current(ramp[s_long:end])
        z_mismatch_short[i] = robust_z_current(mismatch_mag[s_short:end])
        z_mismatch_mid[i] = robust_z_current(mismatch_mag[s_mid:end])
        z_mismatch_long[i] = robust_z_current(mismatch_mag[s_long:end])

        p_mid = p_agg[s_mid:end]
        p_long = p_agg[s_long:end]
        z_step_mid[i] = two_means_step(p_mid) / (1.4826 * median_abs_deviation(p_mid) + 1e-12)
        z_step_long[i] = two_means_step(p_long) / (1.4826 * median_abs_deviation(p_long) + 1e-12)

        d_p_mid = d_p[s_mid:end]
        d_p_long = d_p[s_long:end]
        mean_dt_mid = float(np.mean(dt[s_mid:end]))
        mean_dt_long = float(np.mean(dt[s_long:end]))
        if not np.isfinite(mean_dt_mid) or mean_dt_mid <= 0:
            mean_dt_mid = dt0
        if not np.isfinite(mean_dt_long) or mean_dt_long <= 0:
            mean_dt_long = dt0
        b_mid = linear_slope(d_p_mid) / mean_dt_mid
        b_long = linear_slope(d_p_long) / mean_dt_long
        scale_drift_mid = (median_abs_deviation(d_p_mid) / (d_p_mid.size * mean_dt_mid)) + 1e-12
        scale_drift_long = (median_abs_deviation(d_p_long) / (d_p_long.size * mean_dt_long)) + 1e-12
        z_drift_mid[i] = abs(b_mid) / scale_drift_mid
        z_drift_long[i] = abs(b_long) / scale_drift_long

        z_curv_mid[i] = robust_z_current(curvature[s_mid:end])
        z_energy_mid[i] = robust_z_current(energy_resid[s_mid:end])

    comp_ramp = 0.50 * z_ramp_short + 0.35 * z_ramp_mid + 0.15 * z_ramp_long
    comp_step = 0.65 * z_step_mid + 0.35 * z_step_long
    comp_drift = 0.55 * z_drift_mid + 0.45 * z_drift_long
    comp_mismatch = 0.45 * z_mismatch_short + 0.35 * z_mismatch_mid + 0.20 * z_mismatch_long

    base = (
        0.25 * np.tanh(comp_ramp / 3.0)
        + 0.18 * np.tanh(comp_step / 3.0)
        + 0.14 * np.tanh(comp_drift / 3.0)
        + 0.14 * np.tanh(comp_mismatch / 3.0)
        + 0.12 * np.tanh(z_curv_mid / 3.0)
        + 0.10 * np.tanh(z_energy_mid / 3.0)
        + 0.05 * angle_penalty
        + 0.02 * distribution_penalty
    )
    rsi_base = np.clip(base, 0.0, 1.0)

    ewma = pd.Series(rsi_base).ewm(span=30, adjust=False).mean().to_numpy(dtype=np.float64)
    trend_dev = np.abs(rsi_base - ewma)
    roll_std = (
        pd.Series(rsi_base)
        .rolling(window=30, min_periods=5)
        .std()
        .fillna(0.0)
        .to_numpy(dtype=np.float64)
    )
    std_scale = quantile_linear(roll_std, 0.95) + 1e-12
    vol_norm = np.clip(roll_std / std_scale, 0.0, 1.0)

    rsi_trend = np.clip(0.72 * rsi_base + 0.20 * trend_dev + 0.08 * vol_norm, 0.0, 1.0)
    rsi_rounded = np.round(rsi_trend, 6)

    df["z_ramp_60"] = np.round(z_ramp_mid, 6)
    df["z_step_60"] = np.round(z_step_mid, 6)
    df["z_drift_60"] = np.round(z_drift_mid, 6)
    df["z_mismatch_60"] = np.round(z_mismatch_mid, 6)
    df["z_curv_60"] = np.round(z_curv_mid, 6)
    df["z_energy_60"] = np.round(z_energy_mid, 6)
    df["angle_penalty"] = np.round(angle_penalty, 6)
    df["distribution_penalty"] = np.round(distribution_penalty, 6)
    df["rsi_base"] = np.round(rsi_base, 6)
    df["rsi_trend"] = np.round(rsi_trend, 6)
    df["RSI_row"] = rsi_rounded
    df.to_csv(output_csv, index=False)

    top20 = (
        df[["DateTimeUTC", "RSI_row"]]
        .sort_values(["RSI_row", "DateTimeUTC"], ascending=[False, True], kind="mergesort")
        .head(20)
        .copy()
    )
    top20_timestamps = [ts.strftime("%Y-%m-%d %H:%M:%S") for ts in top20["DateTimeUTC"]]
    top20_rsi = [round(float(v), 6) for v in top20["RSI_row"]]

    hash_payload_top20 = "\n".join(
        f"{ts.strftime('%Y-%m-%d %H:%M:%S')},{float(val):.8f}"
        for ts, val in zip(top20["DateTimeUTC"], top20["RSI_row"])
    )
    sha_top20 = hashlib.sha256(hash_payload_top20.encode("utf-8")).hexdigest()
    first200_rsi_payload = "\n".join(f"{float(v):.8f}" for v in rsi_rounded[:200])
    sha_first200_rsi = hashlib.sha256(first200_rsi_payload.encode("utf-8")).hexdigest()

    component_signature = "|".join(
        f"{v:.8f}"
        for v in [
            float(np.mean(comp_ramp)),
            float(np.mean(comp_step)),
            float(np.mean(comp_drift)),
            float(np.mean(comp_mismatch)),
            float(np.mean(z_curv_mid)),
            float(np.mean(z_energy_mid)),
            float(np.mean(angle_penalty)),
            float(np.mean(distribution_penalty)),
        ]
    )
    sha_component = hashlib.sha256(component_signature.encode("utf-8")).hexdigest()

    high_alert_mask = rsi_rounded >= 0.7
    very_high_alert_mask = rsi_rounded >= 0.85

    summary = {
        "rows": int(n),
        "window_short": w_short,
        "window_mid": w_mid,
        "window_long": w_long,
        "rsi_mean": round(float(np.mean(rsi_rounded)), 8),
        "rsi_std": round(float(np.std(rsi_rounded)), 8),
        "rsi_min": round(float(np.min(rsi_rounded)), 8),
        "rsi_p50": round(quantile_linear(rsi_rounded, 0.50), 8),
        "rsi_p90": round(quantile_linear(rsi_rounded, 0.90), 8),
        "rsi_p95": round(quantile_linear(rsi_rounded, 0.95), 8),
        "rsi_p99": round(quantile_linear(rsi_rounded, 0.99), 8),
        "rsi_max": round(float(np.max(rsi_rounded)), 8),
        "high_alert_count": int(np.sum(high_alert_mask)),
        "very_high_alert_count": int(np.sum(very_high_alert_mask)),
        "sustained_alert_segments": int(count_sustained_segments(high_alert_mask, min_len=3)),
        "median_dt_seconds": round(float(np.median(dt)), 6),
        "dt_replaced_count": dt_replaced_count,
        "component_mean_ramp": round(float(np.mean(comp_ramp)), 8),
        "component_mean_step": round(float(np.mean(comp_step)), 8),
        "component_mean_drift": round(float(np.mean(comp_drift)), 8),
        "component_mean_mismatch": round(float(np.mean(comp_mismatch)), 8),
        "component_mean_curvature": round(float(np.mean(z_curv_mid)), 8),
        "component_mean_energy": round(float(np.mean(z_energy_mid)), 8),
        "component_mean_angle_penalty": round(float(np.mean(angle_penalty)), 8),
        "component_mean_distribution_penalty": round(float(np.mean(distribution_penalty)), 8),
        "top20_timestamps": top20_timestamps,
        "top20_rsi": top20_rsi,
        "sha256_top20": sha_top20,
        "sha256_first200_rsi": sha_first200_rsi,
        "sha256_component_signature": sha_component,
    }

    with output_json.open("w", encoding="utf-8") as f:
        json.dump(summary, f, separators=(",", ":"))

    print(json.dumps(summary, separators=(",", ":")))


if __name__ == "__main__":
    main()
```

**Explanation:** The final reliability score is produced by
Loading power.csv, parsing and sorting timestamps, cleaning data, coercing numerics, and filling gaps.
Deriving key signals: power ramps and curvature, aggregate-vs-unit mismatch, energy residuals, vector angle inconsistencies, and unit imbalance.
Computing rolling robust z-scores (short/mid/long windows) for ramps, step shifts, drift, mismatch, curvature, and residuals.
Combining them with weighted nonlinear scaling into a bounded base score, then smoothing and volatility-adjusting it to generate a per-row reliability index in [0,1].
Saving diagnostics and RSI_row to power_with_rsi.csv, plus summary stats, top-risk timestamps, and SHA256 hashes to summary.json.

**GTFA**

summary.json
{
  "rows": 1001,
  "window_short": 20,
  "window_mid": 60,
  "window_long": 180,
  "rsi_mean": 0.30704694,
  "rsi_std": 0.07085234,
  "rsi_min": 0.001515,
  "rsi_p50": 0.296681,
  "rsi_p90": 0.408261,
  "rsi_p95": 0.44477,
  "rsi_p99": 0.515467,
  "rsi_max": 0.627625,
  "high_alert_count": 0,
  "very_high_alert_count": 0,
  "sustained_alert_segments": 0,
  "median_dt_seconds": 10,
  "dt_replaced_count": 0,
  "component_mean_ramp": 0.86498864,
  "component_mean_step": 0.98933729,
  "component_mean_drift": 2.72466253,
  "component_mean_mismatch": 1.16599159,
  "component_mean_curvature": 0.86949136,
  "component_mean_energy": 0.80991175,
  "component_mean_angle_penalty": 0,
  "component_mean_distribution_penalty": 0.08985762,
  "top20_timestamps": [
    "2019-07-01 05:55:40",
    "2019-07-01 06:41:50",
    "2019-07-01 06:32:10",
    "2019-07-01 04:02:00",
    "2019-07-01 05:28:30",
    "2019-07-01 06:32:00",
    "2019-07-01 06:07:20",
    "2019-07-01 04:19:50",
    "2019-07-01 05:24:20",
    "2019-07-01 05:24:30",
    "2019-07-01 04:02:30",
    "2019-07-01 05:28:40",
    "2019-07-01 04:30:10",
    "2019-07-01 04:59:40",
    "2019-07-01 06:08:00",
    "2019-07-01 06:39:10",
    "2019-07-01 04:59:50",
    "2019-07-01 05:00:00",
    "2019-07-01 04:26:50",
    "2019-07-01 04:21:20"
  ],
  "top20_rsi": [
    0.627625,
    0.569521,
    0.564033,
    0.560138,
    0.556725,
    0.551134,
    0.54016,
    0.52336,
    0.519905,
    0.516658,
    0.515467,
    0.514111,
    0.513266,
    0.511685,
    0.508975,
    0.503139,
    0.503116,
    0.49566,
    0.493794,
    0.490957
  ],
  "sha256_top20": "54ac19eb338121d8a62ebda84832a2226ba6a4ce2fbb106fab58e099ad837755",
  "sha256_first200_rsi": "884e0d7be456bdfc2a829f61c232df8c15ef9bf1011a6466997d6ecd660169b9",
  "sha256_component_signature": "ea53d52ba3e17de9e5f002f707bffb5e6c29d0612a8196f0f77380e5daa36804"
}