Skip to content

qlroms.utils.metrics

Lightweight diagnostic metrics used to compare ROM trajectories against the FOM.

relative_error_series(X_ref, X_approx, eps=1e-12)

Per-snapshot relative error of (N, T) column-trajectory tensors (torch).

Source code in qlroms/utils/metrics.py
75
76
77
78
79
80
81
82
83
84
85
86
def relative_error_series(X_ref, X_approx, eps: float = 1e-12) -> np.ndarray:
    """Per-snapshot relative error of (N, T) column-trajectory tensors (torch)."""
    import torch

    # cached snapshots are usually CPU while model output sits on the model's device
    X_approx = torch.as_tensor(X_approx, dtype=X_ref.dtype, device=X_ref.device)
    if X_approx.shape != X_ref.shape:
        hint = (" -- looks time-major; pass .T" if X_approx.shape == X_ref.shape[::-1] else "")
        raise ValueError(f"Both trajectories must be (N, T) with COLUMNS = time: got reference "
                         f"{tuple(X_ref.shape)} and approximation {tuple(X_approx.shape)}{hint}.")
    denom = torch.linalg.norm(X_ref, dim=0).clamp_min(eps)
    return (torch.linalg.norm(X_ref - X_approx, dim=0) / denom).cpu().numpy()

one_step_errors_series(X, model)

Open-loop one-step-ahead error: at every snapshot i, step the model ONE step forward from the TRUE state x_i (not from its own previous prediction) and compare to the TRUE next snapshot x_{i+1}. Unlike a free multi-step run, errors here cannot accumulate across steps -- this isolates the one-step fit quality itself (and, for a quantized-local model, shows up as error spikes right at cluster-boundary snapshots).

Source code in qlroms/utils/metrics.py
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
def one_step_errors_series(X, model) -> np.ndarray:
    """Open-loop one-step-ahead error: at every snapshot i, step the model ONE
    step forward from the TRUE state x_i (not from its own previous prediction) and
    compare to the TRUE next snapshot x_{i+1}. Unlike a free multi-step run,
    errors here cannot accumulate across steps -- this isolates the one-step fit
    quality itself (and, for a quantized-local model, shows up as error spikes right
    at cluster-boundary snapshots)."""
    import torch

    X = X.to(model.device, model.rdtype)
    Nt = X.shape[1]
    errs = np.zeros(Nt - 1)
    for i in range(Nt - 1):
        apod_next = model.step(model.project_state(X[:, i:i + 1]))
        x_pred = model.recover_state(apod_next)
        x_true = X[:, i + 1:i + 2]
        errs[i] = float(torch.linalg.norm(x_true - x_pred)
                        / torch.linalg.norm(x_true).clamp_min(1e-12))
    return errs

reconstruction_error_series(X, model)

Pure POD projection+reconstruction relative error per snapshot -- NO dynamics/ time-stepping: for the nearest cluster k, a = Phi_k^T Mw (x - c_k), x_hat = c_k + Phi_k a. Isolates how well the fitted basis alone represents each snapshot, independent of the model's fitted dynamics (contrast one_step_errors_series).

Source code in qlroms/utils/metrics.py
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
def reconstruction_error_series(X, model) -> np.ndarray:
    """Pure POD projection+reconstruction relative error per snapshot -- NO dynamics/
    time-stepping: for the nearest cluster k, a = Phi_k^T Mw (x - c_k), x_hat = c_k +
    Phi_k a. Isolates how well the fitted basis alone represents each snapshot,
    independent of the model's fitted dynamics (contrast one_step_errors_series)."""
    import torch

    X = X.to(model.device, model.rdtype)
    Nt = X.shape[1]
    if model.K == 1:
        cids = torch.zeros(Nt, dtype=torch.long, device=model.device)
    else:
        _, cids_np = model.tree.query(X.cpu().numpy().T, k=1)
        cids = torch.as_tensor(cids_np, dtype=torch.long, device=model.device).reshape(-1)
    X_hat = torch.empty_like(X)
    for k in cids.unique():
        mask = cids == int(k)
        rom_k = model._get_cluster_rom(int(k))
        X_hat[:, mask] = rom_k.recover_state(rom_k.project_state(X[:, mask]))
    return relative_error_series(X, X_hat)

prediction_horizon(rel_err, dt, threshold=0.5)

First time (in physical units) at which rel_err exceeds threshold, matching the ql-ROM papers' T_ph definition (normalized-error threshold, default tau=0.5).

Source code in qlroms/utils/metrics.py
132
133
134
135
136
137
def prediction_horizon(rel_err: np.ndarray, dt: float, threshold: float = 0.5) -> float:
    """First time (in physical units) at which rel_err exceeds `threshold`, matching
    the ql-ROM papers' T_ph definition (normalized-error threshold, default tau=0.5)."""
    over = np.nonzero(np.asarray(rel_err) > threshold)[0]
    n = int(over[0]) if over.size else len(rel_err)
    return n * dt