def evaluate_basin(model: nn.Module,
                   loader: DataLoader) -> Tuple[np.ndarray, np.ndarray]:
    """Evaluate model on a single basin

    Parameters
    ----------
    model : nn.Module
        The PyTorch model to train
    loader : DataLoader
        PyTorch DataLoader containing the basin data in batches.

    Returns
    -------
    preds : np.ndarray
        Array containing the (rescaled) network prediction for the entire data period
    obs : np.ndarray
        Array containing the observed discharge for the entire data period

    """
    model.eval()

    preds, obs = None, None

    with torch.no_grad():
        for data in loader:
            if len(data) == 2:
                x, y = data
                x, y = x.to(DEVICE), y.to(DEVICE)
                p = model(x)[0]
            elif len(data) == 3:
                x_d, x_s, y = data
                x_d, x_s, y = x_d.to(DEVICE), x_s.to(DEVICE), y.to(DEVICE)
                p = model(x_d, x_s[:, 0, :])[0]

            if preds is None:
                preds = p.detach().cpu()
                obs = y.detach().cpu()
            else:
                preds = torch.cat((preds, p.detach().cpu()), 0)
                obs = torch.cat((obs, y.detach().cpu()), 0)

        preds = rescale_features(preds.numpy(), variable='output')
        obs = obs.numpy()
        # set discharges < 0 to zero
        preds[preds < 0] = 0

    return preds, obs
def eval_with_added_noise(model: torch.nn.Module, loader: DataLoader,
                          noise: torch.Tensor) -> float:
    """Evaluate model on a single basin with added noise

    Parameters
    ----------
    model : nn.Module
        The PyTorch model to train
    loader : DataLoader
        PyTorch DataLoader containing the basin data in batches.
    noise : torch.Tensor
        Tensor containing the noise for this evaluation run.
    
    Returns
    -------
    float
        Nash-Sutcliff-Efficiency of the simulations with added noise.
    """
    model.eval()
    preds, obs = None, None
    with torch.no_grad():
        for x_d, x_s, y in loader:
            x_d, x_s, y = x_d.to(DEVICE), x_s.to(DEVICE), y.to(DEVICE)
            batch_noise = noise.repeat(*x_s.size()[:2], 1)
            x_s = x_s.add(batch_noise)
            y_hat = model(x_d, x_s[:, 0, :])[0]

            if preds is None:
                preds = y_hat.detach().cpu()
                obs = y.detach().cpu()
            else:
                preds = torch.cat((preds, y_hat.detach().cpu()), 0)
                obs = torch.cat((obs, y.detach().cpu()), 0)

        obs = obs.numpy()
        preds = rescale_features(preds.numpy(), variable='output')

        # set discharges < 0 to zero
        preds[preds < 0] = 0

        nse = calc_nse(obs[obs >= 0], preds[obs >= 0])
        return nse
def eval_with_added_noise(model: xgb.XGBRegressor, ds_test: CamelsTXT,
                          noise: torch.Tensor) -> float:
    """Evaluate model on a single basin with added noise

    Parameters
    ----------
    model : xgb.XGBRegressor
        The XGBoost model to evaluate
    ds_test : CamelsTXT
        Dataset containing the basin data.
    noise : torch.Tensor
        Tensor containing the noise for this evaluation run.
    
    Returns
    -------
    float
        Nash-Sutcliffe-Efficiency of the simulations with added noise.
    """
    preds, obs = None, None

    x = ds_test.x.reshape(len(ds_test.x), -1).numpy()
    obs = ds_test.y.numpy()

    attributes = ds_test.attributes.repeat(len(x), 1).clone()
    batch_noise = noise.repeat(len(attributes), 1)
    attributes = attributes.add(batch_noise)
    x = np.concatenate([x, attributes.numpy()], axis=1)
    preds = model.predict(x)

    preds = rescale_features(preds, variable='output')

    # set discharges < 0 to zero
    preds[preds < 0] = 0

    nse = calc_nse(obs[obs >= 0], preds[obs.reshape(-1) >= 0])
    return nse
def evaluate_basin(model: xgb.XGBRegressor, ds_test: CamelsTXT,
                   no_static: bool) -> Tuple[np.ndarray, np.ndarray]:
    """Evaluate model on a single basin

    Parameters
    ----------
    model : xgb.XGBRegressor
        The XGBoost model to evaluate
    ds_test : CamelsTXT
        CAMELS dataset containing the basin data
    no_static: bool
        If True, will not include static attributes as input features

    Returns
    -------
    preds : np.ndarray
        Array containing the (rescaled) prediction for the entire data period
    obs : np.ndarray
        Array containing the observed discharge for the entire data period

    """
    preds, obs = None, None

    x = ds_test.x.reshape(len(ds_test.x), -1).numpy()
    obs = ds_test.y.numpy()
    if not no_static:
        x = np.concatenate([x, ds_test.attributes.repeat(len(x), 1).numpy()],
                           axis=1)

    preds = model.predict(x)
    preds = rescale_features(preds, variable='output')

    # set discharges < 0 to zero
    preds[preds < 0] = 0

    return preds, obs