contrib: clboss-xrebalance-survival defaults frac/cap/min-samples to live config
Some checks are pending
Code Base Sanity Check / tests (push) Waiting to run
Code Base Sanity Check / coverage (push) Waiting to run
Code Base Sanity Check / build-clang (push) Waiting to run

The survival replay took --frac/--cap/--min-samples as args defaulting to the
compiled 2.0/86400/2, which silently diverged from the live predictor after a
setconfig: a no-arg run measured the wrong settings. (Only the wrong-while-
asserting headlines and floor sweep are affected -- they gate on frac/cap/
min-samples; the P(changed|...) curve tables are settings-independent.)

Default them to the live predictor config instead, read from lightning-cli
listconfigs (the clboss-xrebalance-predict-horizon-frac / -horizon-max-secs /
-min-samples options), matching how clboss-xrebalance-survey already mirrors
live. An explicit arg still wins, for frac x cap what-if grids; with no
reachable node the run falls back to the compiled defaults. The effective
values and their source (live / arg / default) are printed in a new settings:
header line so the run is never ambiguous. LIGHTNING_CLI overrides the
executable and may carry flags (e.g. for signet).
This commit is contained in:
Ken Sedgwick 2026-06-15 10:38:58 -07:00
parent fb226dd419
commit 8e5282294e
No known key found for this signature in database
GPG key ID: DBD2AF0849D711A9

View file

@ -58,7 +58,15 @@ difference is confined to that excluded bucket.
Usage:
clboss-xrebalance-survival /path/to/data.clboss
[--frac 2.0] [--cap 86400] [--min-samples 2] [--days N]
[--frac F] [--cap SECS] [--min-samples N] [--days N]
frac / cap / min-samples default to the LIVE predictor config (read
from `lightning-cli listconfigs`), so a no-arg run measures what the
node is actually doing; pass them explicitly to override (e.g. a
frac x cap grid for what-if analysis). With no reachable node they
fall back to the compiled defaults (2.0 / 86400 / 2). The effective
values + their source are printed in the `settings:` header line.
LIGHTNING_CLI overrides the executable/flags (e.g. for signet).
ALWAYS run against a copy, never the live file: CLBOSS opens its
database with no sqlite busy handler (rollback-journal mode, lone-
@ -76,7 +84,11 @@ honest decision statistic, not a hazard curve.
"""
import argparse
import json
import os
import shlex
import sqlite3
import subprocess
import sys
INF = 1 << 63
@ -261,20 +273,83 @@ def rate(changed, n):
return "%.1f%%%s" % (100.0 * changed / n, flag)
# Compiled fallbacks (match the predictor's spot-check defaults), used
# only when no explicit arg is given AND the live config can't be read.
DEFAULT_FRAC = 2.0
DEFAULT_CAP = 86400
DEFAULT_MIN_SAMPLES = 2
# Live clboss-xrebalance-predict-* option -> survival param name.
LIVE_OPTS = {
"clboss-xrebalance-predict-horizon-frac": "frac",
"clboss-xrebalance-predict-horizon-max-secs": "cap",
"clboss-xrebalance-predict-min-samples": "min_samples",
}
def read_live_config():
"""{frac, cap, min_samples} from `lightning-cli listconfigs`, so a
no-arg run mirrors the live predictor instead of the compiled
defaults. Returns {} if no node is reachable (offline copy, missing
binary, etc.) -- the caller then falls back to the defaults.
LIGHTNING_CLI overrides the executable and may carry flags, e.g.
LIGHTNING_CLI='lightning-cli --network=signet'."""
cli = shlex.split(os.environ.get("LIGHTNING_CLI", "lightning-cli"))
try:
out = subprocess.run(cli + ["listconfigs"], timeout=10,
capture_output=True, text=True)
if out.returncode != 0:
return {}
configs = json.loads(out.stdout).get("configs", {})
except (OSError, ValueError, subprocess.SubprocessError):
return {}
live = {}
for opt, name in LIVE_OPTS.items():
c = configs.get(opt)
if not c:
continue
v = c.get("value_int", c.get("value_str"))
if v is None:
continue
live[name] = float(v) if name == "frac" else int(v)
return live
def main():
ap = argparse.ArgumentParser(
description="xrebalance regime survival calibration")
ap.add_argument("db", help="path to (a copy of) data.clboss")
ap.add_argument("--frac", type=float, default=2.0,
help="horizon frac (default 2.0)")
ap.add_argument("--cap", type=int, default=86400,
help="horizon max secs (default 86400)")
ap.add_argument("--min-samples", type=int, default=2,
help="per-side sample gate (default 2)")
ap.add_argument("--frac", type=float, default=None,
help="horizon frac (default: live config, else 2.0)")
ap.add_argument("--cap", type=int, default=None,
help="horizon max secs"
" (default: live config, else 86400)")
ap.add_argument("--min-samples", type=int, default=None,
help="per-side sample gate"
" (default: live config, else 2)")
ap.add_argument("--days", type=int, default=0,
help="restrict to the most recent N days")
args = ap.parse_args()
# Resolve frac/cap/min-samples: an explicit arg wins; else mirror
# the live predictor via listconfigs; else the compiled fallback.
live = read_live_config()
def resolve(argval, name, default):
if argval is not None:
return argval, "arg"
if name in live:
return live[name], "live"
return default, "default"
frac, frac_src = resolve(args.frac, "frac", DEFAULT_FRAC)
cap, cap_src = resolve(args.cap, "cap", DEFAULT_CAP)
min_samples, ms_src = resolve(
args.min_samples, "min_samples", DEFAULT_MIN_SAMPLES)
note = "" if live else " [no live node; built-in defaults]"
print("settings: frac=%g (%s) cap=%d (%s) min-samples=%d (%s)%s"
% (frac, frac_src, cap, cap_src, min_samples, ms_src, note))
conn = sqlite3.connect("file:%s?mode=ro" % args.db, uri=True)
conn.execute("PRAGMA busy_timeout = 5000")
q = ('SELECT time, scid, dir, kind, amount_msat'
@ -304,8 +379,7 @@ def main():
trials = []
visit_rate = {}
for key, rows in per_dir.items():
for tr in replay(rows, args.frac, args.cap,
args.min_samples):
for tr in replay(rows, frac, cap, min_samples):
tr["dir_key"] = key
trials.append(tr)
times = sorted(set(r[0] for r in rows))