import itertools
import json
import os
import numpy as np
import scipy.linalg
HERE = os.path.dirname(os.path.abspath(__file__))
ROOT = os.path.join(HERE, "..")
FIXTURE_DIR = os.path.join(ROOT, "tests", "fixtures", "faultobs")
NETWORK_JSON = os.path.join(FIXTURE_DIR, "network.json")
REF_JSON = os.path.join(FIXTURE_DIR, "reference.json")
REL_TOL = 1e-9
DET_TOL = 1e-8
def whitened_svd(G, w):
sw = np.sqrt(w) G_tilde = G * sw[:, None] U, s, _Vt = np.linalg.svd(G_tilde, full_matrices=True)
smax2 = float(s[0]) ** 2 if s.size else 0.0
rank = int(np.sum(s**2 > REL_TOL * smax2))
return U, s, rank, sw
def parity_projector(G, w):
n = G.shape[0]
U, _s, rank, sw = whitened_svd(G, w)
U_range = U[:, :rank] P_tilde_G = U_range @ U_range.T inv_sw = 1.0 / sw
P_perp = np.eye(n) - inv_sw[:, None] * P_tilde_G * sw[None, :]
Q = scipy.linalg.null_space((G * sw[:, None]).T, rcond=float(np.sqrt(REL_TOL)))
assert Q.shape[1] == n - rank, (
f"parity dim mismatch: null_space gave {Q.shape[1]}, expected {n - rank}"
)
P_tilde_perp = Q @ Q.T
cross = float(np.max(np.abs(P_tilde_perp - (np.eye(n) - P_tilde_G))))
assert cross < 1e-9, f"SVD vs null_space parity projector disagree: {cross:.3e}"
return P_perp, U, rank, sw
def parity_basis(U, rank):
return U[:, rank:]
def eff_col_rank(cols):
if cols.shape[1] == 0:
return 0
s = np.linalg.svd(cols, compute_uv=False)
smax2 = float(s[0]) ** 2
if smax2 <= 0.0:
return 0
return int(np.sum(s**2 > REL_TOL * smax2))
def peer_signature(n_meas, incidence):
B = np.zeros((n_meas, len(incidence)))
for k, i in enumerate(incidence):
B[i, k] = 1.0
return B
def block_spark(peers, P_perp, n_meas, cap=12):
eff = [P_perp @ peer_signature(n_meas, inc) for inc in peers]
M = len(peers)
kmax = min(M, cap)
for k in range(1, kmax + 1):
for subset in itertools.combinations(range(M), k):
cols = np.hstack([eff[j] for j in subset])
total = cols.shape[1]
if total == 0:
continue
if eff_col_rank(cols) < total:
return k
return kmax + 1
def main():
with open(NETWORK_JSON) as fh:
net = json.load(fh)
G = np.array(net["rows"], dtype=np.float64) sigma = np.array(net["sigma"], dtype=np.float64) w = 1.0 / sigma**2 n_meas, state_dim = G.shape
P_perp, U, rank, sw = parity_projector(G, w)
U_parity = parity_basis(U, rank) parity_dim = n_meas - rank
detectability = {}
for name, b in net["fault_vectors"].items():
b = np.array(b, dtype=np.float64)
pb = P_perp @ b
norm = float(np.linalg.norm(pb))
detectability[name] = {"norm": norm, "detectable": bool(norm > DET_TOL)}
mdb_out = []
for d in net["mdb_directions"]:
c = np.array(d["c"], dtype=np.float64)
ncp = float(d["ncp"])
w_tilde = sw * c
q = float(np.sum((U_parity.T @ w_tilde) ** 2))
q_direct = float(c @ (w * (P_perp @ c)))
assert abs(q - q_direct) < 1e-9 * max(1.0, abs(q_direct)), (
f"{d['name']}: Baarda parity form {q:.6e} != cᵀWP⊥c {q_direct:.6e}"
)
mdb_val = float(np.sqrt(ncp / q)) if q > 1e-12 else None
mdb_out.append({"name": d["name"], "q": q, "mdb": mdb_val})
block = {}
for name, peers in net["peer_coalitions"].items():
bs = block_spark(peers, P_perp, n_meas)
block[name] = {
"block_spark": bs,
"f_detect": bs - 1,
"f_identify": (bs - 1) // 2,
}
reference = {
"description": (
"Validated-anchor reference for `tests/lunar_faultobs_reference.rs`. "
"Computed independently by numpy/scipy (SVD of the whitened design matrix "
"for P⊥; textbook Baarda parity-subspace form for the MDB non-centrality; "
"numpy SVD rank for the block spark) from the rows in `network.json` "
"(real-DE440 per-node network built by `examples/gen_faultobs_rows.rs`). "
"Validated claim: kshana's lunar_faultobs LA pipeline reproduces these "
"values to rel<1e-3 AND abs<1e-3 (integer counts exactly)."
),
"oracle": (
"numpy/scipy: np.linalg.svd (whitened design matrix), "
"scipy.linalg.null_space (parity cross-check), np.linalg.svd (block rank)"
),
"rel_tol": REL_TOL,
"det_tol": DET_TOL,
"n_meas": n_meas,
"state_dim": state_dim,
"rank_G": rank,
"parity_dim": parity_dim,
"pperp_trace": float(np.trace(P_perp)),
"pperp_fro": float(np.linalg.norm(P_perp, "fro")),
"pperp": [[float(x) for x in row] for row in P_perp],
"detectability": detectability,
"mdb": mdb_out,
"block_spark": block,
}
os.makedirs(FIXTURE_DIR, exist_ok=True)
with open(REF_JSON, "w") as fh:
json.dump(reference, fh, indent=1)
print(f"n_meas={n_meas} state_dim={state_dim} rank(G)={rank} parity_dim={parity_dim}")
print(f"pperp_trace={reference['pperp_trace']:.6f} (== parity_dim) "
f"pperp_fro={reference['pperp_fro']:.6f}")
for name, d in detectability.items():
print(f" detect {name}: ‖P⊥b‖={d['norm']:.6e} detectable={d['detectable']}")
for d in mdb_out:
print(f" mdb {d['name']}: q={d['q']:.6e} mdb={d['mdb']}")
for name, d in block.items():
print(f" block_spark {name}: {d}")
print(f"wrote {REF_JSON}")
if __name__ == "__main__":
main()