Source code for qmlhc.metrics.control

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Control Metrics
---------------
Stability and responsiveness metrics for dynamic systems.

Provided metrics
----------------
- ``overshoot``: maximum relative overshoot of the response over the target.
- ``settling_time``: samples required to remain within a tolerance band.
- ``robustness``: inverse sensitivity to disturbances (MSE-based).
"""

from __future__ import annotations

import numpy as np

from ..core.types import TensorLike


[docs] def overshoot(y_true: TensorLike, y_pred: TensorLike) -> float: """ Maximum relative overshoot of the response over the reference. The metric is computed as: ``max(0, max(y_pred) - max(y_true)) / |max(y_true)|``. If the reference maximum is zero, returns ``0.0``. Parameters ---------- y_true : TensorLike Reference/target signal, shape ``(T,)``. y_pred : TensorLike Response/prediction signal, shape ``(T,)``. Returns ------- float Relative overshoot (unitless, ``>= 0``). """ t = np.asarray(y_true, dtype=float).reshape(-1) p = np.asarray(y_pred, dtype=float).reshape(-1) max_ref = np.max(t) if max_ref == 0: return 0.0 max_err = np.max(np.maximum(0.0, p - max_ref)) return float(max_err / abs(max_ref))
[docs] def settling_time(y_true: TensorLike, y_pred: TensorLike, tol: float = 0.05) -> int: """ Samples until the response stays within a tolerance band around the target. The tolerance band is defined around the final target value: ``[ref*(1 - tol), ref*(1 + tol)]``, where ``ref = y_true[-1]``. The function scans backward and returns the last index violating the band, plus one. If the entire sequence is within band, returns ``0``. Parameters ---------- y_true : TensorLike Reference/target signal, shape ``(T,)``. y_pred : TensorLike Response/prediction signal, shape ``(T,)``. tol : float, optional Relative tolerance (e.g., ``0.05`` for ±5%), by default ``0.05``. Returns ------- int Settling time in samples. """ t = np.asarray(y_true, dtype=float).reshape(-1) p = np.asarray(y_pred, dtype=float).reshape(-1) ref = t[-1] lower = ref * (1 - tol) upper = ref * (1 + tol) for i in range(len(p) - 1, -1, -1): if not (lower <= p[i] <= upper): return i + 1 return 0
[docs] def robustness(y_true: TensorLike, y_pred: TensorLike) -> float: """ Inverse sensitivity to disturbances based on mean squared error. Defined as ``1 / (1 + MSE(y_true, y_pred))``. Values lie in ``(0, 1]``, where larger is better (more robust). Parameters ---------- y_true : TensorLike Reference/target signal, shape ``(T,)``. y_pred : TensorLike Response/prediction signal, shape ``(T,)``. Returns ------- float Robustness score in ``(0, 1]``. """ t = np.asarray(y_true, dtype=float).reshape(-1) p = np.asarray(y_pred, dtype=float).reshape(-1) mse = np.mean((t - p) ** 2) return float(1.0 / (1.0 + mse))