"""r05_facts.py: descriptive facts the manuscript states that no analysis script registers.

Each value is read from a file (the run inventory, the stored spectra, the Phase 2 device code, the sources
recorded for this phase) or counted here; none is typed. Registered in values4.json.
"""
import sys, json, re, statistics
sys.path.insert(0, "/home/claude/mill/phase4/code")
sys.path.insert(0, "/home/claude/mill/phase3/code")
sys.path.insert(0, "/home/claude/mill/code")
import r_freeze
r_freeze.assert_freeze()

import numpy as np
import pandas as pd
from vals import Values
import q_common as QC

P4 = "/home/claude/mill/phase4"
V = Values(path=f"{P4}/outputs/values4.json", script="r05_facts.py")
S4 = json.load(open(f"{P4}/inputs/sources4.json"))
E = lambda f, s: {"population": "documented facts", "filter": f, "statistic": s}

# ------------------------------------------------------------------ early-stopped runs (corrected rule, see the deviations log)
inv = pd.read_csv("/home/claude/mill/outputs/inventory_runs.csv")
rem = inv.summary_remark.fillna("").str.lower()
early = inv[rem.str.startswith("early stop")]
V.set("facts_early_runs_vm20i", int((early.machine == "imi_vm20i").sum()), "runs",
      E("dataset_summary.xlsx remarks, imi_vm20i", "runs whose remark starts with 'Early stopped'"))
V.set("facts_early_runs_lab", int(early.machine.isin(["imi_vm20i", "imi_vmx30ui"]).sum()), "runs",
      E("dataset_summary.xlsx remarks, laboratory machines", "runs whose remark starts with 'Early stopped' (the Phase 3 count of 9 also matched 'no early stopped')"))
V.set("facts_early_vibration_vm20i", int(((early.machine == "imi_vm20i") & early.summary_remark.str.lower().str.contains("vibration")).sum()), "runs",
      E("dataset_summary.xlsx remarks, imi_vm20i", "early-stopped runs whose remark names excessive vibration"))

# ------------------------------------------------------------------ spectra: settings of the Phase 2 device pass, read from its code and from the stored files
code = open("/home/claude/mill/raw/Phase2/code/d03_segment_features.py").read()
nfft = int(re.search(r"NFFT, NB = (\d+), (\d+)", code).group(1))
assert "np.hanning(nfft)" in code and "hop = nfft // 2" in code
z = np.load(f"{QC.SEG}/imi_vm20i_dataset1.npz", allow_pickle=True)
meta = json.loads(str(z["meta"]))
assert meta["nfft"] == nfft
V.set("spec_nfft", nfft, "samples", E("Phase 2 device code d03_segment_features.py", "segment length of the Welch spectra (Hann window)"))
V.set("spec_overlap", 0.5, "fraction", E("Phase 2 device code d03_segment_features.py (hop = nfft // 2)", "overlap of consecutive Welch segments"))
V.set("spec_df_acc_hz", float(meta["channels"]["acc0"]["df"]), "Hz", E("stored spectra of imi_vm20i", "bin width of the accelerometer spectra"))
V.set("spec_df_snd_hz", float(meta["channels"]["s2"]["df"]), "Hz", E("stored spectra of imi_vm20i", "bin width of the sound spectra"))

# ------------------------------------------------------------------ dates and identifiers cited in the text
V.set("u3_accessed", "22 September 2026", "text", E("U3 repository", "date the repository README was read (novelty.json, " + S4["u3"]["accessed"] + ")"), kind="text")
assert S4["u3"]["accessed"] == "2026-09-22"
import datetime as _dt
_u4 = _dt.date.fromisoformat(S4["u4"]["accessed"])
V.set("u4_accessed", f"{_u4.day} {_u4.strftime('%B %Y')}", "text",
      E("U4 Kaggle notebook", "date the notebook was read at the final gate (sources4.json u4, " + S4["u4"]["accessed"] + ")"), kind="text")

# ------------------------------------------------------------------ recent IJAMT titles (for the title options; not printed in the paper)
T = json.load(open(f"{P4}/inputs/ijamt_recent_titles.json"))
ch = [i["title"] for i in T["items"] if i["chosen"]]
wc = [len(t.split()) for t in ch]
V.set("title_study_n", len(ch), "titles", E("IJAMT online-first page, the ten chosen original articles", "titles read"))
V.set("title_study_words_median", float(statistics.median(wc)), "words", E("the ten chosen titles", "median word count"))
V.set("title_study_words_min", min(wc), "words", E("the ten chosen titles", "fewest words"))
V.set("title_study_words_max", max(wc), "words", E("the ten chosen titles", "most words"))
V.set("title_study_colon_n", sum(":" in t for t in ch), "titles", E("the ten chosen titles", "titles with a colon"))
titlecased = lambda t: sum(1 for w in t.split()[1:] if re.fullmatch(r"[A-Z][a-z]+[,:;]?", w))
V.set("title_study_sentence_case_n", sum(titlecased(t) < 3 for t in ch), "titles",
      E("the ten chosen titles", "titles in sentence case: fewer than three words after the first written with a capital initial and lower-case rest"))
