CoolFace
Apppublic

FatResConBUT/TT-DMD

sourceHugging Facemitupdated 3mo agoView on Hugging Face
1likes
app.py1421 linesDownload Raw Back to root
1"""2TT-DMD Online Solver3Copyright (c) 2026 Yue Li, Miroslav Vořechovský4Institute of Structural Mechanics, Brno University of Technology (BUT), Czech Republic.5ERC Synergy Grant project FatResCon · MIT License6"""7 8import streamlit as st9import numpy as np10import plotly.graph_objects as go11from scipy.linalg import eig12from scipy import io as sio13from typing import List, Optional, Tuple14import time15import io16import json17from pathlib import Path18 19st.set_page_config(20    page_title="TT-DMD Solver · FatResCon",21    layout="wide",22    initial_sidebar_state="collapsed",23    page_icon="⚡"24)25 26APP_DIR = Path(__file__).resolve().parent27FATRESCON_LOGO_PATH = APP_DIR / "src" / "FatResCon_logo.png"28EU_LOGO_PATH        = APP_DIR / "src" / "EU_fund_logo.png"29 30# ══════════════════════════════════════════════════════════════════════════════31# DESIGN SYSTEM32# ══════════════════════════════════════════════════════════════════════════════33st.markdown("""34<style>35@import url('https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500;600&family=Bebas+Neue&family=Inter:wght@400;500;600;700&display=swap');36 37:root {38  --bg:      #0d1117;39  --s1:      #161b27;40  --s2:      #1e2535;41  --s3:      #252d42;42  --b1:      #2d3654;43  --b2:      #3d4f7a;44  --tx:      #f0f4ff;45  --tx2:     #9aaccc;46  --tx3:     #5a6d94;47  --ac:      #ff6b35;48  --ac2:     #ffd166;49  --ac3:     #06d6a0;50  --ac4:     #a78bfa;51  --ok:      #22d3a0;52  --err:     #f87171;53  --info:    #60a5fa;54}55 56html, body, .stApp {57  background: var(--bg) !important;58  color: var(--tx) !important;59  font-family: 'Inter', -apple-system, sans-serif;60}61 62#MainMenu, footer, header { visibility: hidden; }63.stDeployButton { display: none; }64 65/* ── Scrollbar ── */66::-webkit-scrollbar { width: 5px; height: 5px; }67::-webkit-scrollbar-track { background: var(--bg); }68::-webkit-scrollbar-thumb { background: var(--b2); border-radius: 3px; }69 70/* ══ TYPOGRAPHY ══ */71h1, h2, h3, h4, h5, h6 { color: var(--tx) !important; }72 73/* ══ HERO BANNER ══ */74.hero-wrap {75  padding: 2rem 0 1.5rem;76  border-bottom: 1px solid var(--b1);77  margin-bottom: 1.5rem;78}79.hero-eyebrow {80  font-family: 'IBM Plex Mono', monospace;81  font-size: 0.68rem;82  font-weight: 500;83  letter-spacing: 0.22em;84  color: var(--ac);85  text-transform: uppercase;86  margin-bottom: 0.5rem;87}88.hero-title {89  font-family: 'Bebas Neue', sans-serif;90  font-size: 3rem;91  font-weight: 400;92  color: var(--tx);93  letter-spacing: 0.06em;94  line-height: 0.95;95  margin-bottom: 0.5rem;96}97.hero-title em { color: var(--ac); font-style: normal; }98.hero-sub {99  font-family: 'IBM Plex Mono', monospace;100  font-size: 0.72rem;101  font-weight: 400;102  color: var(--tx2);103  letter-spacing: 0.04em;104}105 106/* ══ SECTION LABELS ══ */107.sec {108  font-family: 'IBM Plex Mono', monospace;109  font-size: 0.62rem;110  font-weight: 600;111  letter-spacing: 0.18em;112  color: var(--ac);113  text-transform: uppercase;114  padding-bottom: 0.5rem;115  border-bottom: 1px solid var(--b1);116  margin-bottom: 0.7rem;117}118 119/* ══ METRIC CARDS ══ */120.mcard-row {121  display: grid;122  grid-template-columns: 1fr 1fr;123  gap: 0.6rem;124  margin: 0.8rem 0;125}126.mcard {127  background: var(--s1);128  border: 1px solid var(--b1);129  border-radius: 6px;130  padding: 0.8rem 1rem;131  position: relative;132  overflow: hidden;133}134.mcard::after {135  content: '';136  position: absolute;137  top: 0; left: 0; right: 0; height: 2px;138}139.mcard.orange::after { background: var(--ac); }140.mcard.teal::after   { background: var(--ac3); }141.mcard.yellow::after { background: var(--ac2); }142.mcard.purple::after { background: var(--ac4); }143.mcard.red::after    { background: var(--err); }144.mcard.blue::after   { background: var(--info); }145 146.mcard-label {147  font-family: 'IBM Plex Mono', monospace;148  font-size: 0.6rem;149  font-weight: 500;150  letter-spacing: 0.14em;151  color: var(--tx3);152  text-transform: uppercase;153  margin-bottom: 0.35rem;154}155.mcard-val {156  font-family: 'Bebas Neue', sans-serif;157  font-size: 1.9rem;158  letter-spacing: 0.04em;159  color: var(--tx);160  line-height: 1;161}162.mcard-sub {163  font-family: 'IBM Plex Mono', monospace;164  font-size: 0.62rem;165  font-weight: 400;166  color: var(--tx3);167  margin-top: 0.2rem;168}169 170/* ══ BIG STAT ROW ══ */171.stat-row {172  display: flex;173  gap: 0;174  margin: 1rem 0;175  border: 1px solid var(--b1);176  border-radius: 6px;177  overflow: hidden;178}179.stat-cell {180  flex: 1;181  padding: 0.85rem 0.6rem;182  background: var(--s1);183  border-right: 1px solid var(--b1);184  text-align: center;185}186.stat-cell:last-child { border-right: none; }187.stat-num {188  font-family: 'Bebas Neue', sans-serif;189  font-size: 2rem;190  letter-spacing: 0.05em;191  line-height: 1;192  color: var(--tx);193}194.stat-lbl {195  font-family: 'IBM Plex Mono', monospace;196  font-size: 0.58rem;197  font-weight: 500;198  letter-spacing: 0.12em;199  color: var(--tx3);200  text-transform: uppercase;201  margin-top: 0.2rem;202}203 204/* ══ PCT BOXES ══ */205.pct-row { display: flex; gap: 0.5rem; margin: 0.5rem 0; }206.pct-box {207  flex: 1; text-align: center;208  border-radius: 5px; padding: 0.5rem 0;209}210.pct-box.train {211  background: rgba(6,214,160,0.08);212  border: 1px solid rgba(6,214,160,0.25);213}214.pct-box.pred {215  background: rgba(255,107,53,0.08);216  border: 1px solid rgba(255,107,53,0.25);217}218.pct-num { font-family: 'Bebas Neue', sans-serif; font-size: 1.5rem; letter-spacing: .05em; line-height: 1; }219.pct-num.t { color: var(--ac3); }220.pct-num.p { color: var(--ac); }221.pct-lbl { font-family: 'IBM Plex Mono', monospace; font-size: 0.58rem; font-weight: 500; color: var(--tx3); text-transform: uppercase; letter-spacing: .1em; }222 223/* ══ INFO BOX ══ */224.info-box {225  background: var(--s1);226  border: 1px solid var(--b1);227  border-radius: 5px;228  padding: 0.6rem 0.85rem;229  margin: 0.4rem 0;230  font-family: 'IBM Plex Mono', monospace;231  font-size: 0.68rem;232  font-weight: 400;233  color: var(--tx2);234  line-height: 1.65;235}236.info-box b { color: var(--ac); font-weight: 600; }237.info-box a { color: var(--ac3) !important; }238 239/* ══ WARN BOX ══ */240.warn-box {241  background: rgba(255,209,102,0.07);242  border-left: 3px solid var(--ac2);243  border-radius: 0 4px 4px 0;244  padding: 0.6rem 0.85rem;245  margin: 0.5rem 0;246  font-family: 'IBM Plex Mono', monospace;247  font-size: 0.68rem;248  font-weight: 500;249  color: var(--ac2);250  line-height: 1.5;251}252 253/* ══ NATIVE STREAMLIT OVERRIDES ══ */254 255/* Inputs */256.stSelectbox > div > div {257  background: var(--s2) !important;258  border: 1px solid var(--b1) !important;259  color: var(--tx) !important;260  font-family: 'IBM Plex Mono', monospace !important;261  font-size: 0.78rem !important;262  font-weight: 500 !important;263}264[data-testid="stNumberInput"] input {265  background: var(--s2) !important;266  color: var(--tx) !important;267  border: 1px solid var(--b1) !important;268  font-family: 'IBM Plex Mono', monospace !important;269  font-size: 0.78rem !important;270  font-weight: 500 !important;271}272[data-testid="stNumberInput"] button {273  background: var(--s3) !important;274  color: var(--tx) !important;275  border: 1px solid var(--b1) !important;276}277 278/* Slider */279.stSlider [data-baseweb="thumb"] { background: var(--ac) !important; border-color: var(--ac) !important; }280.stSlider [data-baseweb="track-background"] { background: var(--b2) !important; }281.stSlider [data-baseweb="track"] { background: var(--ac) !important; }282 283/* Buttons */284.stButton > button {285  font-family: 'Inter', sans-serif !important;286  font-weight: 600 !important;287  font-size: 0.82rem !important;288  background: var(--s2) !important;289  border: 1px solid var(--b2) !important;290  color: var(--tx) !important;291  border-radius: 5px !important;292  letter-spacing: 0.03em;293  transition: all 0.18s;294}295.stButton > button:hover {296  background: var(--s3) !important;297  border-color: var(--ac) !important;298  color: var(--ac) !important;299}300button[data-testid="baseButton-primary"] {301  background: var(--ac) !important;302  border-color: var(--ac) !important;303  color: #fff !important;304}305 306/* File uploader */307[data-testid="stFileUploader"] {308  background: var(--s1);309  border: 1px dashed var(--b2);310  border-radius: 6px;311  padding: 0.8rem;312}313[data-testid="stFileUploaderDropzone"] {314  background: var(--s1) !important;315  border: none !important;316}317[data-testid="stFileUploaderDropzone"] * { color: var(--tx) !important; }318[data-testid="stFileUploaderDropzone"] button,319[data-testid="stFileUploader"] button {320  background: var(--s2) !important;321  color: var(--tx) !important;322  border: 1px solid var(--b2) !important;323  font-family: 'IBM Plex Mono', monospace !important;324  font-size: 0.72rem !important;325}326 327/* Textarea log */328.stTextArea textarea {329  font-family: 'IBM Plex Mono', monospace !important;330  font-size: 0.7rem !important;331  font-weight: 400 !important;332  background: var(--s1) !important;333  color: #5af7b0 !important;334  -webkit-text-fill-color: #5af7b0 !important;335  opacity: 1 !important;336  border: 1px solid var(--b1) !important;337  line-height: 1.65 !important;338}339.stTextArea textarea:disabled {340  color: #5af7b0 !important;341  -webkit-text-fill-color: #5af7b0 !important;342  opacity: 1 !important;343}344 345/* Radio */346.stRadio label {347  font-family: 'Inter', sans-serif !important;348  font-size: 0.82rem !important;349  font-weight: 500 !important;350  color: var(--tx) !important;351}352.stRadio [data-testid="stMarkdownContainer"] p { color: var(--tx) !important; }353 354/* All labels */355div[data-testid="stWidgetLabel"] *,356[data-testid="stWidgetLabel"] p,357label p, label span,358.stSlider span, .stMarkdown p, .stMarkdown span {359  color: var(--tx2) !important;360  font-size: 0.8rem !important;361  font-weight: 500 !important;362}363 364/* Divider */365hr { border-color: var(--b1) !important; margin: 1rem 0 !important; }366 367/* ══ PROJECT FOOTER ══ */368.proj-footer {369  background: var(--s1);370  border: 1px solid var(--b1);371  border-top: 2px solid var(--ac);372  border-radius: 6px;373  padding: 1rem 1.2rem;374  margin-top: 1.5rem;375}376.proj-footer p {377  font-family: 'Inter', sans-serif;378  font-size: 0.78rem;379  color: var(--tx2);380  margin: 0.2rem 0;381  line-height: 1.55;382}383.proj-footer strong { color: var(--ac); }384.proj-footer a { color: var(--ac3) !important; }385.foot-meta {386  font-family: 'IBM Plex Mono', monospace;387  font-size: 0.65rem;388  font-weight: 400;389  color: var(--tx3);390  margin-top: 0.8rem;391  line-height: 1.6;392}393 394/* ══ VIZ SECTION HEADERS ══ */395.viz-sec {396  font-family: 'IBM Plex Mono', monospace;397  font-size: 0.65rem;398  font-weight: 600;399  letter-spacing: 0.18em;400  color: var(--tx2);401  text-transform: uppercase;402  padding: 0.5rem 0 0.35rem;403  border-bottom: 1px solid var(--b1);404  margin-bottom: 0.4rem;405}406 407/* ══ PLACEHOLDER CHIPS ══ */408.placeholder-banner {409  background: var(--s1);410  border: 1px solid var(--b1);411  border-radius: 6px;412  padding: 1.5rem;413  text-align: center;414  margin: 0.4rem 0;415}416.placeholder-banner .ph-icon {417  font-family: 'Bebas Neue', sans-serif;418  font-size: 2rem;419  letter-spacing: .1em;420  color: var(--tx3);421}422.placeholder-banner .ph-label {423  font-family: 'IBM Plex Mono', monospace;424  font-size: 0.65rem;425  font-weight: 500;426  color: var(--tx3);427  margin-top: 0.3rem;428  letter-spacing: .1em;429}430</style>431""", unsafe_allow_html=True)432 433 434# ══════════════════════════════════════════════════════════════════════════════435# CORE COMPUTATION (UNCHANGED)436# ══════════════════════════════════════════════════════════════════════════════437 438def tt_svd(X, ranks=None, epsilon=1e-10):439    shape = X.shape; d = len(shape)440    if ranks is None: ranks = [None] * (d - 1)441    cores = []; X_temp = X.copy(); r_prev = 1442    for i in range(d - 1):443        N_i = shape[i]; N_remaining = int(np.prod(shape[i+1:]))444        X_mat = X_temp.reshape(r_prev * N_i, N_remaining)445        U, S, Vt = np.linalg.svd(X_mat, full_matrices=False)446        if ranks[i] is not None: r_i = min(ranks[i], len(S))447        else: r_i = max(np.sum(S > epsilon * S[0]), 1)448        U_r, S_r, Vt_r = U[:, :r_i], S[:r_i], Vt[:r_i, :]449        cores.append(U_r.reshape(r_prev, N_i, r_i))450        remaining_shape = [r_i] + list(shape[i+1:])451        X_temp = (np.diag(S_r) @ Vt_r).reshape(remaining_shape)452        r_prev = r_i453    cores.append(X_temp.reshape(r_prev, shape[-1], 1))454    return cores455 456def reconstruct_tt_from_cores(cores):457    X_hat = cores[0]458    for core in cores[1:]:459        X_hat = np.tensordot(X_hat, core, axes=([-1], [0]))460    return np.squeeze(X_hat, axis=(0, -1))461 462def predict_last_core_latent(model, time_offsets):463    time_offsets = np.asarray(time_offsets, dtype=float)464    U_r = model["U_r"]; W = model["W"]465    Omega_ttdmd = model["Omega_ttdmd"]; b_ttdmd = model["b_ttdmd"]466    Z_pred = np.zeros((W.shape[0], len(time_offsets)), dtype=complex)467    for i, t in enumerate(time_offsets):468        Z_pred[:, i] = W @ (np.exp(Omega_ttdmd * t) * b_ttdmd)469    return np.real(U_r @ Z_pred)470 471def reconstruct_from_latent(V_pred, spatial_cores, spatial_shape):472    n_steps = V_pred.shape[1]; r_last = spatial_cores[-1].shape[-1]473    X_pred = np.zeros((*spatial_shape, n_steps), dtype=float)474    for i in range(n_steps):475        temporal_core_pred = V_pred[:, i].reshape(r_last, 1, 1)476        field_i = reconstruct_tt_from_cores([*spatial_cores, temporal_core_pred])477        if field_i.shape == (*spatial_shape, 1):478            field_i = np.squeeze(field_i, axis=-1)479        X_pred[..., i] = field_i480    return X_pred481 482def predict_tt_dmd_from_model(model, time_offsets):483    V_pred = predict_last_core_latent(model, time_offsets)484    return reconstruct_from_latent(V_pred, model["spatial_cores"], model["spatial_shape"])485 486def classic_dmd_predict(Phi, Omega, b, time_steps, result_shape):487    time_steps = np.asarray(time_steps, dtype=float)488    X_pred = np.zeros((Phi.shape[0], len(time_steps)), dtype=complex)489    for idx, t in enumerate(time_steps):490        X_pred[:, idx] = Phi @ (np.exp(Omega * t) * b)491    return np.real(X_pred).reshape(result_shape)492 493def run_tt_dmd(X_input, t_start, t_end, tt_ranks=None, epsilon=1e-5, log_fn=None):494    def log(msg):495        if log_fn: log_fn(msg)496    X_train_full = X_input[..., t_start - 1:t_end]497    spatial_shape = X_train_full.shape[:-1]; n_time = X_train_full.shape[-1]498    log(f"Input tensor shape: {X_input.shape}")499    log(f"Training tensor shape: {X_train_full.shape}")500    log(f"Spatial dims: {spatial_shape}, Time steps: {n_time}")501    log(f"Using epsilon truncation threshold: {epsilon:.2e}")502    if tt_ranks is not None and len(tt_ranks) != X_train_full.ndim - 1:503        tt_ranks = [tt_ranks[0]] * (X_train_full.ndim - 1)504        log(f"Normalized TT ranks: {tt_ranks}")505    tt_cores = tt_svd(X_train_full, ranks=tt_ranks, epsilon=epsilon)506    log("TT core shapes:")507    for i, core in enumerate(tt_cores): log(f"  G_{i+1}: {core.shape}")508    spatial_cores = tt_cores[:-1]; temporal_core = tt_cores[-1]509    if temporal_core.shape[1] != n_time:510        raise ValueError(f"Temporal core dim ({temporal_core.shape[1]}) must match n_time ({n_time}).")511    r_d = temporal_core.shape[0]512    log(f"Last core shape: {temporal_core.shape}, Latent rank r_d: {r_d}")513    V_latent = temporal_core[:, :, 0]; V_train = V_latent[:, :-1]; V_target = V_latent[:, 1:]514    epsilon_svd = 1e-5515    U_v, s_v, Vh_v = np.linalg.svd(V_train, full_matrices=False)516    if s_v.size == 0: raise ValueError('SVD of V_train returned no singular values.')517    V_v = Vh_v.T; r_v = max(1, int(np.sum((s_v / s_v[0]) > epsilon_svd)))518    U_r = U_v[:, :r_v]; s_r = s_v[:r_v]; V_r = V_v[:, :r_v]519    A_tilde = U_r.T @ V_target @ V_r @ np.diag(1.0 / s_r)520    log(f"A_tilde shape: {A_tilde.shape} (rank r_v={r_v})")521    Lambda, W = eig(A_tilde)522    idx = np.argsort(np.abs(Lambda))[::-1]; Lambda = Lambda[idx]; W = W[:, idx]523    Omega_ttdmd = np.log(Lambda + 1e-14) / 1.0524    log(f"Eigenvalues computed: {len(Lambda)}")525    log(f"  |λ| range: [{np.min(np.abs(Lambda)):.4e}, {np.max(np.abs(Lambda)):.4e}]")526    for i in range(min(5, len(Lambda))):527        mag = np.abs(Lambda[i]); phase = np.angle(Lambda[i]) * 180 / np.pi528        log(f"    λ_{i+1}: |λ|={mag:.6f}, ∠={phase:+.2f}°")529    v0 = V_latent[:, 0]; z0 = U_r.T @ v0530    b_ttdmd = np.linalg.lstsq(W, z0, rcond=None)[0]531    log(f"Initial latent amplitude shape: {b_ttdmd.shape}")532    Phi = U_r @ W; Omega = Omega_ttdmd; b = b_ttdmd533    X_recon_input = reconstruct_from_latent(V_latent, spatial_cores, spatial_shape)534    rel_recon_error = np.linalg.norm(X_recon_input - X_train_full) / (np.linalg.norm(X_train_full) + 1e-14)535    log(f"Input-window reconstruction relative error: {rel_recon_error:.6e}")536    tt_model = {"U_r": U_r, "W": W, "Omega_ttdmd": Omega_ttdmd, "b_ttdmd": b_ttdmd,537                "spatial_cores": spatial_cores, "spatial_shape": spatial_shape,538                "V_latent": V_latent, "r_d": r_d, "r_v": r_v}539    return Phi, Omega, b, Lambda, tt_model540 541def run_classic_dmd(X_input, t_start, t_end, svd_rank=0, svd_rank_epsilon=None, log_fn=None):542    def log(msg):543        if log_fn: log_fn(msg)544    X_train_full = X_input[..., t_start - 1:t_end]545    original_shape = X_train_full.shape[:-1]; n_time = X_train_full.shape[-1]546    log(f"Training tensor shape: {X_train_full.shape}")547    X_train_flat = X_train_full.reshape(-1, n_time)548    X_snapshot = X_train_flat[:, :-1]; Y_snapshot = X_train_flat[:, 1:]549    epsilon_dmd = svd_rank_epsilon if svd_rank_epsilon is not None else 1e-5550    U_dmd, S_dmd, Vt_dmd = np.linalg.svd(X_snapshot, full_matrices=False)551    if S_dmd.size == 0: raise ValueError('SVD returned no singular values.')552    if svd_rank > 0: r_dmd = min(svd_rank, len(S_dmd))553    else: r_dmd = max(1, int(np.sum((S_dmd / S_dmd[0]) > epsilon_dmd)))554    log(f"SVD rank r_dmd={r_dmd}")555    U_dmd = U_dmd[:, :r_dmd]; S_dmd = S_dmd[:r_dmd]; Vt_dmd = Vt_dmd[:r_dmd, :]556    A_tilde_dmd = U_dmd.T @ Y_snapshot @ Vt_dmd.T @ np.diag(1.0 / S_dmd)557    Lambda_dmd, W_dmd = eig(A_tilde_dmd)558    idx_sort = np.argsort(np.abs(Lambda_dmd))[::-1]559    Lambda_dmd = Lambda_dmd[idx_sort]; W_dmd = W_dmd[:, idx_sort]560    Phi_dmd = U_dmd @ W_dmd; Omega_dmd = np.log(Lambda_dmd + 1e-14)561    X1_classic = X_snapshot[:, 0]562    b_dmd = np.linalg.lstsq(Phi_dmd, X1_classic, rcond=None)[0]563    log(f"Eigenvalues computed: {len(Lambda_dmd)}")564    log(f"  |λ| range: [{np.min(np.abs(Lambda_dmd)):.4e}, {np.max(np.abs(Lambda_dmd)):.4e}]")565    X_recon_train = np.zeros_like(X_train_flat, dtype=complex)566    for t_i in range(X_train_flat.shape[1]):567        X_recon_train[:, t_i] = Phi_dmd @ (np.exp(Omega_dmd * t_i) * b_dmd)568    X_recon_train = np.real(X_recon_train)569    train_rel_err = np.linalg.norm(X_recon_train - X_train_flat) / (np.linalg.norm(X_train_flat) + 1e-14)570    log(f"  Training Relative Error: {train_rel_err:.4e} ({train_rel_err*100:.2f}%)")571    return Phi_dmd, Omega_dmd, b_dmd, Lambda_dmd572 573 574# ══════════════════════════════════════════════════════════════════════════════575# SESSION STATE576# ══════════════════════════════════════════════════════════════════════════════577defaults = {578    'raw_data': None, 'reconstruction': None, 'dmd_results': None, 'errors': None,579    'axis_mapping': {'x': None, 'y': None, 'z': None, 't': None},580    'frame_idx': 0, 'playing': False, 't_start': 1, 't_end': 40,581    'camera': dict(eye=dict(x=1.5, y=1.5, z=1.2)),582    'lambda_clipped': False, 'compute_log': [], 'dmd_method': 'TT-DMD',583    'file_format': None, 'file_name': None, 'extra_steps': 100,584    'prev_axis_mapping': None, 'prev_t_start': None, 'prev_t_end': None,585    'prev_dmd_method': None, 'prev_control_mode': None, 'prev_rank': None, 'prev_eps': None,586    'log_update_key': 0, 'temporal_sync_key': 0587}588for k, v in defaults.items():589    if k not in st.session_state: st.session_state[k] = v590 591_log_ph = {'placeholder': None}592 593if len(st.session_state.compute_log) == 0:594    ts = time.strftime('%H:%M:%S')595    st.session_state.compute_log += [596        f"[{ts}] ══════════════════════════════════",597        f"[{ts}]  TT-DMD SOLVER  ·  INITIALIZED",598        f"[{ts}] ══════════════════════════════════",599        f"[{ts}]  Upload .npy / .mat / .csv to begin",600        f"[{ts}]  Demo data → drive.google.com link",601    ]602 603def add_log(msg):604    st.session_state.compute_log.append(f"[{time.strftime('%H:%M:%S')}] {msg}")605    st.session_state.log_update_key += 1606    ph = _log_ph.get('placeholder')607    if ph is not None:608        ph.text_area("", value="\n".join(st.session_state.compute_log),609                     height=180, disabled=True,610                     key=f"log_{st.session_state.log_update_key}",611                     label_visibility="collapsed")612 613def get_spatial_dims():614    return sum(1 for k in ['x','y','z'] if st.session_state.axis_mapping.get(k) is not None)615 616 617# ══════════════════════════════════════════════════════════════════════════════618# PLOT HELPERS619# ══════════════════════════════════════════════════════════════════════════════620 621BASE = dict(622    template="plotly_dark",623    paper_bgcolor='rgba(0,0,0,0)',624    plot_bgcolor='rgba(13,17,23,0.6)',625    font=dict(family='IBM Plex Mono, monospace', size=11, color='#9aaccc'),626    margin=dict(t=42, b=32, l=50, r=20),627)628 629def _title(txt, sz=13):630    return dict(text=txt, font=dict(family='Bebas Neue, sans-serif', size=sz, color='#f0f4ff'), x=0.5)631 632def _axis(label, grid=True):633    return dict(title=dict(text=label, font=dict(size=11, color='#9aaccc')),634                tickfont=dict(size=10, color='#5a6d94'),635                gridcolor='rgba(255,255,255,0.05)' if grid else 'rgba(0,0,0,0)',636                linecolor='#2d3654', linewidth=1)637 638def make_heatmap_2d_static(data_2d, title, cs='RdBu_r'):639    fig = go.Figure(go.Heatmap(z=data_2d, colorscale=cs,640        colorbar=dict(thickness=10, tickfont=dict(size=10, color='#9aaccc'))))641    fig.update_layout(**BASE, height=330, title=_title(title),642        xaxis=_axis('t'), yaxis={**_axis('x'), 'scaleanchor': 'x'}, dragmode='pan')643    return fig644 645def make_heatmap_2d_frame(data_2d, title, cs='RdBu_r', z_min=None, z_max=None):646    fig = go.Figure(go.Heatmap(z=data_2d, colorscale=cs, zsmooth='fast',647        zmin=z_min, zmax=z_max,648        colorbar=dict(thickness=10, tickfont=dict(size=10, color='#9aaccc'))))649    fig.update_layout(**BASE, height=310, title=_title(title),650        xaxis=_axis('y', grid=False), yaxis={**_axis('x', grid=False), 'scaleanchor': 'x'}, dragmode='pan')651    return fig652 653def make_volume_3d(data_3d, title, camera):654    nx, ny, nz = data_3d.shape655    X, Y, Z = np.mgrid[0:nx, 0:ny, 0:nz]656    fig = go.Figure(go.Volume(657        x=X.flatten(), y=Y.flatten(), z=Z.flatten(), value=data_3d.flatten(),658        isomin=np.percentile(data_3d, 10), isomax=np.percentile(data_3d, 90),659        opacity=0.1, surface_count=15, colorscale='Plasma',660        colorbar=dict(thickness=10, tickfont=dict(size=10, color='#9aaccc'))))661    fig.update_layout(**BASE, height=320, title=_title(title),662        scene=dict(xaxis=_axis('x'), yaxis=_axis('y'), zaxis=_axis('z'),663                   bgcolor='rgba(0,0,0,0)', camera=camera))664    return fig665 666def make_placeholder(label):667    """Dark grid placeholder shown before data/results are available."""668    rng = np.random.default_rng(99)669    noise = rng.uniform(0, 0.015, (20, 20))670    fig = go.Figure(go.Heatmap(z=noise, colorscale=[[0,'#0d1117'],[0.5,'#161b27'],[1,'#1e2535']],671                               showscale=False, zsmooth='best'))672    fig.update_layout(**BASE, height=290,673        title=dict(text=f'<span style="color:#2d3654;font-family:IBM Plex Mono">── {label} ──</span>',674                   font=dict(size=11), x=0.5),675        xaxis=dict(showticklabels=False, showgrid=False, zeroline=False),676        yaxis=dict(showticklabels=False, showgrid=False, zeroline=False))677    return fig678 679def make_placeholder_spectrum(label):680    """Unit-circle placeholder for eigenvalue spectrum before results."""681    theta = np.linspace(0, 2*np.pi, 200)682    fig = go.Figure()683    fig.add_trace(go.Scatter(x=np.cos(theta), y=np.sin(theta), mode='lines',684        line=dict(color='rgba(45,54,84,0.8)', width=1.5, dash='dot'), showlegend=False, hoverinfo='skip'))685    for xs, ys in [([-1.4,1.4],[0,0]),([0,0],[-1.4,1.4])]:686        fig.add_trace(go.Scatter(x=xs, y=ys, mode='lines',687            line=dict(color='rgba(45,54,84,0.5)', width=0.8), showlegend=False, hoverinfo='skip'))688    fig.add_trace(go.Scatter(x=[0], y=[0], mode='markers',689        marker=dict(size=4, color='#2d3654'), showlegend=False, hoverinfo='skip'))690    fig.update_layout(**BASE, height=310,691        title=dict(text=f'<span style="color:#2d3654;font-family:IBM Plex Mono">── {label} ──</span>',692                   font=dict(size=11), x=0.5),693        xaxis=dict(title=dict(text='Re(λ)', font=dict(size=11, color='#2d3654')),694                   tickfont=dict(size=10, color='#2d3654'), range=[-1.6,1.6],695                   gridcolor='rgba(45,54,84,0.3)', zeroline=False),696        yaxis=dict(title=dict(text='Im(λ)', font=dict(size=11, color='#2d3654')),697                   tickfont=dict(size=10, color='#2d3654'), range=[-1.6,1.6],698                   scaleanchor='x', gridcolor='rgba(45,54,84,0.3)', zeroline=False))699    return fig700 701def make_placeholder_line(label):702    """Empty axes placeholder for charts before results."""703    fig = go.Figure()704    fig.update_layout(**BASE, height=220,705        title=dict(text=f'<span style="color:#2d3654;font-family:IBM Plex Mono">── {label} ──</span>',706                   font=dict(size=11), x=0.5),707        xaxis=dict(showgrid=True, gridcolor='rgba(45,54,84,0.3)', tickfont=dict(size=10, color='#2d3654'),708                   zeroline=False),709        yaxis=dict(showgrid=True, gridcolor='rgba(45,54,84,0.3)', tickfont=dict(size=10, color='#2d3654'),710                   zeroline=False))711    return fig712 713# ── BUG-FIXED error chart ──────────────────────────────────────────────────────714def make_error_chart(errors, t_end):715    """Relative L1 Error over Time. FIX: use len() not truthiness on ndarray."""716    errors = np.asarray(errors)717    n = len(errors)718    x = list(range(1, n + 1))719    t_end_clipped = min(t_end, n)720    train_x = x[:t_end_clipped]721    train_y = errors[:t_end_clipped].tolist()722    pred_x  = x[t_end_clipped:]723    pred_y  = errors[t_end_clipped:].tolist()724 725    fig = go.Figure()726    fig.add_trace(go.Scatter(727        x=train_x, y=train_y, mode='lines', name='Training',728        line=dict(color='#06d6a0', width=2.5),729        fill='tozeroy', fillcolor='rgba(6,214,160,0.12)'))730 731    if len(pred_y) > 0:   # ← FIX: was `if pred_y:` which fails on numpy array732        fig.add_trace(go.Scatter(733            x=pred_x, y=pred_y, mode='lines', name='Prediction',734            line=dict(color='#ff6b35', width=2.5),735            fill='tozeroy', fillcolor='rgba(255,107,53,0.12)'))736 737    fig.add_vline(x=t_end_clipped, line=dict(color='#ffd166', width=1.5, dash='dot'))738    if len(errors) > 0:739        y_max = float(np.max(errors))740        fig.add_annotation(741            x=t_end_clipped, y=y_max * 0.82,742            text='training cutoff',743            font=dict(family='IBM Plex Mono', size=10, color='#ffd166'),744            showarrow=False, xanchor='left', xshift=8)745 746    fig.update_layout(**BASE, height=185,747        title=_title('Relative L1 Error over Time', sz=13),748        xaxis=dict(title=dict(text='Time Step', font=dict(size=11, color='#9aaccc')),749                   tickfont=dict(size=10, color='#5a6d94'),750                   gridcolor='rgba(255,255,255,0.05)', linecolor='#2d3654'),751        yaxis=dict(title=dict(text='Rel. L1 Error', font=dict(size=11, color='#9aaccc')),752                   tickfont=dict(size=10, color='#5a6d94'), tickformat='.2e',753                   gridcolor='rgba(255,255,255,0.05)', linecolor='#2d3654'),754        showlegend=True,755        legend=dict(font=dict(family='IBM Plex Mono', size=10, color='#9aaccc'),756                    bgcolor='rgba(0,0,0,0)', orientation='h', y=1.15, x=0.5, xanchor='center'))757    return fig758 759def make_eigenvalue_spectrum(Lambda):760    """Complex eigenvalue plot on unit circle."""761    re = np.real(Lambda); im = np.imag(Lambda)762    mag = np.abs(Lambda)763    stable_mask = mag <= 1.0764    n_stable = int(np.sum(stable_mask))765 766    theta = np.linspace(0, 2*np.pi, 300)767    fig = go.Figure()768 769    # Unit circle770    fig.add_trace(go.Scatter(x=np.cos(theta), y=np.sin(theta), mode='lines',771        line=dict(color='rgba(255,209,102,0.35)', width=1.2, dash='dot'),772        showlegend=False, hoverinfo='skip'))773 774    # Axes775    for xs, ys in [([-1.5,1.5],[0,0]),([0,0],[-1.5,1.5])]:776        fig.add_trace(go.Scatter(x=xs, y=ys, mode='lines',777            line=dict(color='rgba(255,255,255,0.10)', width=0.8),778            showlegend=False, hoverinfo='skip'))779 780    # Radius spokes (faint)781    for i in range(len(Lambda)):782        fig.add_trace(go.Scatter(x=[0, float(re[i])], y=[0, float(im[i])], mode='lines',783            line=dict(color='rgba(255,107,53,0.07)', width=0.8),784            showlegend=False, hoverinfo='skip'))785 786    # Stable eigenvalues787    if n_stable > 0:788        re_s = re[stable_mask]; im_s = im[stable_mask]; mag_s = mag[stable_mask]789        sz_s = (7 + 12 * (mag_s / (mag_s.max() + 1e-14))).tolist()790        htxt_s = [f"λ_{i+1}  |λ|={mag_s[i]:.4f}  ∠={float(np.angle(Lambda[stable_mask][i]))*180/np.pi:+.1f}°  STABLE"791                  for i in range(len(re_s))]792        fig.add_trace(go.Scatter(x=re_s.tolist(), y=im_s.tolist(), mode='markers', name=f'Stable ({n_stable})',793            marker=dict(size=sz_s, color='#06d6a0', opacity=0.9,794                        line=dict(color='rgba(255,255,255,0.25)', width=0.8)),795            hovertext=htxt_s, hoverinfo='text'))796 797    # Unstable eigenvalues798    n_unstable = len(Lambda) - n_stable799    if n_unstable > 0:800        unstable_mask = ~stable_mask801        re_u = re[unstable_mask]; im_u = im[unstable_mask]; mag_u = mag[unstable_mask]802        sz_u = (7 + 12 * (mag_u / (mag_u.max() + 1e-14))).tolist()803        htxt_u = [f"λ_{i+1}  |λ|={mag_u[i]:.4f}  ∠={float(np.angle(Lambda[unstable_mask][i]))*180/np.pi:+.1f}°  UNSTABLE"804                  for i in range(len(re_u))]805        fig.add_trace(go.Scatter(x=re_u.tolist(), y=im_u.tolist(), mode='markers', name=f'Unstable ({n_unstable})',806            marker=dict(size=sz_u, color='#f87171', opacity=0.9,807                        line=dict(color='rgba(255,255,255,0.25)', width=0.8)),808            hovertext=htxt_u, hoverinfo='text'))809 810    title_txt = (f'<span style="font-family:Bebas Neue;font-size:14px;color:#f0f4ff">Eigenvalue Spectrum</span>'811                 f'  <span style="font-size:11px;color:#06d6a0">● {n_stable} stable</span>'812                 f'  <span style="font-size:11px;color:#f87171">● {n_unstable} unstable</span>')813 814    fig.update_layout(**BASE, height=320,815        title=dict(text=title_txt, font=dict(size=13), x=0.5),816        xaxis=dict(title=dict(text='Re(λ)', font=dict(size=12, color='#9aaccc')),817                   tickfont=dict(size=10, color='#5a6d94'), range=[-1.55, 1.55],818                   gridcolor='rgba(255,255,255,0.05)', zeroline=False, linecolor='#2d3654'),819        yaxis=dict(title=dict(text='Im(λ)', font=dict(size=12, color='#9aaccc')),820                   tickfont=dict(size=10, color='#5a6d94'), range=[-1.55, 1.55],821                   scaleanchor='x', gridcolor='rgba(255,255,255,0.05)', zeroline=False, linecolor='#2d3654'),822        legend=dict(font=dict(family='IBM Plex Mono', size=10, color='#9aaccc'),823                    bgcolor='rgba(13,17,23,0.7)', orientation='h', y=1.12, x=0.5, xanchor='center'))824    return fig825 826def make_frequency_chart(Omega):827    """Growth rate vs. frequency map of DMD modes."""828    growth = np.real(Omega)829    freq   = np.imag(Omega) / (2 * np.pi)830 831    stable_m   = growth <= 0832    n_decaying = int(np.sum(stable_m))833    n_growing  = len(Omega) - n_decaying834 835    fig = go.Figure()836 837    # Zero line838    fig.add_hline(y=0, line=dict(color='rgba(255,209,102,0.4)', width=1.2, dash='dot'))839 840    # Decaying modes841    if n_decaying > 0:842        fig.add_trace(go.Scatter(843            x=freq[stable_m].tolist(), y=growth[stable_m].tolist(),844            mode='markers', name=f'Decaying ({n_decaying})',845            marker=dict(size=8, color='#06d6a0', opacity=0.85,846                        line=dict(color='rgba(255,255,255,0.2)', width=0.6)),847            hovertemplate='freq=%{x:.4f}<br>growth=%{y:.4f}<extra>Decaying</extra>'))848 849    # Growing modes850    if n_growing > 0:851        fig.add_trace(go.Scatter(852            x=freq[~stable_m].tolist(), y=growth[~stable_m].tolist(),853            mode='markers', name=f'Growing ({n_growing})',854            marker=dict(size=8, color='#f87171', opacity=0.85,855                        line=dict(color='rgba(255,255,255,0.2)', width=0.6)),856            hovertemplate='freq=%{x:.4f}<br>growth=%{y:.4f}<extra>Growing</extra>'))857 858    fig.update_layout(**BASE, height=235,859        title=_title('Growth / Frequency Map', sz=13),860        xaxis=dict(title=dict(text='Frequency (cycles/step)', font=dict(size=11, color='#9aaccc')),861                   tickfont=dict(size=10, color='#5a6d94'),862                   gridcolor='rgba(255,255,255,0.05)', zeroline=False, linecolor='#2d3654'),863        yaxis=dict(title=dict(text='Growth Rate', font=dict(size=11, color='#9aaccc')),864                   tickfont=dict(size=10, color='#5a6d94'),865                   gridcolor='rgba(255,255,255,0.05)', zeroline=False, linecolor='#2d3654'),866        legend=dict(font=dict(family='IBM Plex Mono', size=10, color='#9aaccc'),867                    bgcolor='rgba(13,17,23,0.7)', orientation='h', y=1.18, x=0.5, xanchor='center'))868    return fig869 870 871# ══════════════════════════════════════════════════════════════════════════════872# HEADER873# ══════════════════════════════════════════════════════════════════════════════874st.markdown("""875<div class="hero-wrap">876  <div class="hero-eyebrow">ERC Synergy Grant · FatResCon · VUT Brno · 2026</div>877  <div class="hero-title">Tensor-Train <em>DMD</em> Online Solver</div>878  <div class="hero-sub">Dynamic Mode Decomposition for Spatiotemporal Tensor Data</div>879</div>880""", unsafe_allow_html=True)881 882ctrl_col, viz_col = st.columns([1, 1.2])883 884 885# ══════════════════════════════════════════════════════════════════════════════886# CONTROL PANEL887# ══════════════════════════════════════════════════════════════════════════════888with ctrl_col:889 890    # ── ① INPUT ───────────────────────────────────────────────────────────────891    st.markdown('<div class="sec">① Input Data</div>', unsafe_allow_html=True)892    st.markdown(893        '<div class="info-box">Upload spatiotemporal tensor — <b>.npy · .mat · .csv</b><br>'894        '📁 <a href="https://drive.google.com/drive/folders/1lkrPxlxPeS73P69qs4izPLXCRR25U2kC" '895        'target="_blank">Demo test data ↗</a></div>',896        unsafe_allow_html=True)897    uploaded_file = st.file_uploader("", type=['npy','mat','csv'], label_visibility="collapsed")898 899    data    = st.session_state.raw_data900    mapping = st.session_state.axis_mapping901 902    if uploaded_file:903        try:904            file_name = uploaded_file.name905            file_ext  = file_name.split('.')[-1].lower()906            if file_ext == 'npy':907                new_data = np.load(uploaded_file)908            elif file_ext == 'mat':909                mat  = sio.loadmat(uploaded_file)910                keys = [k for k in mat.keys() if not k.startswith('__')]911                arrs = [mat[k] for k in keys if isinstance(mat[k], np.ndarray)]912                if not arrs: raise ValueError("No data found in .mat file")913                new_data = max(arrs, key=lambda x: x.size)914            elif file_ext == 'csv':915                new_data = np.loadtxt(uploaded_file, delimiter=',')916            else:917                raise ValueError(f"Unsupported format: {file_ext}")918 919            if data is None or new_data.shape != getattr(data, 'shape', None):920                st.session_state.raw_data = new_data921                for k in ['reconstruction','errors','dmd_results']:922                    st.session_state[k] = None923                st.session_state.lambda_clipped = False924                st.session_state.compute_log    = []925                st.session_state.file_format    = file_ext926                st.session_state.file_name      = file_name927                data = new_data; ndim = data.ndim928                st.session_state.axis_mapping = {929                    'x': 0 if ndim >= 1 else None,930                    'y': 1 if ndim >= 2 else None,931                    'z': 2 if ndim >= 4 else None,932                    't': ndim - 1 if ndim >= 2 else None933                }934                mapping = st.session_state.axis_mapping935                t_max_init = data.shape[mapping['t']] if mapping['t'] is not None else 100936                st.session_state.t_start = 1937                st.session_state.t_end   = min(40, t_max_init)938                st.session_state.prev_axis_mapping  = mapping.copy()939                st.session_state.prev_t_start       = st.session_state.t_start940                st.session_state.prev_t_end         = st.session_state.t_end941                st.session_state.prev_dmd_method    = 'TT-DMD'942                st.session_state.prev_control_mode  = 'Fixed Rank'943                st.session_state.prev_rank          = 10944                st.session_state.prev_eps           = 1e-5945                add_log("══════════════════════════════════")946                add_log(f"FILE  {file_name}  [{file_ext.upper()}]")947                add_log(f"Shape   {data.shape}")948                add_log(f"dtype   {data.dtype}   MB {data.nbytes/1024/1024:.2f}")949                add_log(f"Range   [{data.min():.3e}, {data.max():.3e}]")950                add_log("Auto-axis mapping:")951                for axis, idx2 in mapping.items():952                    if idx2 is not None:953                        add_log(f"  {axis} → dim {idx2}  size={data.shape[idx2]}")954                add_log(f"Training window  t=[1, {st.session_state.t_end}]")955                add_log("══════════════════════════════════")956        except Exception as e:957            st.error(str(e))958 959    if data is not None:960        t_ax = mapping.get('t')961        t_len_disp = data.shape[t_ax] if t_ax is not None else '—'962        mem_mb = data.nbytes / 1024 / 1024963        st.markdown(f"""964<div class="mcard-row">965  <div class="mcard teal">966    <div class="mcard-label">Dimensions</div>967    <div class="mcard-val">{data.ndim}<span style="font-size:1rem;color:#5a6d94"> D</span></div>968    <div class="mcard-sub">{data.shape}</div>969  </div>970  <div class="mcard blue">971    <div class="mcard-label">Time Steps</div>972    <div class="mcard-val">{t_len_disp}</div>973    <div class="mcard-sub">{data.dtype} · {mem_mb:.2f} MB</div>974  </div>975</div>""", unsafe_allow_html=True)976 977    # ── ② DIMENSION ASSIGNMENT ────────────────────────────────────────────────978    st.markdown('<div class="sec">② Dimension Assignment</div>', unsafe_allow_html=True)979    if data is not None:980        shape, ndim = data.shape, data.ndim981        opts = ["--"] + [f"d{i}: {shape[i]}" for i in range(ndim)]982        def g_idx(k):983            v = mapping.get(k); return (v if v is not None else -1) + 1984        def p_dim(v): return None if v == "--" else int(v.split(":")[0].replace("d",""))985        c1, c2, c3 = st.columns(3)986        mx = c1.selectbox("x", opts, index=g_idx('x'), key="ax_x")987        my = c2.selectbox("y", opts, index=g_idx('y'), key="ax_y")988        mz = c3.selectbox("z", opts, index=g_idx('z'), key="ax_z")989        mt = st.selectbox("t (temporal)", opts, index=g_idx('t'), key="ax_t")990        new_m = {'x': p_dim(mx), 'y': p_dim(my), 'z': p_dim(mz), 't': p_dim(mt)}991        asgn = [v for v in new_m.values() if v is not None]992        if len(asgn) == len(set(asgn)):993            if st.session_state.prev_axis_mapping != new_m:994                add_log("Axis mapping: " + "  ".join(f"{k}→d{v}" for k,v in new_m.items() if v is not None))995                st.session_state.prev_axis_mapping = new_m.copy()996            st.session_state.axis_mapping = new_m; mapping = new_m997 998    # ── ③ TRAINING WINDOW ─────────────────────────────────────────────────────999    st.markdown('<div class="sec">③ Training Window</div>', unsafe_allow_html=True)1000    t_idx = mapping.get('t')1001    t_max = data.shape[t_idx] if data is not None and t_idx is not None else 1001002    if st.session_state.t_end   > t_max: st.session_state.t_end   = t_max1003    if st.session_state.t_start > t_max: st.session_state.t_start = 11004    if st.session_state.t_start > st.session_state.t_end: st.session_state.t_start = st.session_state.t_end1005 1006    tc1, tc2 = st.columns(2)1007    new_ts = tc1.number_input("t_start", 1, t_max, st.session_state.t_start, key=f"ts_{st.session_state.temporal_sync_key}")1008    new_te = tc2.number_input("t_end",   1, t_max, st.session_state.t_end,   key=f"te_{st.session_state.temporal_sync_key}")1009    if new_ts != st.session_state.t_start or new_te != st.session_state.t_end:1010        st.session_state.t_start = new_ts; st.session_state.t_end = new_te1011        st.session_state.temporal_sync_key += 1; st.rerun()1012    new_range = st.slider("tw", 1, t_max, (st.session_state.t_start, st.session_state.t_end),1013                          key=f"tsl_{st.session_state.temporal_sync_key}", label_visibility="collapsed")1014    if new_range[0] != st.session_state.t_start or new_range[1] != st.session_state.t_end:1015        st.session_state.t_start = new_range[0]; st.session_state.t_end = new_range[1]1016        st.session_state.temporal_sync_key += 1; st.rerun()1017 1018    t_start, t_end = st.session_state.t_start, st.session_state.t_end1019 1020    if data is not None and t_idx is not None:1021        total_t  = data.shape[t_idx]1022        tr_pct   = (t_end - t_start + 1) / total_t * 1001023        pr_pct   = max(0, total_t - t_end) / total_t * 1001024        st.markdown(f"""1025<div class="pct-row">1026  <div class="pct-box train">1027    <div class="pct-num t">{tr_pct:.0f}%</div>1028    <div class="pct-lbl">Training</div>1029  </div>1030  <div class="pct-box pred">1031    <div class="pct-num p">{pr_pct:.0f}%</div>1032    <div class="pct-lbl">Prediction</div>1033  </div>1034</div>""", unsafe_allow_html=True)1035 1036    if st.session_state.prev_t_start != t_start or st.session_state.prev_t_end != t_end:1037        add_log(f"Training window  t=[{t_start}, {t_end}]")1038        st.session_state.prev_t_start = t_start; st.session_state.prev_t_end = t_end1039 1040    # ── ④ ALGORITHM ───────────────────────────────────────────────────────────1041    st.markdown('<div class="sec">④ Algorithm Configuration</div>', unsafe_allow_html=True)1042    dmd_method = st.radio("Algorithm", ["TT-DMD", "Classic DMD"], horizontal=True, key="dmd_method_select")1043    st.session_state.dmd_method = dmd_method1044    if st.session_state.prev_dmd_method != dmd_method:1045        add_log(f"Algorithm: {dmd_method}"); st.session_state.prev_dmd_method = dmd_method1046 1047    control_mode = st.radio("Truncation", ["Fixed Rank", "Energy Threshold (ε)"], horizontal=True, key="ctrl_mode")1048    if st.session_state.prev_control_mode != control_mode:1049        add_log(f"Truncation: {control_mode}"); st.session_state.prev_control_mode = control_mode1050 1051    if control_mode == "Fixed Rank":1052        lbl_r = "TT-rank (r)" if dmd_method == "TT-DMD" else "SVD rank (r)"1053        rmax  = 80 if dmd_method == "TT-DMD" else 1001054        rank_val = st.slider(lbl_r, 2 if dmd_method=="TT-DMD" else 1, rmax, 10, key="rank_sl")1055        eps_val  = None1056        if st.session_state.prev_rank != rank_val:1057            add_log(f"Rank r={rank_val}"); st.session_state.prev_rank = rank_val1058    else:1059        eps_val  = st.select_slider("ε", [1e-2,1e-3,1e-4,1e-5,1e-6,1e-7], value=1e-5, key="eps_sl")1060        rank_val = None1061        if st.session_state.prev_eps != eps_val:1062            add_log(f"ε={eps_val:.0e}"); st.session_state.prev_eps = eps_val1063 1064    run_disabled = (data is None or t_idx is None)1065    if st.button(f"▶  Execute {dmd_method}", key="run_btn", use_container_width=True,1066                 disabled=run_disabled, type="primary"):1067        st.session_state.compute_log = []1068        t_total = data.shape[t_idx]1069        add_log("══════════════════════════════════")1070        add_log(f"STARTING  {dmd_method}")1071        add_log(f"Data  {data.shape}  {data.dtype}")1072        add_log(f"Train  t=[{t_start},{t_end}]  {(t_end-t_start+1)/t_total*100:.1f}%")1073        add_log(f"Trunc  {'rank='+str(rank_val) if rank_val else 'eps='+str(eps_val)}")1074        add_log("══════════════════════════════════")1075 1076        with st.spinner("Computing…"):1077            try:1078                data_t = np.moveaxis(data, t_idx, -1)1079                add_log(f"Reshaped (t-last): {data_t.shape}")1080 1081                if dmd_method == "TT-DMD":1082                    tt_ranks = [rank_val]*(data_t.ndim-1) if rank_val else None1083                    epsilon  = eps_val if eps_val else 1e-51084                    add_log("Step 1/4  TT-SVD…")1085                    Phi, Omega, b, Lambda, tt_model = run_tt_dmd(1086                        data_t, t_start, t_end, tt_ranks, epsilon, log_fn=add_log)1087                    add_log(f"TT-DMD done  {Phi.shape[1]} modes")1088                else:1089                    add_log("Step 1/4  Classic SVD-DMD…")1090                    Phi, Omega, b, Lambda = run_classic_dmd(1091                        data_t, t_start, t_end,1092                        svd_rank=rank_val if rank_val else 0,1093                        svd_rank_epsilon=eps_val, log_fn=add_log)1094                    tt_model = None1095                    add_log(f"Classic DMD done  {len(Lambda)} modes")1096 1097                add_log("Step 2/4  Eigenvalue stability check…")1098                lambda_abs = np.abs(Lambda)1099                n_unstable = int(np.sum(lambda_abs > 1))1100                add_log(f"|λ| range [{np.min(lambda_abs):.6f}, {np.max(lambda_abs):.6f}]")1101                if n_unstable > 0:1102                    st.session_state.lambda_clipped = True1103                    add_log(f"⚠ {n_unstable} unstable → projected to unit circle")1104                    Lambda = np.where(lambda_abs > 1, Lambda / lambda_abs, Lambda)1105                    Omega  = np.log(Lambda + 1e-14)1106                else:1107                    st.session_state.lambda_clipped = False1108                    add_log(f"✓ All {len(Lambda)} eigenvalues stable")1109 1110                add_log("Step 3/4  Prediction…")1111                n_t = data_t.shape[-1]1112                result_shape = tuple(list(data_t.shape[:-1]) + [n_t])1113                if dmd_method == "Classic DMD":1114                    X_pred = classic_dmd_predict(Phi, Omega, b, np.arange(n_t), result_shape)1115                else:1116                    X_pred = predict_tt_dmd_from_model(tt_model, np.arange(n_t))1117                recon = np.moveaxis(X_pred, -1, t_idx)1118                st.session_state.reconstruction = recon1119                st.session_state.dmd_results = {1120                    'method': dmd_method, 'Phi': Phi, 'Omega': Omega,1121                    'b': b, 'Lambda': Lambda,1122                    'tt_model': tt_model if dmd_method=="TT-DMD" else None1123                }1124 1125                add_log("Step 4/4  Error analysis…")1126                pred_t = np.moveaxis(recon, t_idx, -1)1127                gt_t   = np.moveaxis(data,  t_idx, -1)1128                errs   = np.array([1129                    np.linalg.norm(pred_t[...,ti] - gt_t[...,ti]) /1130                    (np.linalg.norm(gt_t[...,ti]) + 1e-14)1131                    for ti in range(gt_t.shape[-1])1132                ])1133                st.session_state.errors = errs1134                add_log(f"Train err  mean={np.mean(errs[:t_end]):.4e}  max={np.max(errs[:t_end]):.4e}")1135                if t_end < n_t:1136                    add_log(f"Pred  err  mean={np.mean(errs[t_end:]):.4e}  max={np.max(errs[t_end:]):.4e}")1137                add_log("══════════════════════════════════")1138                add_log("✓  COMPUTATION COMPLETE")1139                add_log("══════════════════════════════════")1140 1141            except Exception as e:1142                add_log(f"ERROR: {e}")1143                import traceback; add_log(traceback.format_exc())1144                st.error(str(e))1145 1146    if st.session_state.lambda_clipped:1147        st.markdown('<div class="warn-box">⚠  Unstable eigenvalues (|λ|>1) projected onto unit circle for stability</div>',1148                    unsafe_allow_html=True)1149 1150    # ── RESULTS STATS ─────────────────────────────────────────────────────────1151    dmd_r = st.session_state.dmd_results1152    if dmd_r is not None:1153        l        = dmd_r['Lambda']1154        n_stable = int(np.sum(np.abs(l) <= 1))1155        n_modes  = dmd_r['Phi'].shape[1]1156        errs     = st.session_state.errors1157        tr_e = f"{np.mean(errs[:t_end]):.2e}" if errs is not None else "—"1158        pr_e = f"{np.mean(errs[t_end:]):.2e}" if errs is not None and t_end < len(errs) else "—"1159 1160        dmd_r_info = st.session_state.dmd_results1161        r_v_val = dmd_r_info['tt_model']['r_v'] if dmd_r_info.get('tt_model') else '—'1162 1163        st.markdown(f"""1164<div class="stat-row">1165  <div class="stat-cell">1166    <div class="stat-num" style="color:#f0f4ff">{n_modes}</div>1167    <div class="stat-lbl">Modes</div>1168  </div>1169  <div class="stat-cell">1170    <div class="stat-num" style="color:#06d6a0">{n_stable}</div>1171    <div class="stat-lbl">Stable λ</div>1172  </div>1173  <div class="stat-cell">1174    <div class="stat-num" style="color:#ffd166;font-size:1.3rem">{tr_e}</div>1175    <div class="stat-lbl">Train Err</div>1176  </div>1177  <div class="stat-cell">1178    <div class="stat-num" style="color:#ff6b35;font-size:1.3rem">{pr_e}</div>1179    <div class="stat-lbl">Pred Err</div>1180  </div>1181</div>""", unsafe_allow_html=True)1182 1183        if dmd_r_info.get('tt_model'):1184            rv = dmd_r_info['tt_model']['r_v']1185            rd = dmd_r_info['tt_model']['r_d']1186            st.markdown(f"""1187<div class="mcard-row">1188  <div class="mcard purple">1189    <div class="mcard-label">Latent TT rank r_v</div>1190    <div class="mcard-val">{rv}</div>1191    <div class="mcard-sub">SVD of latent dynamics</div>1192  </div>1193  <div class="mcard orange">1194    <div class="mcard-label">Temporal rank r_d</div>1195    <div class="mcard-val">{rd}</div>1196    <div class="mcard-sub">Last TT core</div>1197  </div>1198</div>""", unsafe_allow_html=True)1199 1200    # ── ⑤ EXPORT ──────────────────────────────────────────────────────────────

Showing the first 1,200 of 1421 lines. Download the file for the rest.