Source code for pypielm.metrics.metrics
"""Evaluation metrics for PDE solution accuracy.
All functions operate on :class:`torch.Tensor` or array-likes and return
plain Python ``float`` values for easy logging and comparison.
"""
from __future__ import annotations
import numpy as np
import torch
def _to_tensor(x: torch.Tensor | np.ndarray) -> torch.Tensor:
if isinstance(x, np.ndarray):
return torch.from_numpy(x)
return x
[docs]
def rmse(y_pred: torch.Tensor | np.ndarray, y_true: torch.Tensor | np.ndarray) -> float:
"""Root Mean Squared Error.
.. math:: \\text{RMSE} = \\sqrt{\\frac{1}{N} \\|\\hat{u} - u\\|_2^2}
Args:
y_pred: Predicted values, shape ``(N,)`` or ``(N, d)``.
y_true: Reference values, same shape.
Returns:
Scalar RMSE.
"""
p = _to_tensor(y_pred).float()
t = _to_tensor(y_true).float()
return float(torch.sqrt(torch.mean((p - t) ** 2)).item())
[docs]
def mae(y_pred: torch.Tensor | np.ndarray, y_true: torch.Tensor | np.ndarray) -> float:
"""Mean Absolute Error.
.. math:: \\text{MAE} = \\frac{1}{N} \\|\\hat{u} - u\\|_1
Args:
y_pred: Predicted values.
y_true: Reference values.
Returns:
Scalar MAE.
"""
p = _to_tensor(y_pred).float()
t = _to_tensor(y_true).float()
return float(torch.mean(torch.abs(p - t)).item())
[docs]
def relative_l2(
y_pred: torch.Tensor | np.ndarray,
y_true: torch.Tensor | np.ndarray,
eps: float = 1e-12,
) -> float:
"""Relative L₂ error (also called normalised RMSE in the benchmark).
.. math::
\\epsilon_{L_2} = \\frac{\\|\\hat{u} - u\\|_2}{\\|u\\|_2 + \\epsilon}
Args:
y_pred: Predicted values.
y_true: Reference values.
eps: Small constant to avoid division by zero.
Returns:
Scalar relative L₂ error.
"""
p = _to_tensor(y_pred).float()
t = _to_tensor(y_true).float()
return float((torch.norm(p - t) / (torch.norm(t) + eps)).item())
[docs]
def max_error(y_pred: torch.Tensor | np.ndarray, y_true: torch.Tensor | np.ndarray) -> float:
"""Maximum absolute pointwise error (L∞ norm).
.. math:: \\epsilon_{\\infty} = \\max_i |\\hat{u}_i - u_i|
Args:
y_pred: Predicted values.
y_true: Reference values.
Returns:
Scalar L∞ error.
"""
p = _to_tensor(y_pred).float()
t = _to_tensor(y_true).float()
return float(torch.max(torch.abs(p - t)).item())
[docs]
def r2_score(y_pred: torch.Tensor | np.ndarray, y_true: torch.Tensor | np.ndarray) -> float:
"""Coefficient of determination R².
.. math::
R^2 = 1 - \\frac{\\text{SS}_\\text{res}}{\\text{SS}_\\text{tot}}
= 1 - \\frac{\\|u - \\hat{u}\\|_2^2}{\\|u - \\bar{u}\\|_2^2}
Args:
y_pred: Predicted values.
y_true: Reference values.
Returns:
Scalar R² (1.0 is perfect, can be negative for bad models).
"""
p = _to_tensor(y_pred).float()
t = _to_tensor(y_true).float()
ss_res = torch.sum((t - p) ** 2)
ss_tot = torch.sum((t - t.mean()) ** 2)
return float((1.0 - ss_res / (ss_tot + 1e-12)).item())
[docs]
class MetricsBundle:
"""Compute and store all standard metrics in one call.
Args:
y_pred: Predicted values, shape ``(N,)`` or ``(N, d)``.
y_true: Ground truth values, same shape.
Attributes:
rmse: Root mean squared error.
mae: Mean absolute error.
rel_l2: Relative L₂ error.
max_err: Maximum absolute error.
r2: Coefficient of determination.
Example::
mb = MetricsBundle(model.predict(X_test), y_test)
print(mb)
"""
def __init__(self, y_pred: torch.Tensor | np.ndarray, y_true: torch.Tensor | np.ndarray) -> None:
self.rmse: float = rmse(y_pred, y_true)
self.mae: float = mae(y_pred, y_true)
self.rel_l2: float = relative_l2(y_pred, y_true)
self.max_err: float = max_error(y_pred, y_true)
self.r2: float = r2_score(y_pred, y_true)
[docs]
def to_dict(self) -> dict[str, float]:
"""Return metrics as a plain dictionary."""
return {
"rmse": self.rmse,
"mae": self.mae,
"rel_l2": self.rel_l2,
"max_err": self.max_err,
"r2": self.r2,
}
def __repr__(self) -> str:
return (
f"MetricsBundle("
f"rmse={self.rmse:.4e}, "
f"rel_l2={self.rel_l2:.4e}, "
f"r2={self.r2:.4f})"
)