DynamisLab/src/OID_analysis/li22b/phase_a3_lse.py
Frank14f 52229ea0f0 feat(oid): Li22b replication — collection, POD, LSE, OID, synthesis scripts
Phase A.1: Open-loop DB collection (50 commands, LHS, new CelerisLab)
Phase A.2: Snapshot POD on Li22b DB (ROI-masked, energy analysis)
Phase A.3: LSE [sensors, b] -> POD coefficients
Phase B: Delta-q OID + cross-mapping + joint-input OID
Phase C: Three-framework synthesis (SR, Li22b, OID)

Partial data collected (in progress). Reference fields done.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-28 21:00:18 +08:00

135 lines
4.9 KiB
Python

# OID_analysis/li22b/phase_a3_lse.py
"""Phase A.3: LSE — [sensors, b] → POD coefficients.
Li22b Eq 2.7-2.8: T_ij solves a_i = sum_j T_ij * q_j
where q = [sensors (18 channels), b (3)] = 21 inputs.
Usage:
PYTHONPATH="src:$PYTHONPATH" python3 src/OID_analysis/li22b/phase_a3_lse.py
"""
import os, sys, json, glob
import numpy as np
_REPO = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", ".."))
if _REPO not in sys.path:
sys.path.insert(0, _REPO)
DATA_BASE = os.path.join(os.path.dirname(__file__), "..", "data_li22b")
DERIVED = os.path.join(DATA_BASE, "derived")
os.makedirs(os.path.join(DERIVED, "lse"), exist_ok=True)
def load_all():
cmd_dirs = sorted(glob.glob(os.path.join(DATA_BASE, "[0-9][0-9][0-9]")))
all_sens, all_forces, all_b_snap, all_configs = [], [], [], []
for d in cmd_dirs:
sfp = os.path.join(d, "sensors.npz")
ffp = os.path.join(d, "forces.npz")
if not os.path.isfile(sfp): continue
sens = np.load(sfp)["sensors"] # (200, 18)
force = np.load(ffp)["forces"] # (200, 6)
all_sens.append(sens); all_forces.append(force)
with open(os.path.join(d, "config.json")) as f:
cfg = json.load(f)
all_configs.append(cfg)
# Repeat b for each time step
b_rep = np.tile(cfg["b"], (sens.shape[0], 1)) # (200, 3)
all_b_snap.append(b_rep)
S = np.concatenate(all_sens, axis=0)
B = np.concatenate(all_b_snap, axis=0)
N = S.shape[0]
print(f"Loaded {N} snapshots from {len(cmd_dirs)} commands")
return S, B, all_configs, len(cmd_dirs)
def main():
S, B, configs, n_cmds = load_all()
N = S.shape[0]
# Load POD coefs (handles .npy or .npy.npz naming)
coef_base = os.path.join(DERIVED, "pod", "pod_coefs.npy")
for ext in [".npy", ".npy.npz", ".npz"]:
fp = coef_base + ext
if os.path.isfile(fp):
pod = np.load(fp)
break
else:
raise FileNotFoundError(f"No pod_coefs found")
A = pod["coefs"] # (N, r)
r = A.shape[1]
print(f"POD coefs: {A.shape}, rank={r}")
# Build input matrix Q = [S, B] (21-dim)
# Standardize sensors per-channel
S_std = np.zeros_like(S)
S_mean, S_std_scale = [], []
for j in range(18):
m = np.mean(S[:, j]); s = np.std(S[:, j]) or 1.0
S_std[:, j] = (S[:, j] - m) / s
S_mean.append(m); S_std_scale.append(s)
Q = np.hstack([S_std, B]) # (N, 21)
# Train/test split by commands (80/20 per Li22b)
rng = np.random.RandomState(42)
cmd_indices = np.arange(n_cmds)
rng.shuffle(cmd_indices)
n_train = int(0.8 * n_cmds)
# Map back to snapshot indices
snaps_per_cmd = N // n_cmds
train_mask = np.zeros(N, dtype=bool)
for ci in cmd_indices[:n_train]:
train_mask[ci*snaps_per_cmd:(ci+1)*snaps_per_cmd] = True
test_mask = ~train_mask
# Solve LSE: T = (Q^T Q)^{-1} Q^T A
Q_train = Q[train_mask]; A_train = A[train_mask]
QTQ = Q_train.T @ Q_train
T = np.linalg.solve(QTQ + 1e-6 * np.eye(21), Q_train.T @ A_train) # (21, r)
print(f"T matrix shape: {T.shape}")
# Predict test set
Q_test = Q[test_mask]; A_test = A[test_mask]
A_pred = Q_test @ T
# Error metrics (Li22b Eq 3.3, 3.5)
# Per-command error ε_a(b)
eps_a = []
for ci in cmd_indices[n_train:]:
i0 = ci * snaps_per_cmd; i1 = (ci + 1) * snaps_per_cmd
mask = np.zeros(N, dtype=bool); mask[i0:i1] = True
mask_test = mask[test_mask]
if np.sum(mask_test) == 0: continue
a_true = A_test[mask_test]; a_pred = A_pred[mask_test]
num = np.mean(np.sum((a_true - a_pred)**2, axis=1))
den = np.mean(np.sum(a_true**2, axis=1))
eps_a.append(float(np.sqrt(num / den)) if den > 1e-30 else 0.0)
E = float(np.sqrt(np.mean(np.array(eps_a)**2)))
print(f"\n=== LSE Results ===")
print(f"Train commands: {n_train}, Test: {n_cmds - n_train}")
print(f"Mean ε_a: {np.mean(eps_a):.4f} ± {np.std(eps_a):.4f}")
print(f"Overall E: {E:.4f} (Li22b ~0.15-0.25 for periodic)")
print(f"Best ε_a: {np.min(eps_a):.4f}, Worst: {np.max(eps_a):.4f}")
# Per-mode estimation error
per_mode_mse = np.mean((A_test - A_pred)**2, axis=0)
per_mode_energy = np.mean(A_test**2, axis=0)
per_mode_err = np.sqrt(per_mode_mse / (per_mode_energy + 1e-30))
print(f"\nTop 10 mode errors: {per_mode_err[:10].round(4).tolist()}")
# Save
out_dir = os.path.join(DERIVED, "lse")
np.savez(os.path.join(out_dir, "lse_T.npz"), T=T,
S_mean=S_mean, S_std=S_std_scale)
json.dump({"n_train": n_train, "n_test": n_cmds - n_train,
"E": E, "mean_eps_a": float(np.mean(eps_a)),
"std_eps_a": float(np.std(eps_a)),
"eps_a_all": eps_a,
"per_mode_err": per_mode_err[:20].tolist()},
open(os.path.join(out_dir, "lse_results.json"), "w"), indent=2)
print(f"\nSaved to {out_dir}")
if __name__ == "__main__":
main()