allo = [i for i in T["items"] if i["type"] == "ORIGINAL ARTICLE"]
V.set("title_study_page_original_n", len(allo), "titles", E("IJAMT online-first page 1", "original articles among the fifty listed"))
V.set("title_study_page_colon_n", sum(":" in i["title"] for i in allo), "titles", E("IJAMT online-first page 1, original articles", "titles with a colon"))
# ------------------------------------------------------------------ authors shared by the dataset descriptor and the earlier chatter study (text claim in the Introduction)
R = {r["key"]: r for r in json.load(open(f"{P4}/inputs/references.json"))["references"]}
fam_of = lambda k: {a.split(",")[0].strip() + "|" + a.split(",")[1].strip()[:1] for a in R[k]["author"]}
shared = fam_of("kim2026msm") & fam_of("kim2024chatter")
V.set("facts_shared_authors", len(shared), "authors", E("references kim2026msm and kim2024chatter (Crossref author lists)", "authors on both papers (family name and first initial): " + ", ".join(sorted(shared))))
V.save()
# ------------------------------------------------------------------ family A by material on imi_vm20i, and runs per family (Section 2 and the Fig. 1 caption)
P = QC.load_paths()
for mat, tag in (("AA 6061", "aa6061"), ("AISI 1018", "aisi1018")):
    m = (P.family == "A") & (P.Workpiece == mat)
    V.set(f"vm20i_a_{tag}_n", int(m.sum()), "paths", {"population": "labelled cutting paths (label.csv rows)", "filter": f"imi_vm20i, labels 0 and 1, family A, workpiece {mat}", "statistic": "paths"})
    V.set(f"vm20i_a_{tag}_l1_share", float(P.y[m].mean()), "fraction", {"population": "labelled cutting paths (label.csv rows)", "filter": f"imi_vm20i, labels 0 and 1, family A, workpiece {mat}", "statistic": "share of label 1 paths"})
V.set("facts_l1_d_f1", int(((P.family.isin(["D", "F1"])) & (P.y == 1)).sum()), "paths", {"population": "labelled cutting paths (label.csv rows)", "filter": "imi_vm20i, label 1, families D and F1", "statistic": "label 1 paths of families D and F1 together"})
V.set("vm20i_max_families_per_run", int(P.groupby("run").family.nunique().max()), "families", {"population": "labelled cutting paths (label.csv rows)", "filter": "imi_vm20i, labels 0 and 1", "statistic": "largest number of tool families in one run"})
V.save()
# ------------------------------------------------------------------ leave-one-run-out flips of H2 (check A1) and label 1 indicators changed by V6c and V7
L = pd.read_csv(f"{P4}/outputs/a1_leave_one_run_out.csv")
fl = sorted(int(r.split(":")[1]) for r in L[L.rule_H2].run)
V.set("facts_a1_h2_flip_runs", " or ".join(f"run {x}" for x in fl), "text", {"population": "labelled cutting paths (label.csv rows)", "filter": "imi_vm20i, labels 0 and 1, check A1", "statistic": "runs whose removal alone would make the H2 decision rule hold, joined by 'or' (each alone flips the rule)"}, kind="text")
V.set("facts_a1_h2_flip_n", len(fl), "runs", {"population": "labelled cutting paths (label.csv rows)", "filter": "imi_vm20i, labels 0 and 1, check A1", "statistic": "runs whose removal alone would make the H2 decision rule hold"})
assert len(fl) == V.v["a1_h2_flips"]["value"], "the flip runs must match the count registered by check A1"
for which, idx in (("min", L.h2.values.argmin()), ("max", L.h2.values.argmax())):
    V.set(f"facts_a1_h2_{which}_run", int(L.run.values[idx].split(":")[1]), "run", {"population": "labelled cutting paths (label.csv rows)", "filter": "imi_vm20i, labels 0 and 1, check A1",
          "statistic": f"run whose removal gives the {'smallest' if which == 'min' else 'largest'} h2_drop (a1_h2_{which})"})
A = np.load(f"{P4}/outputs/r03_arrays.npz")
lab1 = (P.y == 1).values
for sname, tag in (("rel", "v6c"), ("band5k", "v7")):
    ch = np.zeros(len(P), bool)
    for c in ("acc0", "s2"):
        b, v = A[f"{c}_base"], A[f"{c}_{sname}"]
        cc = np.isfinite(b) & np.isfinite(v) & (b != v)
        V.set(f"facts_{tag}_changed_{c}", int(cc.sum()), "paths", {"population": "labelled cutting paths (label.csv rows)", "filter": f"imi_vm20i, labels 0 and 1, variant {tag.upper()}", "statistic": f"paths whose {'Acc0' if c == 'acc0' else 'Sound2'} indicator differs from the D5 settings"})
        ch |= cc
    V.set(f"facts_{tag}_l1_changed", int((ch & lab1).sum()), "paths", {"population": "labelled cutting paths (label.csv rows)", "filter": f"imi_vm20i, label 1, variant {tag.upper()}", "statistic": "label 1 paths whose Acc0 or Sound2 indicator differs from the D5 settings"})
    V.set(f"facts_{tag}_changed", int(ch.sum()), "paths", {"population": "labelled cutting paths (label.csv rows)", "filter": f"imi_vm20i, labels 0 and 1, variant {tag.upper()}", "statistic": "paths whose Acc0 or Sound2 indicator differs from the D5 settings"})
V.save()
print("r05_facts: registered", sum(1 for r in V.v.values() if r["source"] == "r05_facts.py"), "values")
