Skip to content

Downscale Sentinel-3 Data

Configuration

s3lst_ds.downscaling.downscale.downscale_config.DownscaleConfig dataclass

Configurations for wrangling the data of timestamps of interest, training a downscaler using the coarse data of the training timestamps, downscaling the data of the inference timestamps with the model as well as scoring the downscaler and returning or writing the results to files.

Attributes:

Name Type Description
data_wrangler DataWrangler or Path or None, default=None

Data wrangler or a path to a Joblib file containing it. If not issued, a data wrangler is created from scratch using the data_wrangler_-prefixed parameters of the present DownscaleConfig instance. Note that transform parameter of the downscaler is in any case enforced (therefore, transforming/re-transforming the wrangled data) on the one of the data wrangler regardless of the previous value. Also, data_wrangler_max_workers parameter of the present DownscaleConfig instance is also in any case enforced.

data_wrangler_path_sentinel3 Path or None, default=None

If data_wrangler is not issued: path to directory containing Sentinel-3 product folders whose data is to be wrangled. Each of such folders must contain georeferenced Sentinel-3 SLSTR Level-2 LST product file (https://sentiwiki.copernicus.eu/web/slstr-products#S3-SLSTR-Products-L2-LST-Products) as well as a georeferenced Sentinel-3 Synergy Level-2 product file (https://sentiwiki.copernicus.eu/web/synergy-products#SYNERGYProducts-L2SYNSDRprocessingS3-Synergy-Products-L2-SYN-SDR-processing). Furthermore, the name of such folders must correspond to the respective start sensing time in the format "YYYYMMDDTHHMMSS".

data_wrangler_path_spatial_pred Path or None, default=None

If data_wrangler is not issued: path to a NetCDF file with the spatial predictor data whose data is to be wrangled. If not set, no spatial predictor data is considered.

data_wrangler_aoi str or Path or None, default=None

If data_wrangler is not issued: WKT string or path to AOI geometry file to mask out the data. The data wrangler will add the AOI to the wrangled data as variable "aoi". If not set, no such variable is defined and no masking is applied.

data_wrangler_path_landsat Path or None, default=None

If data_wrangler is not issued and score is True: Path to the directory containing Landsat 8/9 folders whose data is to be wrangled. Each of such folders must contain a LST.TIF file with georeferenced Landsat 8/9 Level-2 LST data (https://www.usgs.gov/centers/eros/science/usgs-eros-archive-landsat-archives-landsat-8-9-olitirs-collection-2-level-2), having a resolution of 30 m. In the wrangling, such data and Sentinel-3's will be "matched" if the respective folders have the same name (it is implied here that the user had analysed the acquisitions obtained by the two platforms and set the names of the Landsat 8/9 data folders as the ones of Sentinel-3's (start sensing times) whose start sensing times and spatial extents are approximately the same). Note that Landsat data will be solely used if score is True as it may be used for the coarse and fine-scoring of the downscaler in the training and inference timestamps for which such data is available. If no Landsat data is issued or it is not available for the timestamps of interest, only coarse-scoring using Sentinel-3 LST as ground truth is performed.

data_wrangler_vars list[str] or None, default=None

If data_wrangler is not issued: aliases of the variables to be wrangled besides the target (such as predictor, sample_weight and visualization variables). If vars is not issued, but downscaler is, it will be set to the aliases of the predictors (cols_X) considered by the latter. Otherwise, if downscaler is not issued but downscaler_X is, it will be set to downscaler_X, or, if not, to all aliases of the predictors (X) considered by a default DataVars instance (s3lst_ds.utilities.var_utils.DataVars).

data_wrangler_max_workers int, default=1

Number of simultaneous multiple processes to be considered by the data wrangler in wrangling. Note that if negative, one has the following conditions: - -1: all processors are used; - -k: all processors except k-1 are used. This parameter is enforced regardless of the data wrangler being issued or created from scratch.

downscaler PiecewiseDownscaler or Downscaler or Path or None, default=None

Downscaler or a path to a Joblib file containing it. If not issued, a downscaler is created from scratch using the downscaler_-prefixed parameters of the present DownscaleConfig. Note that downscaler_masks and downscaler_max_workers are in any case enforced, regardless of the downscaler being issued or created from scratch.

downscaler_architecture {"single", "multi"}, default="single"

If downscaler is not issued: the architecture of the downscaler to be created: - "single" - for the case of a single-timestamp one (a sub-downscaler per timestamp, trained with solely the coarse data of a timestamp and that infers solely the fine target of that same timestamp); - "multi" - for the case of a multi-timestamp one (a downscaler trained with the coarse data of multiple timestamps and that can infer the fine target of any other).

downscaler_base_model Regressor, default=LinearRegression()

If downscaler is not issued: the regression model to be used as the base model of the downscaler to be created. If not issued, it is set to LinearRegression() by default.

downscaler_X list[str], default=["FVC", "NDWI"]

If downscaler is not issued: aliases of the predictors to be considered by the downscaler to be created. If not issued, it is set to ["FVC", "NDWI"].

downscaler_masks list[str] or None, default=None

Aliases of the mask variables (e.g. ["aoi"]) to regard (wherever the variables have nan values, the respective data records are masked out). If not issued, it is set to [] and no masking is considered by the downscaler. This parameter is enforced regardless of the downscaler being issued or created from scratch.

downscaler_scale {"standardize", "min_max_normalize", None}, default="standardize"

If downscaler is not issued: the scaling method to apply to numerical predictors: - "standardize": to standardize the numerical predictors (zero mean and unit variance); - "min_max_normalize": to min-max normalize the numerical predictors (to the range [0, 1]); - None: to regard the numerical predictors raw (no scaling).

downscaler_encode {"one_hot", "dummy", None}, default="dummy"

If downscaler is not issued: the encoding method to apply to the categorical predictors: - "one_hot": to one-hot encode the categorical predictors; - "dummy": to dummy encode the categorical predictors (one-hot encoding with the first component dropped); - None: to regard the categorical predictors raw (no encoding).

Note that dummy encoding is usually considered in place of one-hot to avoid multicollinearity problems (one may show that a component of a one-hot encoding vector is fully determined by all the other components making it redundant).

downscaler_transform {None, "center", "standardize"}, default=None

If downscaler is not issued: the transform to apply on the coarse target and coarse and fine spatio-temporal predictors from a copy of the wrangled data in each SingleDataWrangler instance of the data_wrangler by using coarse data statistics. The transformations are set in SingleDataWrangler'sdatawith the same names as the original columns with the substring"_trans"suffixed to them. Note that the transformations are timestamp-specific, that is, the computed statistics and the applied transformations in each timestamp solely concern the data of that timestamp. The possible values fordownscaler_transformare: -None- not transforming the data; -"center"- subtracting the mean from the data; -"standardize"` - subtracting the mean from the data and dividing the result by the standard deviation. Note that such transforms are redundant for the case of the single-timestamp architecture. They only take effect for the multi-timestamp architecture.

downscaler_lasso_sel bool, default=False

If downscaler is not issued: whether to use a Lasso regression for selecting the scaled-encoded downscaler_X predictors downstream of the preprocessor. Lasso selection is such that solely the input predictors associated with coefficients of the fitted Lasso regression model having absolute values larger than 1e-5 are selected. Note that the non-encoded downscaler_X predictors are regardlessly considered downstream of the preprocessor.

downscaler_lasso_alpha float, default=1.0

If downscaler is not issued: the regularization strength of the Lasso regression model used for selecting the scaled-encoded downscaler_X predictors downstream of the preprocessor. Such regularization strength is the multiplying constant of the weight vector L1-norm (sum of the absolute values of the components) in the Lasso regression objective function. The larger the value, the stronger the regularization. Note that this parameter only takes effect if downscaler_lasso_sel is True.

downscaler_max_workers int, default=1

Number of simultaneous multiple processes to be considered by the downscaler (in training, prediction and scoring). Note that if negative, one has the following conditions: - -1: all processors are used; - -k: all processors except k-1 are used. This parameter is enforced regardless of the downscaler being issued or created from scratch.

retrain bool, default=True

If downscaler is issued, its architecture is multi-timestamp and it had been trained: whether to retrain the downscaler with the coarse data of the training timestamps. Note that training/retraining is in any case considered if the model had not been trained or its architecture is a single-timestamp one. A single-timestamp architecture is such that inference of the fine target of some timestamp can only be done with a downscaler trained with the coarse data of that same timestamp.

timestamps_infer list[pd.Timestamp] or list[str] or None, default=None

Timestamps for inferring fine target with the downscaler either as pd.Timestamp values or in any format parsable by pd.Timestamp (e.g. "YYYY-MM-DD HH:MM:SS"). These must correspond to the start sensing times of the respective acquisitions. Furthermore, they must be part of the timestamps of the data_wrangler if it was issued, or data_wrangler_path_sentinel3 if it was not. If timestamps_infer is not issued, it will be set to all such timestamps.

timestamps_fit list[pd.Timestamp] or list[str] or None, default=None

Timestamps for training the downscaler either as pd.Timestamp values or in any format parsable by pd.Timestamp (e.g. "YYYY-MM-DD HH:MM:SS"). These must correspond to the start sensing times of the respective acquisitions. Furthermore, they must be part of the timestamps of the data_wrangler if it was issued, or data_wrangler_path_sentinel3 if it was not. If timestamps_fit is not issued and the downscaler is a multi-timestamp one, it will be set to all such timestamps. If the architecture of the downscaler is single-timestamp, timestamps_fit is set to timestamps_infer regardless of the issued value. Note that in the case of the single-timestamp architecture, inference of the fine target of some timestamp can only be done with a downscaler trained with the coarse data of that same timestamp.

sample_weight_fit str or None, default=None

Alias of the variable to be regarded as sample weight for training the downscaler. If not issued, no sample weight in training is considered.

sample_weight_score str or None, default=None

Alias of the variable to be regarded as sample weight for scoring the downscaler. If not issued, no sample weight in scoring is considered.

score bool, default=True

Whether to score the predictions in the training and inference timestamps.

scorers list[str], default=["r2", "rmse", "mae", "mbe"]

Aliases of the scorers to consider in scoring.

correct bool, default=True

Whether to correct the predicted fine raw target for each image (from fine predictors and masks, X_and_mask_fine) using the finely-resampled residual for the prediction of the coarse raw target (from coarse predictors and masks, X_and_mask_coarse).

gridded bool, default=True

Whether to get the predicted fine raw target of each image in grid form (as an xr.DataArray) or in flattened form (as a pd.Series).

dims tuple or None, default=None

If gridded is True: labels for the dimensions of the predicted gridded target. If not issued, it is set to ("lat", "lon") by default.

attrs dict or None, default=None

If gridded is True: attributes to set in the predicted gridded target. If not issued, it is set as in accordance with the CF conventions (https://cf-convention.github.io/Data/cf-conventions/cf-conventions-1.13/cf-conventions.pdf#temperature-units):

{
    "standard_name": "land_surface_temperature",
    "long_name": "Land surface temperature",
    "units": "K",
}

path_out Path or None, default=None

The directory path to save the downscaled LST data, obtained scores, downscaler and data wrangler. If not issued, the results are instead returned.

file_ext_grid str, default=".nc"

If gridded is True and path_out is issued: The file extension to use when writing the gridded predicted fine target to file (e.g. ".tif" for GeoTIFF and ".nc" for NetCDF). If path_out is issued and gridded is False, the predicted fine target is written in flattened form with the ".csv" extension.

out_data_wrangler bool, default=True

Whether to return or write (if path_out is issued) the data wrangler.

out_downscaler bool, default=True

Whether to return or write (if path_out is issued) the downscaler.

log_mode {None, "console", "file", "both"}, default="both"

The logging mode for wrangling, training, inferring and scoring: - None: No logging is done; - "console": Logging is done to console only; - "file": Logging is done to a log file only; - "both": Logging is done to both console and a log file. Note the log file would be defined as downscale.log at out_dir.

Methods:

Name Description
__init__
Source code in src/s3lst_ds/downscaling/downscale/downscale_config.py
@dataclass
class DownscaleConfig:
    """
    Configurations for wrangling the data of timestamps of interest, training a
    downscaler using the coarse data of the training timestamps, downscaling the data of
    the inference timestamps with the model as well as scoring the downscaler and
    returning or writing the results to files.

    Attributes
    ----------

    data_wrangler : DataWrangler or Path or None, default=None
        Data wrangler or a path to a Joblib file containing it. If not issued, a data
        wrangler is created from scratch using the `data_wrangler_`-prefixed parameters
        of the present `DownscaleConfig` instance. Note that `transform` parameter of
        the downscaler is in any case enforced (therefore, transforming/re-transforming
        the wrangled data) on the one of the data wrangler regardless of the previous
        value. Also, `data_wrangler_max_workers` parameter of the present
        `DownscaleConfig` instance is also in any case enforced.

    data_wrangler_path_sentinel3 : Path or None, default=None
        If `data_wrangler` is not issued: path to directory containing Sentinel-3
        product folders whose data is to be wrangled. Each of such folders must contain
        georeferenced Sentinel-3 SLSTR Level-2 LST product file
        (https://sentiwiki.copernicus.eu/web/slstr-products#S3-SLSTR-Products-L2-LST-Products)
        as well as a georeferenced Sentinel-3 Synergy Level-2 product file
        (https://sentiwiki.copernicus.eu/web/synergy-products#SYNERGYProducts-L2SYNSDRprocessingS3-Synergy-Products-L2-SYN-SDR-processing).
        Furthermore, the name of such folders must correspond to the respective start
        sensing time in the format "YYYYMMDDTHHMMSS".

    data_wrangler_path_spatial_pred : Path or None, default=None
        If `data_wrangler` is not issued: path to a NetCDF file with the spatial
        predictor data whose data is to be wrangled. If not set, no spatial predictor
        data is considered.

    data_wrangler_aoi : str or Path or None, default=None
        If `data_wrangler` is not issued: WKT string or path to AOI geometry file to
        mask out the data. The data wrangler will add the AOI to the wrangled data as
        variable `"aoi"`. If not set, no such variable is defined and no masking is
        applied.

    data_wrangler_path_landsat : Path or None, default=None
        If `data_wrangler` is not issued and `score` is `True`: Path to the directory
        containing Landsat 8/9 folders whose data is to be wrangled. Each of such
        folders must contain a `LST.TIF` file with georeferenced Landsat 8/9 Level-2
        LST data
        (https://www.usgs.gov/centers/eros/science/usgs-eros-archive-landsat-archives-landsat-8-9-olitirs-collection-2-level-2),
        having a resolution of 30 m. In the wrangling, such data and Sentinel-3's will
        be "matched" if the respective folders have the same name (it is implied here
        that the user had analysed the acquisitions obtained by the two platforms and
        set the names of the Landsat 8/9 data folders as the ones of Sentinel-3's (start
        sensing times) whose start sensing times and spatial extents are approximately
        the same). Note that Landsat data will be solely used if `score` is `True` as it
        may be used for the coarse and fine-scoring of the downscaler in the training
        and inference timestamps for which such data is available. If no Landsat data is
        issued or it is not available for the timestamps of interest, only
        coarse-scoring using Sentinel-3 LST as ground truth is performed.

    data_wrangler_vars : list[str] or None, default=None
        If `data_wrangler` is not issued: aliases of the variables to be wrangled
        besides the target (such as predictor, sample_weight and visualization
        variables). If `vars` is not issued, but `downscaler` is, it will be set to the
        aliases of the predictors (`cols_X`) considered by the latter. Otherwise, if
        `downscaler` is not issued but `downscaler_X` is, it will be set to
        `downscaler_X`, or, if not, to all aliases of the predictors (`X`) considered by
        a default `DataVars` instance (`s3lst_ds.utilities.var_utils.DataVars`).

    data_wrangler_max_workers : int, default=1
        Number of simultaneous multiple processes to be considered by the data wrangler
        in wrangling. Note that if negative, one has the following conditions:
            - `-1`: all processors are used;
            - `-k`: all processors except k-1 are used.
        This parameter is enforced regardless of the data wrangler being issued or
        created from scratch.

    downscaler : PiecewiseDownscaler or Downscaler or Path or None, default=None
        Downscaler or a path to a Joblib file containing it. If not issued, a downscaler
        is created from scratch using the `downscaler_`-prefixed parameters of the
        present `DownscaleConfig`. Note that `downscaler_masks` and
        `downscaler_max_workers` are in any case enforced, regardless of the downscaler
        being issued or created from scratch.

    downscaler_architecture: {"single", "multi"}, default="single"
        If `downscaler` is not issued: the architecture of the downscaler to be created:
            - `"single"` - for the case of a single-timestamp one (a sub-downscaler
            per timestamp, trained with solely the coarse data of a timestamp and that
            infers solely the fine target of that same timestamp);
            - `"multi"` - for the case of a multi-timestamp one (a downscaler trained
            with the coarse data of multiple timestamps and that can infer the fine
            target of any other).

    downscaler_base_model : Regressor, default=LinearRegression()
        If `downscaler` is not issued: the regression model to be used as the base model
        of the downscaler to be created. If not issued, it is set to
        `LinearRegression()` by default.

    downscaler_X : list[str], default=["FVC", "NDWI"]
        If `downscaler` is not issued: aliases of the predictors to be considered by the
        downscaler to be created. If not issued, it is set to `["FVC", "NDWI"]`.

    downscaler_masks: list[str] or None, default=None
        Aliases of the mask variables (e.g. `["aoi"]`) to regard (wherever the variables
        have `nan` values, the respective data records are masked out). If not issued,
        it is set to `[]` and no masking is considered by the downscaler. This parameter
        is enforced regardless of the downscaler being issued or created from scratch.

    downscaler_scale : {"standardize", "min_max_normalize", None}, default="standardize"
        If `downscaler` is not issued: the scaling method to apply to numerical
        predictors:
            - `"standardize"`: to standardize the numerical predictors (zero mean and
            unit variance);
            - `"min_max_normalize"`: to min-max normalize the numerical predictors (to
            the range `[0, 1]`);
            - `None`: to regard the numerical predictors raw (no scaling).

    downscaler_encode : {"one_hot", "dummy", None}, default="dummy"
        If `downscaler` is not issued: the encoding method to apply to the categorical
        predictors:
            - `"one_hot"`: to one-hot encode the categorical predictors;
            - `"dummy"`: to dummy encode the categorical predictors (one-hot
            encoding with the first component dropped);
            - `None`: to regard the categorical predictors raw (no encoding).

        Note that dummy encoding is usually considered in place of one-hot to avoid
        multicollinearity problems (one may show that a component of a one-hot encoding
        vector is fully determined by all the other components making it redundant).

    downscaler_transform : {None, "center", "standardize"}, default=None
        If `downscaler` is not issued: the transform to apply on the coarse target and
        coarse and fine spatio-temporal predictors from a copy of the wrangled `data` in
        each `SingleDataWrangler` instance of the `data_wrangler` by using coarse data
        statistics. The transformations are set in `SingleDataWrangler's `data` with the
        same names as the original columns with the substring `"_trans"` suffixed to
        them. Note that the transformations are timestamp-specific, that is, the
        computed statistics and the applied transformations in each timestamp solely
        concern the data of that timestamp. The possible values for
        `downscaler_transform` are:
            - `None` - not transforming the data;
            - `"center"` - subtracting the mean from the data;
            - `"standardize"` - subtracting the mean from the data and dividing the
            result by the standard deviation.
        Note that such transforms are redundant for the case of the single-timestamp
        architecture. They only take effect for the multi-timestamp architecture.

    downscaler_lasso_sel : bool, default=False
        If `downscaler` is not issued: whether to use a Lasso regression for selecting
        the scaled-encoded `downscaler_X` predictors downstream of the preprocessor.
        Lasso selection is such that solely the input predictors associated with
        coefficients of the fitted Lasso regression model having absolute values larger
        than `1e-5` are selected. Note that the non-encoded `downscaler_X` predictors
        are regardlessly considered downstream of the preprocessor.

    downscaler_lasso_alpha : float, default=1.0
        If `downscaler` is not issued: the regularization strength of the Lasso
        regression model used for selecting the scaled-encoded `downscaler_X` predictors
        downstream of the preprocessor. Such regularization strength is the multiplying
        constant of the weight vector L1-norm (sum of the absolute values of the
        components) in the Lasso regression objective function. The larger the value,
        the stronger the regularization. Note that this parameter only takes effect if
        `downscaler_lasso_sel` is `True`.

    downscaler_max_workers : int, default=1
        Number of simultaneous multiple processes to be considered by the downscaler (in
        training, prediction and scoring). Note that if negative, one has the following
        conditions:
            - `-1`: all processors are used;
            - `-k`: all processors except k-1 are used.
        This parameter is enforced regardless of the downscaler being issued or created
        from scratch.

    retrain : bool, default=True
        If `downscaler` is issued, its architecture is multi-timestamp and it had been
        trained: whether to retrain the downscaler with the coarse data of the training
        timestamps. Note that training/retraining is in any case considered if the model
        had not been trained or its architecture is a single-timestamp one. A
        single-timestamp architecture is such that inference of the fine target of some
        timestamp can only be done with a downscaler trained with the coarse data of
        that same timestamp.

    timestamps_infer : list[pd.Timestamp] or list[str] or None, default=None
        Timestamps for inferring fine target with the downscaler either as
        `pd.Timestamp` values or in any format parsable by `pd.Timestamp` (e.g.
        `"YYYY-MM-DD HH:MM:SS"`). These must correspond to the start sensing times of
        the respective acquisitions. Furthermore, they must be part of the timestamps of
        the `data_wrangler` if it was issued, or `data_wrangler_path_sentinel3` if it
        was not. If `timestamps_infer` is not issued, it will be set to all such
        timestamps.

    timestamps_fit : list[pd.Timestamp] or list[str] or None, default=None
        Timestamps for training the downscaler either as `pd.Timestamp` values or in any
        format parsable by `pd.Timestamp` (e.g. `"YYYY-MM-DD HH:MM:SS"`). These must
        correspond to the start sensing times of the respective acquisitions.
        Furthermore, they must be part of the timestamps of the `data_wrangler` if it
        was issued, or `data_wrangler_path_sentinel3` if it was not. If `timestamps_fit`
        is not issued and the downscaler is a multi-timestamp one, it will be set to all
        such timestamps. If the architecture of the downscaler is single-timestamp,
        `timestamps_fit` is set to `timestamps_infer` regardless of the issued value.
        Note that in the case of the single-timestamp architecture, inference of the
        fine target of some timestamp can only be done with a downscaler trained with
        the coarse data of that same timestamp.

    sample_weight_fit: str or None, default=None
        Alias of the variable to be regarded as sample weight for training the
        downscaler. If not issued, no sample weight in training is considered.

    sample_weight_score: str or None, default=None
        Alias of the variable to be regarded as sample weight for scoring the
        downscaler. If not issued, no sample weight in scoring is considered.

    score : bool, default=True
        Whether to score the predictions in the training and inference timestamps.

    scorers : list[str], default=["r2", "rmse", "mae", "mbe"]
        Aliases of the scorers to consider in scoring.

    correct : bool, default=True
        Whether to correct the predicted fine raw target for each image (from fine
        predictors and masks, `X_and_mask_fine`) using the finely-resampled residual for
        the prediction of the coarse raw target (from coarse predictors and masks,
        `X_and_mask_coarse`).

    gridded : bool, default=True
        Whether to get the predicted fine raw target of each image in grid form (as an
        `xr.DataArray`) or in flattened form (as a `pd.Series`).

    dims : tuple or None, default=None
        If `gridded` is `True`: labels for the dimensions of the predicted gridded
        target. If not issued, it is set to `("lat", "lon")` by default.

    attrs : dict or None, default=None
        If `gridded` is `True`: attributes to set in the predicted gridded target. If
        not issued, it is set as in accordance with the CF conventions
        (https://cf-convention.github.io/Data/cf-conventions/cf-conventions-1.13/cf-conventions.pdf#temperature-units):
            ```
            {
                "standard_name": "land_surface_temperature",
                "long_name": "Land surface temperature",
                "units": "K",
            }
            ```

    path_out : Path or None, default=None
        The directory path to save the downscaled LST data, obtained scores, downscaler
        and data wrangler. If not issued, the results are instead returned.

    file_ext_grid : str, default=".nc"
        If `gridded` is `True` and `path_out` is issued: The file extension to use when
        writing the gridded predicted fine target to file (e.g. ".tif" for GeoTIFF and
        ".nc" for NetCDF). If `path_out` is issued and `gridded` is `False`, the
        predicted fine target is written in flattened form with the ".csv" extension.

    out_data_wrangler : bool, default=True
        Whether to return or write (if `path_out` is issued) the data wrangler.

    out_downscaler : bool, default=True
        Whether to return or write (if `path_out` is issued) the downscaler.

    log_mode : {None, "console", "file", "both"}, default="both"
        The logging mode for wrangling, training, inferring and scoring:
            - `None`: No logging is done;
            - `"console"`: Logging is done to console only;
            - `"file"`: Logging is done to a log file only;
            - `"both"`: Logging is done to both console and a log file.
        Note the log file would be defined as `downscale.log` at `out_dir`.
    """

    data_wrangler: DataWrangler | Path | None = None
    data_wrangler_path_sentinel3: Path | None = None
    data_wrangler_path_spatial_pred: Path | None = None
    data_wrangler_aoi: str | Path | None = None
    data_wrangler_path_landsat: Path | None = None
    data_wrangler_vars: list[str] | None = None
    data_wrangler_max_workers: int = 1
    downscaler: PiecewiseDownscaler | Downscaler | Path | None = None
    downscaler_architecture: Literal["single", "multi"] = "single"
    downscaler_base_model: Regressor = field(default_factory=lambda: LinearRegression())
    downscaler_X: list[str] = field(default_factory=lambda: ["FVC", "NDWI"])
    downscaler_masks: list[str] | None = None
    downscaler_scale: Literal["standardize", "min_max_normalize"] | None = "standardize"
    downscaler_encode: Literal["one_hot", "dummy"] | None = "dummy"
    downscaler_transform: Literal["center", "standardize"] | None = None
    downscaler_lasso_sel: bool = False
    downscaler_lasso_alpha: float = 1.0
    downscaler_max_workers: int = 1
    timestamps_infer: list[pd.Timestamp] | list[str] | None = None
    timestamps_fit: list[pd.Timestamp] | list[str] | None = None
    sample_weight_fit: str | None = None
    sample_weight_score: str | None = None
    score: bool = True
    scorers: list[str] = field(default_factory=lambda: ["r2", "rmse", "mae", "mbe"])
    retrain: bool = True
    correct: bool = True
    gridded: bool = True
    dims: tuple | None = None
    attrs: dict | None = None
    path_out: Path | None = None
    file_ext_grid: str = ".nc"
    out_data_wrangler: bool = True
    out_downscaler: bool = True
    log_mode: Literal["console", "file", "both"] | None = "both"

attrs class-attribute instance-attribute

attrs: dict | None = None

correct class-attribute instance-attribute

correct: bool = True

data_wrangler class-attribute instance-attribute

data_wrangler: DataWrangler | Path | None = None

data_wrangler_aoi class-attribute instance-attribute

data_wrangler_aoi: str | Path | None = None

data_wrangler_max_workers class-attribute instance-attribute

data_wrangler_max_workers: int = 1

data_wrangler_path_landsat class-attribute instance-attribute

data_wrangler_path_landsat: Path | None = None

data_wrangler_path_sentinel3 class-attribute instance-attribute

data_wrangler_path_sentinel3: Path | None = None

data_wrangler_path_spatial_pred class-attribute instance-attribute

data_wrangler_path_spatial_pred: Path | None = None

data_wrangler_vars class-attribute instance-attribute

data_wrangler_vars: list[str] | None = None

dims class-attribute instance-attribute

dims: tuple | None = None

downscaler class-attribute instance-attribute

downscaler: PiecewiseDownscaler | Downscaler | Path | None = None

downscaler_X class-attribute instance-attribute

downscaler_X: list[str] = field(default_factory=lambda: ['FVC', 'NDWI'])

downscaler_architecture class-attribute instance-attribute

downscaler_architecture: Literal['single', 'multi'] = 'single'

downscaler_base_model class-attribute instance-attribute

downscaler_base_model: Regressor = field(default_factory=lambda: LinearRegression())

downscaler_encode class-attribute instance-attribute

downscaler_encode: Literal['one_hot', 'dummy'] | None = 'dummy'

downscaler_lasso_alpha class-attribute instance-attribute

downscaler_lasso_alpha: float = 1.0

downscaler_lasso_sel class-attribute instance-attribute

downscaler_lasso_sel: bool = False

downscaler_masks class-attribute instance-attribute

downscaler_masks: list[str] | None = None

downscaler_max_workers class-attribute instance-attribute

downscaler_max_workers: int = 1

downscaler_scale class-attribute instance-attribute

downscaler_scale: Literal['standardize', 'min_max_normalize'] | None = 'standardize'

downscaler_transform class-attribute instance-attribute

downscaler_transform: Literal['center', 'standardize'] | None = None

file_ext_grid class-attribute instance-attribute

file_ext_grid: str = '.nc'

gridded class-attribute instance-attribute

gridded: bool = True

log_mode class-attribute instance-attribute

log_mode: Literal['console', 'file', 'both'] | None = 'both'

out_data_wrangler class-attribute instance-attribute

out_data_wrangler: bool = True

out_downscaler class-attribute instance-attribute

out_downscaler: bool = True

path_out class-attribute instance-attribute

path_out: Path | None = None

retrain class-attribute instance-attribute

retrain: bool = True

sample_weight_fit class-attribute instance-attribute

sample_weight_fit: str | None = None

sample_weight_score class-attribute instance-attribute

sample_weight_score: str | None = None

score class-attribute instance-attribute

score: bool = True

scorers class-attribute instance-attribute

scorers: list[str] = field(default_factory=lambda: ['r2', 'rmse', 'mae', 'mbe'])

timestamps_fit class-attribute instance-attribute

timestamps_fit: list[Timestamp] | list[str] | None = None

timestamps_infer class-attribute instance-attribute

timestamps_infer: list[Timestamp] | list[str] | None = None

__init__

__init__(
    data_wrangler: DataWrangler | Path | None = None,
    data_wrangler_path_sentinel3: Path | None = None,
    data_wrangler_path_spatial_pred: Path | None = None,
    data_wrangler_aoi: str | Path | None = None,
    data_wrangler_path_landsat: Path | None = None,
    data_wrangler_vars: list[str] | None = None,
    data_wrangler_max_workers: int = 1,
    downscaler: PiecewiseDownscaler | Downscaler | Path | None = None,
    downscaler_architecture: Literal["single", "multi"] = "single",
    downscaler_base_model: Regressor = (lambda: LinearRegression())(),
    downscaler_X: list[str] = (lambda: ["FVC", "NDWI"])(),
    downscaler_masks: list[str] | None = None,
    downscaler_scale: Literal["standardize", "min_max_normalize"]
    | None = "standardize",
    downscaler_encode: Literal["one_hot", "dummy"] | None = "dummy",
    downscaler_transform: Literal["center", "standardize"] | None = None,
    downscaler_lasso_sel: bool = False,
    downscaler_lasso_alpha: float = 1.0,
    downscaler_max_workers: int = 1,
    timestamps_infer: list[Timestamp] | list[str] | None = None,
    timestamps_fit: list[Timestamp] | list[str] | None = None,
    sample_weight_fit: str | None = None,
    sample_weight_score: str | None = None,
    score: bool = True,
    scorers: list[str] = (lambda: ["r2", "rmse", "mae", "mbe"])(),
    retrain: bool = True,
    correct: bool = True,
    gridded: bool = True,
    dims: tuple | None = None,
    attrs: dict | None = None,
    path_out: Path | None = None,
    file_ext_grid: str = ".nc",
    out_data_wrangler: bool = True,
    out_downscaler: bool = True,
    log_mode: Literal["console", "file", "both"] | None = "both",
) -> None

Caller

s3lst_ds.downscaling.downscale.downscale.downscale

downscale(config: DownscaleConfig) -> DownscaleOut

Wrangle the data of timestamps of interest, train a downscaler using the coarse data of the training timestamps, downscale the coarse data of the inference timestamps, score the downscaler, and return or write the results to files.

Parameters:

Name Type Description Default
config DownscaleConfig

Configurations for wrangling, training, downscaling, scoring and writing.

required

Returns:

Type Description
dict

Dictionary containing: - y_fine_pred: dict[pd.Timestamp, np.ndarray or xr.DataArray] or dict[pd.Timestamp, Path] The downscaled LST data for each inference timestamp, either as a dictionary of NumPy arrays (if parameter config.path_out is not issued and config.gridded is False) or Xarray DataArrays (if config.path_out is not issued and parameter config.gridded is True) or as a dictionary of paths to the respective files (if config.path_out is issued). - score: dict[str, dict[str, dict[str, dict[str, float]]]] or Path Prediction scores either as a dictionary (if config.path_out is not issued) or as a path to the respective JSON file. This only takes effect if config.score is True. The scores are keyed by batch ("train" or "infer"), grid ("coarse" or "fine"), ground truth dataset ("sentinel" or "landsat") and metric ("r2", "rmse", etc.). Scores using Landsat ground truth data are solely computed if Landsat data exists (included in the issued config.data_wrangler or in config.data_wrangler_path_landsat if no data wrangler is issued) and if such data is available for the considered training or inference timestamps. Training scores are solely computed if the downscaler is a multi-timestamp one and training is performed. In the case of the single-timestamp architecture, training scores do not need to be computed since such model is always trained with the coarse data from the same timestamp whose target it infers (and, thus, the training scores would coincide with the inference ones). - downscaler: Downscaler or PiecewiseDownscaler or Path The downscaler (if config.path_out is not issued) or a path to the respective Joblib file. This only takes effect if parameter config.out_downscaler is True. - data_wrangler: DataWrangler or Path The data wrangler (if config.path_out is not issued) or a path to the respective Joblib file. The data wrangler also contains the wrangled data and may be useful for debugging or for reusing it without need for reprocessing the original one. This only takes effect if parameter config.out_data_wrangler is True.

Source code in src/s3lst_ds/downscaling/downscale/downscale.py
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
def downscale(
    config: DownscaleConfig,
) -> DownscaleOut:
    """
    Wrangle the data of timestamps of interest, train a downscaler using the coarse data
    of the training timestamps, downscale the coarse data of the inference timestamps,
    score the downscaler, and return or write the results to files.

    Parameters
    ----------

    config: DownscaleConfig
        Configurations for wrangling, training, downscaling, scoring and writing.

    Returns
    -------
    dict
        Dictionary containing:
        - y_fine_pred: dict[pd.Timestamp, np.ndarray or xr.DataArray] or
        dict[pd.Timestamp, Path]
            The downscaled LST data for each inference timestamp, either as a dictionary
            of NumPy arrays (if parameter `config.path_out` is not issued and
            `config.gridded` is `False`) or Xarray DataArrays (if `config.path_out` is
            not issued and parameter `config.gridded` is `True`) or as a dictionary of
            paths to the respective files (if `config.path_out` is issued).
        - score: dict[str, dict[str, dict[str, dict[str, float]]]] or Path
            Prediction scores either as a dictionary (if `config.path_out` is not
            issued) or as a path to the respective JSON file. This only takes effect if
            `config.score` is `True`. The scores are keyed by batch ("train" or
            "infer"), grid ("coarse" or "fine"), ground truth dataset ("sentinel" or
            "landsat") and metric ("r2", "rmse", etc.). Scores using Landsat ground
            truth data are solely computed if Landsat data exists (included in the
            issued `config.data_wrangler` or in `config.data_wrangler_path_landsat` if
            no data wrangler is issued) and if such data is available for the considered
            training or inference timestamps. Training scores are solely computed if the
            downscaler is a multi-timestamp one and training is performed. In the case
            of the single-timestamp architecture, training scores do not need to be
            computed since such model is always trained with the coarse data from the
            same timestamp whose target it infers (and, thus, the training scores would
            coincide with the inference ones).
        - downscaler: Downscaler or PiecewiseDownscaler or Path
            The downscaler (if `config.path_out` is not issued) or a path to the
            respective Joblib file. This only takes effect if parameter
            `config.out_downscaler` is `True`.
        - data_wrangler: DataWrangler or Path
            The data wrangler (if `config.path_out` is not issued) or a path to the
            respective Joblib file. The data wrangler also contains the wrangled data
            and may be useful for debugging or for reusing it without need for
            reprocessing the original one. This only takes effect if parameter
            `config.out_data_wrangler` is `True`.
    """

    # ---> Handle logging

    # Create logger
    logger = RichLogger(
        name="downscale",
        level=logging.INFO,
        file_path=(
            Path(config.path_out) / "downscale.log"
            if config.path_out is not None
            else None
        ),
        file_mode="w",
        log_mode=config.log_mode,
    )

    logger.console.print()
    logger.info("[bold]Downscaling Sentinel-3 LST products[/bold]")

    # ---> Create output directory
    if config.path_out is not None:
        try:
            Path(config.path_out).mkdir(parents=True, exist_ok=True)

        except Exception as e:  # noqa: BLE001
            logger.error(
                "[bold red]Error creating the output directory."
                + f"\nError message: {e}"
                + "\nRun will stop.[/bold red]",
            )
            raise WritingError(
                "Error creating the output directory." + f"\nError message: {e}"
            )

    # ---> Get downscaler if it is issued

    # Load downscaler from file if a path is issued
    if isinstance(config.downscaler, Path):
        logger.console.print()
        logger.info("The downscaler will now be loaded from file.")
        try:
            with logger.console.status(
                f"{'':7}Loading downscaler from file[yellow]...[/yellow]",
                spinner="dots",
                spinner_style="bold blue",
            ):
                downscaler = joblib.load(config.downscaler)

        except Exception as e:  # noqa: BLE001
            logger.error(
                "[bold red]Error loading the downscaler from file."
                + f"\nError message: {e}"
                + "\nRun will stop.[/bold red]",
            )
            raise ReadingError(
                "Error loading the downscaler from file." + f"\nError message: {e}"
            )

        if not isinstance(downscaler, (Downscaler, PiecewiseDownscaler)):
            logger.error(
                "[bold red]The loaded downscaler is neither (multi-timestamp)"
                " Downscaler nor a (single-timestamp) PiecewiseDownscaler object."
                + "\nRun will stop.[/bold red]",
            )
            raise TypeError(
                "The loaded downscaler is neither a (multi-timestamp) Downscaler nor a"
                " (single-timestamp) PiecewiseDownscaler object."
            )

        logger.info("[bold green]Downscaler loaded from file.[/bold green]")

    # Set downscaler if provided directly
    elif isinstance(config.downscaler, (Downscaler, PiecewiseDownscaler)):
        downscaler = config.downscaler

    # ---> Get data wrangler if it is issued
    # Load data wrangler from file if a path is issued
    if isinstance(config.data_wrangler, Path):
        logger.console.print()
        logger.info("The data wrangler will now be loaded from file.")
        try:
            with logger.console.status(
                f"{'':7}Loading data wrangler from file[yellow]...[/yellow]",
                spinner="dots",
                spinner_style="bold blue",
            ):
                data_wrangler = joblib.load(config.data_wrangler)

        except Exception as e:  # noqa: BLE001
            logger.error(
                "[bold red]Error loading the data wrangler from file."
                + f"\nError message: {e}"
                + "\nRun will stop.[/bold red]",
            )
            raise ReadingError(
                "Error loading the data wrangler from file." + f"\nError message: {e}"
            )

        logger.info("[bold green]Data wrangler loaded from file.[/bold green]")

    # Set data wrangler if provided directly
    elif isinstance(config.data_wrangler, DataWrangler):
        data_wrangler = config.data_wrangler

    # ---> Parse parameters

    # Parse downscaler architecture
    downscaler_architecture = (
        type(downscaler)
        if config.downscaler is not None
        else (
            Downscaler
            if config.downscaler_architecture == "multi"
            else PiecewiseDownscaler
        )
    )

    # Parse downscaler predictors
    downscaler_X = (
        downscaler.cols_X if config.downscaler is not None else config.downscaler_X
    )

    # Parse downscaler transform
    # NOTE: in the case of the single-timestamp architecture, a timestamp-specific
    # transform of the spatio-temporal predictors is redundant and may be set to `None`.
    downscaler_transform = (
        downscaler.transform  # type: ignore
        if config.downscaler is not None and downscaler_architecture == Downscaler
        else (
            config.downscaler_transform
            if downscaler_architecture == Downscaler
            else None
        )
    )

    # Parse path to Landsat data
    # NOTE: Landsat data will be solely wrangled if it was issued or already contained
    # in an issued data wrangler.
    data_wrangler_path_landsat = (
        data_wrangler.path_landsat
        if config.data_wrangler is not None
        else config.data_wrangler_path_landsat
    )

    # Parse data wrangling variables
    if config.data_wrangler is not None:
        data_wrangler_data_vars = data_wrangler.data_vars
    else:
        data_wrangler_vars = (
            config.data_wrangler_vars
            if config.data_wrangler_vars is not None
            else (
                downscaler.cols_X
                if config.downscaler is not None
                else config.downscaler_X
                if config.downscaler_X is not None
                else None
            )
        )
        data_wrangler_data_vars = DataVars().subset_X(data_wrangler_vars)  # type: ignore

    # Parse training indicator
    train = bool(
        config.downscaler is not None
        and (
            config.retrain is True
            or downscaler_architecture == PiecewiseDownscaler
            or downscaler.is_fitted_ is False
        )
        or config.downscaler is None
    )

    # Parse timestamps
    timestamps = {"sentinel": {}}
    timestamps["sentinel"]["infer"] = (
        [
            pd.Timestamp(timestamp)
            if not isinstance(timestamp, pd.Timestamp)
            else timestamp
            for timestamp in config.timestamps_infer
        ]
        if config.timestamps_infer is not None
        else (
            data_wrangler.timestamps
            if config.data_wrangler is not None
            else [
                pd.Timestamp(data_wrangler_path_sentinel3_folder.name)
                for data_wrangler_path_sentinel3_folder in config.data_wrangler_path_sentinel3.iterdir()  # type: ignore
                if data_wrangler_path_sentinel3_folder.is_dir()
            ]
        )
    )
    timestamps["sentinel"]["train"] = (
        [
            pd.Timestamp(timestamp)
            if not isinstance(timestamp, pd.Timestamp)
            else timestamp
            for timestamp in config.timestamps_fit
        ]
        if config.timestamps_fit is not None and downscaler_architecture == Downscaler
        else (
            timestamps["sentinel"]["infer"]
            if downscaler_architecture == PiecewiseDownscaler
            else (
                data_wrangler.timestamps
                if config.data_wrangler is not None
                else [
                    pd.Timestamp(data_wrangler_path_sentinel3_folder.name)
                    for data_wrangler_path_sentinel3_folder in config.data_wrangler_path_sentinel3.iterdir()  # type: ignore
                    if data_wrangler_path_sentinel3_folder.is_dir()
                ]
            )
        )
    )

    # Parse indicators for outputting variables
    out = {
        "y_fine_pred": True,
        "score": config.score,
        "data_wrangler": config.out_data_wrangler,
        "downscaler": config.out_downscaler,
    }

    # Parse output paths
    path_out = {
        object_alias: (
            (
                # For the case of the fine target predictions
                {
                    timestamp: config.path_out
                    / (
                        "lst_downscaled_"
                        + timestamp.strftime("%Y%m%dT%H%M%S")
                        + (config.file_ext_grid if config.gridded is True else ".csv")
                    )
                    for timestamp in timestamps["sentinel"]["infer"]
                }
                if object_alias == "y_fine_pred"
                # For the case of scores, data wrangler and downscaler
                else config.path_out
                / (object_alias + (".joblib" if object_alias != "score" else ".json"))
            )
            if config.path_out is not None and out[object_alias] is True
            else None
        )
        for object_alias in out
    }

    # ---> Update parameters of the downscaler and data wrangler if they had been issued

    # Set logger
    if config.data_wrangler is not None:
        data_wrangler.logger = logger
    if config.downscaler is not None:
        downscaler.logger = logger

    # Update masking variables and maximum number of workers of the downscaler if it had
    # been issued
    if config.downscaler is not None:
        downscaler.cols_mask = config.downscaler_masks
        downscaler.max_workers = config.downscaler_max_workers

    # Update transform (with the one of the downscaler, if it is different from the one
    # of the data wrangler) and maximum number of workers of the data wrangler if it had
    # been issued
    if config.data_wrangler is not None:
        if data_wrangler.transform != downscaler_transform:  # type: ignore
            data_wrangler.transform = downscaler_transform  # type: ignore
        data_wrangler.max_workers = config.data_wrangler_max_workers

    # ---> Wrangle the data if a data wrangler was not issued
    if config.data_wrangler is None:
        logger.console.print()
        logger.info("The data will now be wrangled.")
        try:
            # Define a data wrangler
            data_wrangler = DataWrangler(
                data_vars=data_wrangler_data_vars,
                path_sentinel3=config.data_wrangler_path_sentinel3,  # type: ignore
                path_spatial_pred=config.data_wrangler_path_spatial_pred,  # type: ignore
                aoi=config.data_wrangler_aoi,
                path_landsat=data_wrangler_path_landsat,
                timestamps=list(
                    set(timestamps["sentinel"]["train"])
                    | set(timestamps["sentinel"]["infer"])
                ),
                transform=downscaler_transform,  # type: ignore
                max_workers=config.data_wrangler_max_workers,
                logger=logger,
            )

        except Exception as e:  # noqa: BLE001
            logger.error(
                "[bold red]Error wrangling the data."
                + f"\nError message: {e}"
                + "\nRun will stop.[/bold red]",
            )
            raise DataWranglingError(
                "Error wrangling the data." + f"\nError message: {e}"
            )

        logger.info("[bold green]Data wrangled.[/bold green]")

    # ---> Train the downscaler if it was issued and retraining is wanted or it was
    # issued and the architecture is single-timestamp one or the downscaler is to be
    # built from scratch
    if train is True:
        logger.console.print()
        logger.info("The downscaler will now be trained.")
        try:
            # Define a downscaler if it had not been issued
            if config.downscaler is None:
                downscaler = downscaler_architecture(
                    base_model=config.downscaler_base_model,
                    cols_X=downscaler_X,
                    cols_mask=config.downscaler_masks,
                    scale=config.downscaler_scale,
                    encode=config.downscaler_encode,
                    lasso_sel=config.downscaler_lasso_sel,
                    lasso_alpha=config.downscaler_lasso_alpha,
                    max_workers=config.downscaler_max_workers,
                    **(
                        {"transform": downscaler_transform}
                        if downscaler_transform is not None
                        else {}
                    ),  # type: ignore
                    logger=logger,
                )

            # Train the downscaler with the training coarse data
            if downscaler_architecture == Downscaler:
                with logger.console.status(
                    f"{'':7}Training downscaler with the coarse data"
                    "[yellow]...[/yellow]",
                    spinner="dots",
                    spinner_style="bold blue",
                ):
                    downscaler.fit(
                        X_and_mask_coarse=data_wrangler.get_data_X_and_mask(
                            timestamps=timestamps["sentinel"]["train"],
                            grid="coarse",
                            trans=True,
                            aggregate=True,
                        ),  # type: ignore
                        y_coarse=data_wrangler.get_data_y(
                            timestamps=timestamps["sentinel"]["train"],
                            grid="coarse",
                            trans=True,
                            aggregate=True,
                        ),  # type: ignore
                        sample_weight=(
                            data_wrangler.get_data(
                                timestamps=timestamps["sentinel"]["train"],
                                grid="coarse",
                                vars=config.sample_weight_fit,
                                trans=False,
                                aggregate=True,
                            )
                            if config.sample_weight_fit is not None
                            else None
                        ),  # type: ignore
                    )
            else:
                downscaler.fit(
                    X_and_mask_coarse=data_wrangler.get_data_X_and_mask(
                        timestamps=timestamps["sentinel"]["train"],
                        grid="coarse",
                        trans=False,
                        aggregate=False,
                    ),  # type: ignore
                    y_coarse=data_wrangler.get_data_y(
                        timestamps=timestamps["sentinel"]["train"],
                        grid="coarse",
                        trans=False,
                        aggregate=False,
                    ),  # type: ignore
                    sample_weight=(
                        data_wrangler.get_data(
                            timestamps=timestamps["sentinel"]["train"],
                            grid="coarse",
                            vars=config.sample_weight_fit,
                            trans=False,
                            aggregate=False,
                        )
                        if config.sample_weight_fit is not None
                        else None
                    ),  # type: ignore
                )

        except Exception as e:  # noqa: BLE001
            logger.error(
                "[bold red]Error training the downscaler."
                + f"\nError message: {e}"
                + "\nRun will stop.[/bold red]",
            )
            raise TrainingError(
                "Error training the downscaler." + f"\nError message: {e}"
            )

        logger.info("[bold green]Downscaler trained.[/bold green]")

    # ---> Downscale
    logger.console.print()
    logger.info("LST will now be downscaled.")
    try:
        # Downscale for each inference timestamp
        y_fine_pred = downscaler.predict(
            X_and_mask_fine=data_wrangler.get_data_X_and_mask(
                timestamps=timestamps["sentinel"]["infer"],
                grid="fine",
                trans=True,
                aggregate=False,
            ),  # type: ignore
            correct=config.correct,
            X_and_mask_coarse=data_wrangler.get_data_X_and_mask(
                timestamps=timestamps["sentinel"]["infer"],
                grid="coarse",
                trans=True,
                aggregate=False,
            ),  # type: ignore
            y_coarse=data_wrangler.get_data_y(
                timestamps=timestamps["sentinel"]["infer"],
                grid="coarse",
                trans=False,
                aggregate=False,
            ),  # type: ignore
            coords_coarse=data_wrangler.get_coords(
                timestamps=timestamps["sentinel"]["infer"], grid="coarse"
            ),  # type: ignore
            coords_fine=data_wrangler.get_coords(
                timestamps=timestamps["sentinel"]["infer"], grid="fine"
            ),  # type: ignore
            gridded=config.gridded,
            dims=config.dims,
            attrs=config.attrs,
            path_out=path_out["y_fine_pred"],  # type: ignore
        )

    except Exception as e:  # noqa: BLE001
        logger.error(
            "[bold red]Error downscaling."
            + f"\nError message: {e}"
            + "\nRun will stop.[/bold red]",
        )
        raise DownscalingError("Error downscaling." + f"\nError message: {e}")

    logger.info("[bold green]LST downscaled.[/bold green]")

    # ---> Score if wanted
    if config.score is True:
        # Initialize score dictionary
        score = {}

        # Get timestamps for which there is Landsat data available
        timestamps["landsat"] = {
            batch: (
                list(
                    set(timestamps["sentinel"][batch])
                    & set(data_wrangler.timestamps_landsat)
                )
                if data_wrangler_path_landsat is not None
                else None
            )
            for batch in ["train", "infer"]
        }
        for batch in ["train", "infer"]:
            for grid in ["coarse", "fine"]:
                for ground_truth in ["sentinel", "landsat"]:
                    try:
                        if (
                            # Score downscaler on the training data if it is a
                            # multi-timestamp one and was trained/retrained
                            (
                                batch == "train"
                                and train is True
                                and downscaler_architecture == Downscaler
                            )
                            # Score downscaler on the inference data
                            or batch == "infer"
                        ) and (
                            # Score donwscaler using Sentinel-3 data as ground truth
                            # if the grid is coarse
                            (grid == "coarse" and ground_truth == "sentinel")
                            # Score downscaler using Landsat data for both grids as
                            # ground truth only if such data is available
                            or (
                                ground_truth == "landsat"
                                and timestamps["landsat"][batch] is not None
                            )
                        ):
                            logger.console.print()
                            logger.info(
                                "The downscaler will now be scored with respect to the"
                                f" {grid}"
                                f" {'training' if batch == 'train' else 'inference'}"
                                f" data using"
                                f" {'Sentinel-3' if ground_truth == 'sentinel' else 'Landsat 8/9'}"
                                " as ground truth."
                            )

                            if batch not in score:
                                score[batch] = {}
                            if grid not in score[batch]:
                                score[batch][grid] = {}

                            score[batch][grid][ground_truth] = (
                                # Coarse-score if grid is coarse
                                downscaler.score_coarse(
                                    X_and_mask_coarse=data_wrangler.get_data_X_and_mask(
                                        timestamps=timestamps[ground_truth][batch],
                                        grid="coarse",
                                        trans=True,
                                        aggregate=False,
                                    ),  # type: ignore
                                    y_coarse=(
                                        # Use Sentinel-3 data as ground truth if
                                        # that is the case
                                        data_wrangler.get_data_y(
                                            timestamps=timestamps[ground_truth][batch],
                                            grid="coarse",
                                            trans=False,
                                            aggregate=False,
                                        )
                                        if ground_truth == "sentinel"
                                        # Use Landsat data as ground truth if that
                                        # is the case
                                        else data_wrangler.get_data(
                                            timestamps=timestamps[ground_truth][batch],
                                            grid="coarse",
                                            vars=data_wrangler_data_vars.y_val,
                                            trans=False,
                                            aggregate=False,
                                        )
                                    ),  # type: ignore
                                    aggregate=True,
                                    scorers=config.scorers,
                                    sample_weight=(
                                        data_wrangler.get_data(
                                            timestamps=timestamps[ground_truth][batch],
                                            grid="coarse",
                                            vars=config.sample_weight_score,
                                            trans=False,
                                            aggregate=False,
                                        )
                                        if config.sample_weight_score is not None
                                        else None
                                    ),  # type: ignore
                                )
                                if grid == "coarse"
                                # Fine-score if grid is fine
                                else downscaler.score(
                                    X_and_mask_fine=data_wrangler.get_data_X_and_mask(
                                        timestamps=timestamps["landsat"][batch],
                                        grid="fine",
                                        trans=True,
                                        aggregate=False,
                                    ),  # type: ignore
                                    y_fine=data_wrangler.get_data(
                                        timestamps=timestamps["landsat"][batch],
                                        grid="fine",
                                        vars=data_wrangler_data_vars.y_val,
                                        trans=False,
                                        aggregate=False,
                                    ),  # type: ignore
                                    correct=config.correct,
                                    X_and_mask_coarse=data_wrangler.get_data_X_and_mask(
                                        timestamps=timestamps["landsat"][batch],
                                        grid="coarse",
                                        trans=True,
                                        aggregate=False,
                                    ),  # type: ignore
                                    y_coarse=data_wrangler.get_data_y(
                                        timestamps=timestamps["landsat"][batch],
                                        grid="coarse",
                                        trans=False,
                                        aggregate=False,
                                    ),  # type: ignore
                                    coords_coarse=data_wrangler.get_coords(
                                        timestamps=timestamps["landsat"][batch],
                                        grid="coarse",
                                    ),  # type: ignore
                                    coords_fine=data_wrangler.get_coords(
                                        timestamps=timestamps["landsat"][batch],
                                        grid="fine",
                                    ),  # type: ignore
                                    aggregate=True,
                                    scorers=config.scorers,
                                    sample_weight=(
                                        data_wrangler.get_data(
                                            timestamps=timestamps["landsat"][batch],
                                            grid="fine",
                                            vars=config.sample_weight_score,
                                            trans=False,
                                            aggregate=False,
                                        )
                                        if config.sample_weight_score is not None
                                        else None
                                    ),  # type: ignore
                                )
                            )

                            logger.info(
                                f"[bold green]Downscaler scored with respect to {grid}"
                                f" {'training' if batch == 'train' else 'inference'}"
                                " data using"
                                f" {'Sentinel-3' if ground_truth == 'sentinel' else 'Landsat 8/9'}"
                                " as ground truth.[/bold green]"
                            )

                    except Exception as e:  # noqa: BLE001
                        logger.error(
                            "[bold red]Error scoring the downscaler with respect to"
                            f" {grid}"
                            f" {'training' if batch == 'train' else 'inference'} data"
                            " using"
                            f" {'Sentinel-3' if ground_truth == 'sentinel' else 'Landsat 8/9'}"
                            " as ground truth."
                            f"\nError message: {e}"
                            "\nRun will stop.[/bold red]",
                        )
                        raise ScoringError(
                            f"Error scoring the downscaler with respect to {grid}"
                            f" {'training' if batch == 'train' else 'inference'} data"
                            " using"
                            f" {'Sentinel-3' if ground_truth == 'sentinel' else 'Landsat 8/9'}"
                            " as ground truth."
                            f"\nError message: {e}"
                        )

    else:
        score = None

    # ---> Combine all the results in a dictionary
    object = {
        "y_fine_pred": y_fine_pred,
        "score": score,
        "downscaler": downscaler,
        "data_wrangler": data_wrangler,
    }

    # ---> Write scores, downscaler and data wrangler to file if wanted
    for object_alias in object:  # noqa: PLC0206
        if object_alias != "y_fine_pred" and path_out[object_alias] is not None:
            logger.console.print()
            logger.info(
                f"The {object_alias.replace('_', ' ')} will now be written to file."
            )
            try:
                with logger.console.status(
                    f"{'':7}Writing {object_alias.replace('_', ' ')} to file"
                    "[yellow]...[/yellow]",
                    spinner="dots",
                    spinner_style="bold blue",
                ):
                    if object_alias == "score":
                        pd.Series(object[object_alias]).to_json(
                            path_out[object_alias]  # type: ignore
                        )
                    else:
                        object[object_alias].save(path_out[object_alias])
            except Exception as e:  # noqa: BLE001
                logger.error(
                    f"[bold red]Error writing the {object_alias.replace('_', ' ')}"
                    " to file."
                    f"\nError message: {e}"
                    "\nRun will stop.[/bold red]",
                )
                raise WritingError(
                    f"Error writing the {object_alias.replace('_', ' ')} to file."
                    + f"\nError message: {e}"
                )

            logger.info(
                f"[bold green]{object_alias.replace('_', ' ').capitalize()} written"
                " to file.[/bold green]"
            )

    # ---> Show table with scores if they were computed
    if score is not None:
        table_score = Table(title="Metrics")
        table_score.add_column("Batch", justify="left")
        table_score.add_column("Grid", justify="left")
        table_score.add_column("Ground truth", justify="left")
        table_score.add_column("Metric", justify="left")
        table_score.add_column("Value", justify="right")
        for batch in score:
            for i, grid in enumerate(score[batch].keys()):
                for j, ground_truth in enumerate(score[batch][grid].keys()):
                    for k, scorer in enumerate(score[batch][grid][ground_truth].keys()):
                        table_score.add_row(
                            batch if i == 0 and j == 0 and k == 0 else None,
                            grid if j == 0 and k == 0 else None,
                            ground_truth if k == 0 else None,
                            scorer,
                            f"{score[batch][grid][ground_truth][scorer]:.5g}",
                        )

        logger.console.print()
        logger.info(
            "The downscaler attained the following metrics:"
            "\n\n"
            + f"{
                get_rich_text_from_renderable(
                    console=logger.console,
                    renderable=table_score,
                )
            }"
        )

    # ---> Return the results
    logger.console.print()
    return DownscaleOut(
        **{
            object_alias: (
                object if path_out[object_alias] is None else path_out[object_alias]
            )
            for object_alias, object in object.items()
            if out[object_alias] is True
        }  # type: ignore
    )