Files
gaojie c66855adfc
Sync to site1 / sync (push) Has been cancelled
refactor: 将子模块转为普通目录,移除外部 git 依赖
- 移除 JEPA/lejepa-identifiability 子模块 gitlink
- 移除 research/multiply/MultiPLY 子模块 gitlink
- 删除 .gitmodules(不再有外部 URL 依赖)
- 两个目录内容作为普通文件纳入主仓库追踪
- 删除各自内部 .git 目录,消除嵌套 git 仓库
2026-06-05 17:14:01 +08:00

236 lines
8.8 KiB
Python

"""
Generate LaTeX tables for the paper, matching scaling table style.
Usage:
python analysis/make_table_reacher.py --results_dir results/reacher
"""
import json
import argparse
import numpy as np
from pathlib import Path
from collections import defaultdict
def load_all_results(results_dir):
ou, traj = [], []
for p in Path(results_dir).rglob("result.json"):
r = json.load(open(p))
if "delta" in r and r.get("rho") is None:
traj.append(r)
elif "rho" in r:
ou.append(r)
return ou, traj
def best_lambda_per_x(results, x_key):
grouped = defaultdict(list)
for r in results:
grouped[(r[x_key], r["lamb"])].append(r)
best = {}
for x in sorted(set(k[0] for k in grouped)):
best_mean, best_lamb = -np.inf, None
for lamb in set(k[1] for k in grouped if k[0] == x):
m = np.mean([r["r2_hz"] for r in grouped[(x, lamb)]])
if m > best_mean:
best_mean, best_lamb = m, lamb
best[x] = {
"lamb": best_lamb,
"runs": grouped[(x, best_lamb)],
}
return best
def pm(vals, fmt=".2f"):
"""Format as value\\tiny{±std} matching paper style."""
m, s = np.mean(vals), np.std(vals)
return f"{m:{fmt}}\\tiny{{$\\pm${s:.0e}}}"
def make_combined_table(ou_results, traj_results):
ou_best = best_lambda_per_x(ou_results, "rho")
traj_best = best_lambda_per_x(traj_results, "delta")
lines = []
lines.append(r"\begin{table}[t]")
lines.append(r"\centering")
lines.append(r"\caption{")
lines.append(r" \textbf{Pixel-observation identifiability on DMC Reacher} "
r"(mean $\pm$ std over 3 seeds, best $\lambda$ per condition).")
lines.append(r" \textbf{Left:} OU process with Gaussian marginals. "
r"$R^2$ increases monotonically with $\rho$, reaching $0.95$ "
r"at $\rho = 0.99$, confirming linear identifiability from pixels.")
lines.append(r" \textbf{Right:} Real SAC trajectories with non-Gaussian marginals. "
r"The two joints have different autocorrelation timescales ($\rho_0 \neq \rho_1$) "
r"and the wrist has a near-uniform marginal distribution, "
r"leading to anisotropic and reduced identifiability.")
lines.append(r"}")
lines.append(r"\label{tab:reacher}")
lines.append(r"\resizebox{\textwidth}{!}{%")
lines.append(r"\begin{tabular}{r cc | r cc ccc}")
lines.append(r" \multicolumn{3}{c}{\textbf{OU (Gaussian)}} & "
r"\multicolumn{6}{c}{\textbf{Trajectory (non-Gaussian)}} \\")
lines.append(r"\cmidrule(lr){1-3} \cmidrule(lr){4-9}")
lines.append(r"$\rho$ & $R^2(z \to h)$ & $R^2(h \to z)$ & "
r"$\delta$ & $\rho_0$ & $\rho_1$ & "
r"$R^2(z \to h)$ & $R^2(h \to z_0)$ & $R^2(h \to z_1)$ \\")
lines.append(r"\midrule")
ou_rhos = sorted(ou_best.keys())
traj_deltas = sorted(traj_best.keys())
n_rows = max(len(ou_rhos), len(traj_deltas))
for i in range(n_rows):
# OU columns
if i < len(ou_rhos):
rho = ou_rhos[i]
runs = ou_best[rho]["runs"]
r2_zh = pm([r["r2_zh"] for r in runs])
r2_hz = pm([r["r2_hz"] for r in runs])
ou_str = f" {rho:.2f} & {r2_zh} & {r2_hz}"
else:
ou_str = r" & &"
# Traj columns
if i < len(traj_deltas):
delta = traj_deltas[i]
runs = traj_best[delta]["runs"]
rho0 = runs[0].get("rho_shoulder", None)
rho1 = runs[0].get("rho_wrist", None)
rho0_s = f"{rho0:.3f}" if rho0 is not None else "---"
rho1_s = f"{rho1:.3f}" if rho1 is not None else "---"
r2_zh = pm([r["r2_zh"] for r in runs])
r2_d0 = pm([r["r2_hz_per_dim"][0] for r in runs])
r2_d1 = pm([r["r2_hz_per_dim"][1] for r in runs])
traj_str = f"{delta} & {rho0_s} & {rho1_s} & {r2_zh} & {r2_d0} & {r2_d1}"
else:
traj_str = r"& & & & &"
lines.append(f"{ou_str} & {traj_str} \\\\")
lines.append(r"\bottomrule")
lines.append(r"\end{tabular}}")
lines.append(r"\vspace{5pt}")
lines.append(r"\vspace{-20pt}")
lines.append(r"\end{table}")
return "\n".join(lines)
def make_ou_table_standalone(ou_results):
"""Standalone OU table for appendix if needed."""
ou_best = best_lambda_per_x(ou_results, "rho")
lines = []
lines.append(r"\begin{table}[t]")
lines.append(r"\centering")
lines.append(r"\begin{tabular}{r c cc}")
lines.append(r"\toprule")
lines.append(r" \multicolumn{1}{c}{\textbf{Correlation}} & "
r"\multicolumn{1}{c}{\textbf{Regularizer}} & "
r"\multicolumn{2}{c}{\textbf{Linear identifiability}} \\")
lines.append(r"\cmidrule(lr){1-1} \cmidrule(lr){2-2} \cmidrule(lr){3-4}")
lines.append(r"$\rho$ & $\lambda$ & $R^2(z \to h)$ & $R^2(h \to z)$ \\")
lines.append(r"\midrule")
for rho in sorted(ou_best.keys()):
runs = ou_best[rho]["runs"]
lamb = ou_best[rho]["lamb"]
r2_zh = pm([r["r2_zh"] for r in runs])
r2_hz = pm([r["r2_hz"] for r in runs])
lines.append(f" {rho:.2f} & {lamb:.0e} & {r2_zh} & {r2_hz} \\\\")
lines.append(r"\bottomrule")
lines.append(r"\end{tabular}")
lines.append(r"\vspace{5pt}")
lines.append(r"\caption{")
lines.append(r" \textbf{OU (Gaussian) identifiability from pixels} "
r"(mean $\pm$ std over 3 seeds).")
lines.append(r" $R^2$ increases monotonically with temporal correlation $\rho$, "
r"reaching $0.95$ at $\rho = 0.99$.")
lines.append(r"}")
lines.append(r"\label{tab:reacher_ou}")
lines.append(r"\end{table}")
return "\n".join(lines)
def make_traj_table_standalone(traj_results):
"""Standalone traj table for appendix if needed."""
traj_best = best_lambda_per_x(traj_results, "delta")
lines = []
lines.append(r"\begin{table}[t]")
lines.append(r"\centering")
lines.append(r"\resizebox{\textwidth}{!}{%")
lines.append(r"\begin{tabular}{r cc c c cc}")
lines.append(r"\toprule")
lines.append(r" \multicolumn{1}{c}{\textbf{Stride}} & "
r"\multicolumn{2}{c}{\textbf{Autocorrelation}} & "
r"\multicolumn{1}{c}{\textbf{Regularizer}} & "
r"\multicolumn{1}{c}{\textbf{Identifiability}} & "
r"\multicolumn{2}{c}{\textbf{Per-dimension}} \\")
lines.append(r"\cmidrule(lr){1-1} \cmidrule(lr){2-3} \cmidrule(lr){4-4} "
r"\cmidrule(lr){5-5} \cmidrule(lr){6-7}")
lines.append(r"$\delta$ & $\rho_0$ & $\rho_1$ & $\lambda$ & "
r"$R^2(z \to h)$ & $R^2(h \to z_0)$ & $R^2(h \to z_1)$ \\")
lines.append(r"\midrule")
for delta in sorted(traj_best.keys()):
runs = traj_best[delta]["runs"]
lamb = traj_best[delta]["lamb"]
rho0 = runs[0].get("rho_shoulder", None)
rho1 = runs[0].get("rho_wrist", None)
rho0_s = f"{rho0:.3f}" if rho0 is not None else "---"
rho1_s = f"{rho1:.3f}" if rho1 is not None else "---"
r2_zh = pm([r["r2_zh"] for r in runs])
r2_d0 = pm([r["r2_hz_per_dim"][0] for r in runs])
r2_d1 = pm([r["r2_hz_per_dim"][1] for r in runs])
lines.append(f" {delta} & {rho0_s} & {rho1_s} & {lamb:.0e} "
f"& {r2_zh} & {r2_d0} & {r2_d1} \\\\")
lines.append(r"\bottomrule")
lines.append(r"\end{tabular}}")
lines.append(r"\vspace{5pt}")
lines.append(r"\caption{")
lines.append(r" \textbf{Trajectory (non-Gaussian) identifiability from pixels} "
r"(mean $\pm$ std over 3 seeds).")
lines.append(r" The shoulder ($z_0$) and wrist ($z_1$) have different "
r"autocorrelation timescales and marginal distributions, "
r"leading to anisotropic identifiability.")
lines.append(r"}")
lines.append(r"\label{tab:reacher_traj}")
lines.append(r"\end{table}")
return "\n".join(lines)
def main():
parser = argparse.ArgumentParser()
parser.add_argument("--results_dir", type=str, default="results/reacher")
args = parser.parse_args()
ou, traj = load_all_results(args.results_dir)
print(f"Loaded {len(ou)} OU runs, {len(traj)} traj runs\n")
if ou and traj:
print("=" * 70)
print("COMBINED TABLE (for main text)")
print("=" * 70)
print(make_combined_table(ou, traj))
print()
if ou:
print("=" * 70)
print("OU TABLE (standalone, for appendix)")
print("=" * 70)
print(make_ou_table_standalone(ou))
print()
if traj:
print("=" * 70)
print("TRAJ TABLE (standalone, for appendix)")
print("=" * 70)
print(make_traj_table_standalone(traj))
if __name__ == "__main__":
main()