Repository: pySTEPS/pysteps (language: python)

Issue:
Implement importer for NWP (netcdf) data
Implement function to read in CF-compliant NetCDF data, not (yet) including interpolation to the radar / nowcasting grid. This might be simplified by moving to xarray first. Required for #212 .
Use specific exception types instead of generic Exception
Generic Exception classes are used in many places where a more specific exception type would be appropriate. Find ones from the builtin Python exception classes or implement own.


Discussion / hints:

Some of the generic exception were replaced by two custom exception: MissingOptionalDependency and DataModelError.

The rest of the generic exception were replaced by build-in Exceptions.

Commits 28d7d7c25d17843df2c18b4213be51e476ee0381 5100b85fd59b89846b7c150964e8d3786c6f080d 5100b85fd59b89846b7c150964e8d3786c6f080d close this issue.


Fix patch:
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index c80571951..f35ad636b 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -1,6 +1,6 @@
 repos:
 -   repo: https://github.com/psf/black
-    rev: 21.6b0
+    rev: 21.7b0
     hooks:
     - id: black
       language_version: python3
\ No newline at end of file
diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst
index adb6cc88d..c48eb3533 100644
--- a/CONTRIBUTING.rst
+++ b/CONTRIBUTING.rst
@@ -2,41 +2,34 @@ Contributing to Pysteps
 =======================
 
 Welcome! pySTEPS is a community-driven initiative for developing and
-maintaining an easy to use, modular, free and open source Python
+maintaining an easy to use, modular, free and open-source Python
 framework for short-term ensemble prediction systems.
 
-
-If you haven't already, take a look at the project's
-`README.rst file <README.rst>`_ and the
-`pysteps documentation <https://pysteps.github.io/>`_.
-
 There are many ways to contribute to pysteps:
 
-- contributing bug reports and feature requests
-- contributing documentation
-- code contributions new features or bug fixes
-- contribute with usage examples
-
-Our main forum for discussion is the project's
-`GitHub issue tracker <https://github.com/python/mypy/issues>`_.
-This is the right place to start a discussion, report a bug, or request a new
-feature.
-
+* contributing bug reports and feature requests
+* contributing documentation
+* code contributions, new features, or bug fixes
+* contribute with usage examples
 
 Workflow for code contributions
 -------------------------------
 
-We welcome all kind of contributions, from documentation updates, a bug fix,
-or a new feature.
-If your new feature will take a lot of work,
-we recommend creating an issue with the **enhancement** tag to encourage
-discussions.
+We welcome all kinds of contributions, like documentation updates, bug fixes, or new features.
+The workflow for the contibutions uses the usual
+`GitHub pull-request flow <https://help.github.com/en/articles/github-flow>`_.
+
+If you have ideas for new contributions to the project, feel free to get in touch with the pysteps community on our
+`pysteps slack <https://pysteps.slack.com/>`__.
+To get access to it, you need to ask for an invitation or you can use the automatic invitation page
+`here <https://pysteps-slackin.herokuapp.com/>`__.
+Our slack channel is a great place for preliminary discussions about new features or functionalities.
+Another place where you can report bugs and suggest new enhancements is the
+`project's issue tracker <https://github.com/pySTEPS/pysteps/issues>`_.
 
-We use the usual
-`GitHub pull-request flow <https://help.github.com/en/articles/github-flow>`_,
-which may be familiar to you if you've contributed to other projects on GitHub.
 
-**First Time Contributors**
+First Time Contributors
+-----------------------
 
 If you are interested in helping to improve pysteps,
 the best way to get started is by looking for "Good First Issue" in the
@@ -44,27 +37,26 @@ the best way to get started is by looking for "Good First Issue" in the
 
 In a nutshell, the main steps to follow for contributing to pysteps are:
 
-- Setting up the development environment
-- Fork the repository
-- Install pre-commit hooks
-- Create a new branch for each contribution
-- Read the Code Style guide
-- Work on your changes
-- Test your changes
-- Push to your fork repository and create a new PR in GitHub.
+* Setting up the development environment
+* Fork the repository
+* Install pre-commit hooks
+* Create a new branch for each contribution
+* Read the Code Style guide
+* Work on your changes
+* Test your changes
+* Push to your fork repository and create a new PR in GitHub.
 
 
 Setting up the Development environment
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+--------------------------------------
 
 The recommended way to setup up the developer environment is the Anaconda
-(commonly referred as Conda).
-Conda quickly installs, runs and updates packages and their dependencies.
-It also allows to easily create, save, load and switch between different
-environments on your local computer.
+(commonly referred to as Conda).
+Conda quickly installs, runs, and updates packages and their dependencies.
+It also allows you to create, save, load, and switch between different environments on your local computer.
 
 Before continuing, Mac OSX users also need to install a more recent compiler.
-See instructions here: :ref:`OSX users <install_osx_users>`.
+See instructions `here <https://pysteps.readthedocs.io/en/latest/user_guide/install_pysteps.html#install-osx-users>`__.
 
 The developer environment can be created from the file
 `environment_dev.yml <https://github.com/pySTEPS/pysteps/blob/master/environment_dev.yml>`_
@@ -82,32 +74,26 @@ but yet is still editable from the source tree::
 
     pip install -e <path to local pysteps repo>
 
-To test if the installation went fine, you can try launching Python and importing
-pysteps :ref:`Import pysteps <_import_pysteps>`.
+To test if the installation went fine, you can try importing pysteps from the python interpreter by running::
 
-**Note**: In case of installation or import errors, you can remove the pysteps_dev environment
-and try starting from an empty environment (adapt the Python version as needed)::
+    python -c "import pysteps"
 
-    conda create -n pysteps_dev python=3.8
-
-You can then activate it and install pysteps with pip as exaplained above.
 
 Fork the repository
 ~~~~~~~~~~~~~~~~~~~
 
-Once you have set the development environment, the next step is creating your
-local copy of the repository where you will commit your modifications.
+Once you have set the development environment, the next step is creating your local copy of the repository, where you will commit your modifications.
 The steps to follow are:
 
-1. Set up Git in your computer.
-2. Create a GitHub account (if you don't have one).
-3. Fork the repository in your GitHub.
-4. Clone local copy of your fork. For example::
+#. Set up Git on your computer.
+#. Create a GitHub account (if you don't have one).
+#. Fork the repository in your GitHub.
+#. Clone a local copy of your fork. For example::
 
     git clone https://github.com/<your-account>/pysteps.git
 
 Done!, now you have a local copy of pysteps git repository.
-If you are new to GitHub, below you can find a list of useful tutorials:
+If you are new to GitHub, below you can find a list of helpful tutorials:
 
 - http://rogerdudler.github.io/git-guide/index.html
 - https://www.atlassian.com/git/tutorials
@@ -115,30 +101,26 @@ If you are new to GitHub, below you can find a list of useful tutorials:
 Install pre-commit hooks
 ~~~~~~~~~~~~~~~~~~~~~~~~
 
-Once you cloned the repository and the development environment is installed (and active),
-install the git pre-commit hook by executing the following command in the repository's
+After setting up your development environment, install the git pre-commit hook by executing the following command in the repository's
 root::
 
     pre-commit install
 
-The pre-commit hooks are scripts that are executed in every commit automatically to
-identify simple issues with the code. If an issue is identified (the pre-commit script
-exits with non-zero status), the hook aborts the commit and print the error.
-Currently, Pysteps only test the code to be committed complies with black's autoformat.
+The pre-commit hooks are scripts executed automatically in every commit to identify simple issues with the code.
+When an issue is identified (the pre-commit script exits with non-zero status), the hook aborts the commit and prints the error.
+Currently, Pysteps only tests that the code to be committed complies with black's format style.
 In case that the commit is aborted, you only need to run black in the entire source code.
 This can be done by running :code:`black .` or :code:`pre-commit run --all-files`.
-The latter is recommended since it indicates if the commit contained any formatting errors
-(that are automatically corrected).
-To ensure that the same configuration is used in every development environment, black's
-configuration is stored in the `pyproject.toml` file.
-This configuration is automatically load when black is run from any directory in the
+The latter is recommended since it indicates if the commit contained any formatting errors (that are automatically corrected).
+Black's configuration is stored in the `pyproject.toml` file to ensure that the same configuration is used in every development environment.
+This configuration is automatically loaded when black is run from any directory in the
 pysteps project.
 
 IMPORTANT: Periodically update the black version used in the pre-commit hook by running::
 
     pre-commit autoupdate
 
-For more information about git hooks and the pre-commit package see:
+For more information about git hooks and the pre-commit package, see:
 
 - https://git-scm.com/book/en/v2/Customizing-Git-Git-Hooks
 - https://pre-commit.com/
@@ -147,55 +129,32 @@ For more information about git hooks and the pre-commit package see:
 Create a new branch
 ~~~~~~~~~~~~~~~~~~~
 
-As a collaborator, all the new contributions that you want should be done in a
-new branch under your forked repository.
+As a collaborator, all the new contributions you want should be made in a new branch under your forked repository.
 Working on the master branch is reserved for Core Contributors only.
 Core Contributors are developers that actively work and maintain the repository.
-They are the only ones who accept pull requests and push commits directly to
-the pysteps repository.
+They are the only ones who accept pull requests and push commits directly to the pysteps repository.
 
 For more information on how to create and work with branches, see
-`"Branches in a Nutshell" <https://git-scm.com/book/en/v2/Git-Branching-Branches-in-a-Nutshell>`_ in the Git documentation
+`"Branches in a Nutshell" <https://git-scm.com/book/en/v2/Git-Branching-Branches-in-a-Nutshell>`__ in the Git documentation
 
 
 Code Style
-~~~~~~~~~~
-
-We strongly suggest to follow the
-`PEP8 coding standards <https://www.python.org/dev/peps/pep-0008/>`_.
-Note that this is not strictly enforced yet, since many source files in pysteps
-are not PEP8 compliant.
-However, we encourage new contributions to be compliant at least with the coding style
-summarized next.
-
-Coding style summary
-^^^^^^^^^^^^^^^^^^^^
-
-For quick reference, these are the most important good coding practices
-to follow:
-
-- Always use 4 spaces for indentation (don’t use tabs).
-- Write UTF-8 (add **# -*- coding: utf-8 -*-** at the top of each file).
-- Max line-length: 88 characters (note that we don't use the PEP8's 79 value).
-- Always indent wrapped code for readability.
-- Avoid extraneous whitespace.
-- Don’t use whitespace to line up assignment operators (=, :).
-- Spaces around = for assignment.
-- No spaces around = for default parameter values (keywords).
-- Spaces around mathematical operators, but group them sensibly.
-- No multiple statements on the same line.
-- Naming conventions:
-
-   Function names, variable names, and filenames should be descriptive and self
-   explanatory. Avoid using abbreviations that are ambiguous or unfamiliar to
-   readers outside your project, and do not abbreviate by deleting letters
-   within a word.
-   Avoid single letter variables if possible and use more verbose names for
-   clarity. An exception for this are indexes in loops (*i, j, k, etc*).
-
-   The following table summarizes the conventions:
-
-    .. raw:: html
+----------
+
+We strongly suggest following the
+`PEP8 coding standards <https://www.python.org/dev/peps/pep-0008/>`__.
+Since PEP8 is a set of recommendations, these are the most important good coding practices for the pysteps project:
+
+* Always use four spaces for indentation (don’t use tabs).
+* Max line-length: 88 characters (note that we don't use the PEP8's 79 value). Enforced by `black`.
+* Always indent wrapped code for readability. Enforced by `black`.
+* Avoid extraneous whitespace. Enforced by `black`.
+* Don’t use whitespace to line up assignment operators (=, :). Enforced by `black`.
+* Avoid writing multiple statements in the same line.
+* Naming conventions should follow the recomendations from
+  the `Google's python style guide <http://google.github.io/styleguide/pyguide.html>`__, summarized as follows:
+
+   .. raw:: html
 
         <table rules="all" border="1" cellspacing="2" cellpadding="2">
 
@@ -272,20 +231,16 @@ to follow:
           </tr>
 
         </table>
+   (source: `Section 3.16.4, Google's python style guide <http://google.github.io/styleguide/pyguide.html>`__)
 
-    Source: `Google's python style guide
-    <http://google.github.io/styleguide/pyguide.html>`_
-
-- Ignore returned variables:
-
-  If you need to ignore part to the variables returned by a function,
+- If you need to ignore part of the variables returned by a function,
   use "_" (single underscore) or __ (double underscore)::
 
     precip, __, metadata = import_bom_rf3('example_file.bom')
     precip2, _, metadata2 = import_bom_rf3('example_file2.bom')
 
 
-- Zen of Python (PEP 20), the guiding principles for Python’s
+- Zen of Python (`PEP 20 <https://www.python.org/dev/peps/pep-0020/>`__), the guiding principles for Python’s
   design::
 
     >>> import this
@@ -311,19 +266,21 @@ to follow:
     If the implementation is easy to explain, it may be a good idea.
     Namespaces are one honking great idea -- let's do more of those!
 
-For a detailed description of a pythonic code style check these guidelines:
+For more suggestions on good coding practices for python, check these guidelines:
+
+- `The Hitchhiker's Guide to Python <https://docs.python-guide.org/writing/style/>`__
+- `Google's python style guide <http://google.github.io/styleguide/pyguide.html>`__
+- `PEP8 <https://www.python.org/dev/peps/pep-0008/>`__
 
-- `The Hitchhiker's Guide to Python <https://docs.python-guide.org/writing/style/>`_
-- `Google's python style guide <http://google.github.io/styleguide/pyguide.html>`_
-- `PEP8 <https://www.python.org/dev/peps/pep-0008/>`_
 
+**Using Black auto-formatter**
 
-**Auto-formatters**
+To ensure a minimal style consistency, we use
+`black <https://black.readthedocs.io/en/stable/>`__ to auto-format to the source code.
+The black configuration used in the pysteps project is defined in the pyproject.toml,
+and it is automatically detected by black.
 
-Hand-formatting code according the PEP8 style guide can be a tedious process if it is done
-manually. To make our lives easy, in pysteps, we use
-`black <https://black.readthedocs.io/en/stable/>`_ to auto-format the code using its
-default configuration. Black can be installed using any of the following::
+Black can be installed using any of the following::
 
     conda install black
 
@@ -332,25 +289,23 @@ default configuration. Black can be installed using any of the following::
 
     pip install black
 
-Check the `official documentation <https://black.readthedocs.io/en/stable/the_black_code_style.html>`_
+Check the `official documentation <https://black.readthedocs.io/en/stable/the_black_code_style.html>`__
 for more information.
 
-
 **Docstrings**
 
 Every module, function, or class must have a docstring that describe its
-purpose and how to use it, following the conventions described in the
-`PEP 257 <https://www.python.org/dev/peps/pep-0257/#multi-line-docstrings>`_
+purpose and how to use it. The docstrings follows the conventions described in the
+`PEP 257 <https://www.python.org/dev/peps/pep-0257/#multi-line-docstrings>`__
 and the
-`Numpy's docstrings format <https://numpydoc.readthedocs.io/en/latest/format.html>`_.
+`Numpy's docstrings format <https://numpydoc.readthedocs.io/en/latest/format.html>`__.
 
 Here is a summary of the most important rules:
 
-- One-line docstrings Triple quotes are used even though the string fits on one line.
-  This makes it easy to later expand it.
-- A one-line docstring is a phrase ending in a period.
-- All docstrings should be written in imperative ("""Return some value.""")
-  mood rather than descriptive mood ("""Returns some value.""").
+- Always use triple quotes for doctrings, even if it fits a single line.
+- For one-line docstring, end the phrase with a period.
+- Use imperative mood for all docstrings ("""Return some value.""") rather than descriptive mood
+  ("""Returns some value.""").
 
 Here is an example of a docstring::
 
@@ -373,80 +328,49 @@ Here is an example of a docstring::
         """
 
 
-Working on changes
-~~~~~~~~~~~~~~~~~~
-
-**IMPORTANT**
-
-If your changes will take a significant amount of work,
-we highly recommend opening an issue first, explaining what you want
-to do and why. It is better to start the discussions early, in case other
-contributors disagree with what you would like to do or have ideas
-that will help you do it.
+Contributions guidelines
+------------------------
 
+The collaborator guidelines used in pysteps were largely inspired by those of the
+`MyPy project <https://github.com/python/mypy>`__.
 
-**Collaborators guidelines**
+Collaborators guidelines
+~~~~~~~~~~~~~~~~~~~~~~~~
 
-As a collaborator, all the new contributions that you want should be done in a
-new branch under your forked repository.
-Working on the master branch is reserved for Core Contributors only.
+As a collaborator, all your new contributions should be made in a new branch under your forked repository.
+Working on the master branch is reserved for Core Contributors only to submit small changes only.
 Core Contributors are developers that actively work and maintain the repository.
 They are the only ones who accept pull requests and push commits directly to
 the **pysteps** repository.
 
-To include the contributions for collaborators, we use the usual
-`GitHub pull-request flow <https://help.github.com/en/articles/github-flow>`_.
-In their simplest form, pull requests are a mechanism for
-a collaborator to notify to the pysteps project about a completed feature".
-
-Once your proposed changes are ready, you need to create a pull request via
-your GitHub account. Afterward, the core developers review the code and merge
-it into the master branch.
-Be aware that pull requests are more than just a notification, they are also
-an excellent place for discussing the proposed feature. If there is any problem
-with the changes, the other project collaborators can post feedback and the
-author of the commit can even fix the problems by pushing follow-up commits to
-feature branch.
-
-Do not squash your commits after you have submitted a pull request, as this
-erases context during the review.
-The commits will be squashed when the pull request is merged.
-
-To keep you forked repository clean, we suggest deleting branches for
-once the Pull Requests (PRs) are accepted and merged.
-
-Once you've created a pull request, you can push commits from your topic branch
-to add them to your existing pull request.
-These commits will appear in chronological order within your pull request and
-the changes will be visible in the "Files changed" tab.
-
-Other contributors can review your proposed changes, add review comments,
-contribute to the pull request discussion, and even add commits to the pull
-request.
-
-**Important:** each PR should should only address a single objective
-(e.g. fix a bug, improve documentation, etc).
-Pushing changes to an open PR that are outside its objective are highly
-discouraged.
-Under this circumstances, the recommended way to proceed is creating a new PR
-for changes, clearly explaining their goal.
+**IMPORTANT**
+However, for contribution requires a significant amount of work, we strongly suggest opening a new issue with
+the **enhancement** or **discussion** tag to encourage discussions.
+The discussions will help clarify the best way to approach the suggested changes or raise potential concerns.
+
+For code contributions, collaboratos can use the usual
+`GitHub pull-request flow <https://help.github.com/en/articles/github-flow>`__.
+Once your proposed changes are ready, you need to create a pull request (PR) from your fork in your GitHub account.
+Afterward, core contributors will review your proposed changes, provide feedback in the PR discussion, and maybe,
+request changes to the code. Once the PR is ready, a Core Developer will merge the changes into the main branch.
+
+**Important:**
+It is strongly suggested that each PR only addresses a single objective (e.g., fix a bug, improve documentation, etc.).
+This will help to reduce the time needed to process the PR. For changes outside the PR's objectives, we highly
+recommend opening a new PR.
 
 
 Testing your changes
 ~~~~~~~~~~~~~~~~~~~~
 
-Before committing changes or creating pull requests, check that the all the
-tests in the pysteps suite pass.
-See the :ref:`testing_pysteps` for the instruction to run the tests.
+Before committing changes or creating pull requests, check that all the tests in the pysteps suite pass.
+See the `Testing pySTEPS <https://pysteps.readthedocs.io/en/latest/developer_guide/test_pysteps.html#testing-pysteps>`__
+for detailed instruction to run the tests.
 
-
-Although it is not strictly needed, we suggest creating minimal tests for
-new contributions to ensure that it achieves the desired behavior.
-Pysteps uses the pytest framework, that it is easy to use and also
-supports complex functional testing for applications and libraries.
-Check the
-`pytests official documentation <https://docs.pytest.org/en/latest/index.html>`_
-for more information.
+Although it is not strictly needed, we suggest creating minimal tests for new contributions to ensure that it achieves
+the desired behavior. Pysteps uses the pytest framework that it is easy to use and also supports complex functional
+testing for applications and libraries.
+Check the `pytests official documentation <https://docs.pytest.org/en/latest/index.html>`_ for more information.
 
 The tests should be placed under the
 `pysteps.tests <https://github.com/pySTEPS/pysteps/tree/master/pysteps/tests>`_
@@ -457,9 +381,8 @@ descriptive name.
 A quick way to get familiar with the pytest syntax and the testing procedures
 is checking the python scripts present in the pysteps test module.
 
-
 Core developer guidelines
--------------------------
+~~~~~~~~~~~~~~~~~~~~~~~~
 
 Working directly on the master branch is discouraged and is reserved only
 for small changes and updates that do not compromise the stability of the code.
@@ -467,33 +390,23 @@ The *master* branch is a production branch that is ready to be deployed
 (cloned, installed, and ready to use).
 In consequence, this master branch is meant to be stable.
 
-The pysteps repository uses a Travis CI, a Continuous Integration service that
-automatically runs a series of tests every time you commit to GitHub.
-In that way, your modifications along with the entire package is tested.
-
-Pushing untested or work-in-progress changes to the master branch can potentially
-introduce bugs or brake the stability of the package.
-Since the tests takes around 10 minutes and the are run after the commit was
-pushed, any errors introduced in that commit will be noticed after the stable
-in the master branch was compromised.
-In addition, other developers start working on a new feature from master,
-they may start a potentially broken state.
-
-Instead, it is recommended to work on each new feature in its own branch,
-which can be pushed to the central repository for backup/collaboration.
-When you’re done with the development work on the feature, then you can merge
-the feature branch into the master or submit a Pull Request.
-This approach has two main advantages:
-
-- Every commit on the feature branch is tested using Travis CI.
-  If the tests fail, they do not affect the **master** branch.
+The pysteps repository uses the GitHub Actions service to run tests every time you commit to GitHub.
+In that way, your modifications along with the entire library are tested.
+
+Pushing untested or work-in-progress changes to the master branch can potentially introduce bugs or break the stability of the package.
+Since the tests triggered by a commit to the master branch take around 20 minutes, any errors introduced there
+will be noticed after the stablility of the master branch was compromised.
+In addition, other developers start working on a new feature from master from a potentially broken state.
 
-- Once the new feature, improvement, or bug correction is finished and the all
-  tests passed, the commits history can be squashed into a single commit and
-  then merged into the master branch.
+Instead, it is recommended to work on each new feature in its own branch, which can be pushed to the central repository
+for backup/collaboration. When you’re done with the feature's development work, you can merge the feature branch into the
+master or submit a Pull Request. This approach has two main advantages:
+
+- Every commit on the feature branch is tested via GitHub Actions.
+  If the tests fail, they do not affect the **master** branch.
 
-This helps approach helps to keep the commits history clean and allows
-experimentation in the branch without compromising the stability of the package.
+- Once the changes are finished and the tests passed, the commits history can be squashed into a single commit and
+  then merged into the master branch. Squashing the commits helps to keep a clean commit history in the main branch.
 
 
 Processing pull requests
@@ -501,43 +414,27 @@ Processing pull requests
 
 .. _`Squash and merge`: https://github.com/blog/2141-squash-your-commits
 
-Core developers should follow these rules when processing pull requests:
+To process the pull request, we follow similar rules to those used in the
+`mypy <https://github.com/python/mypy/blob/master/CONTRIBUTING.md#core-developer-guidelines>`_
+project:
 
 * Always wait for tests to pass before merging PRs.
-* Use "`Squash and merge`_"
-  to merge PRs.
-* Delete branches for merged PRs (by core devs pushing to the main repo).
-* Edit the final commit message before merging to conform to the following
-  style to help having a clean `git log` output:
-
-    * When merging a multi-commit PR make sure that the commit message doesn't
-      contain the local history from the committer and the review history from
-      the PR. Edit the message to only describe the end state of the PR.
-
-    * Make sure there is a *single* newline at the end of the commit message.
-      This way there is a single empty line between commits in `git log`
-      output.
-
-    * Split lines as needed so that the maximum line length of the commit
-      message is under 80 characters, including the subject line.
-
-    * Capitalize the subject and each paragraph.
-
-    * Make sure that the subject of the commit message has no trailing dot.
-
-    * Use the imperative mood in the subject line (e.g. "Fix typo in README").
-
-    * If the PR fixes an issue, make sure something like "Fixes #xxx." occurs
-      in the body of the message (not in the subject).
+* Always use "`Squash and merge`_"  to merge PRs.
+* Make sure that the subject of the commit message summarizes the objective of the PR and does not finish with a dot.
+* Write a new commit message before merging that provides a detailed description of the changes introduced by the PR.
+  Try to keep the maximum line length under 80 characters, spplitting lines if necessary.
+  **IMPORTANT:** Make sure that the commit message doesn't contain the branch's commit history!
+  Also, if the PR fixes an issue, mention this explicitly.
+* Use the imperative mood in the subject line (e.g. "Fix typo in README").
 
+After the PR is merged, the merged branch can be safely deleted.
 
 Preparing a new release
 ~~~~~~~~~~~~~~~~~~~~~~~
 
 Core developers should follow the steps to prepare a new release (version):
 
-1. Before creating the actual release in GitHub, be sure that every item in the
-   following checklist was followed:
+1. Before creating the actual release in GitHub, be sure that every item in the following checklist was followed:
 
     * In the file setup.py, update the **version="X.X.X"** keyword in the setup
       function.
@@ -545,24 +442,22 @@ Core developers should follow the steps to prepare a new release (version):
     * If new dependencies were added to pysteps since the last release, add
       them to the **environment.yml, requirements.txt**, and
       **requirements_dev.txt** files.
-
 #. Create a new release in GitHub following
    `these guidelines <https://help.github.com/en/articles/creating-releases>`_.
    Include a detailed changelog in the release.
-
 #. Generating the source distribution for new pysteps version and upload it to
    the `Python Package Index <https://pypi.org/>`_ (PyPI).
-   See :ref:`pypi_relase` for a detailed description of this process.
-
+   See `Packaging the pysteps project <https://pysteps.readthedocs.io/en/latest/developer_guide/pypi.html#pypi-relase>`__
+   for a detailed description of this process.
 #. Update the conda-forge pysteps-feedstock following this guidelines:
-   :ref:`update_conda_feedstock`
+   `Updating the conda-forge pysteps-feedstock <https://pysteps.readthedocs.io/en/latest/developer_guide/update_conda_forge.html#update-conda-feedstock>`__
 
 
 Credits
 -------
 
-This documents was based in contributors guides of two Python
-open source projects:
+This document was based on contributors guides of two Python
+open-source projects:
 
 * Py-Art_: Copyright (c) 2013, UChicago Argonne, LLC.
   `License <https://github.com/ARM-DOE/pyart/blob/master/LICENSE.txt>`_.
diff --git a/doc/requirements.txt b/doc/requirements.txt
index 42eda9fad..cd7856d7e 100644
--- a/doc/requirements.txt
+++ b/doc/requirements.txt
@@ -1,4 +1,4 @@
-# Additional requeriments related to the documentation build only
+# Additional requirements related to the documentation build only
 sphinx
 numpydoc
 sphinxcontrib.bibtex
diff --git a/doc/source/pysteps_reference/blending.rst b/doc/source/pysteps_reference/blending.rst
new file mode 100644
index 000000000..69edc1a4c
--- /dev/null
+++ b/doc/source/pysteps_reference/blending.rst
@@ -0,0 +1,14 @@
+================
+pysteps.blending
+================
+
+Implementation of blending methods for blending (ensemble) nowcasts with Numerical Weather Prediction (NWP) models.
+
+.. automodule:: pysteps.blending.clim
+.. automodule:: pysteps.blending.interface
+.. automodule:: pysteps.blending.skill_scores
+.. automodule:: pysteps.blending.utils
+.. automodule:: pysteps.blending.linear_blending
+.. automodule:: pysteps.blending.steps
+
+
diff --git a/doc/source/pysteps_reference/utils.rst b/doc/source/pysteps_reference/utils.rst
index 7e5474145..0622e6bbe 100644
--- a/doc/source/pysteps_reference/utils.rst
+++ b/doc/source/pysteps_reference/utils.rst
@@ -16,3 +16,4 @@ Implementation of miscellaneous utility functions.
 .. automodule:: pysteps.utils.spectral
 .. automodule:: pysteps.utils.tapering
 .. automodule:: pysteps.utils.transformation
+.. automodule:: pysteps.utils.reprojection
diff --git a/doc/source/references.bib b/doc/source/references.bib
index 6c4136f3d..c73f7cee2 100644
--- a/doc/source/references.bib
+++ b/doc/source/references.bib
@@ -1,4 +1,14 @@
 
+@TECHREPORT{BPS2004,
+  AUTHOR = "N. E. Bowler and C. E. Pierce and A. W. Seed",
+  TITLE = "{STEPS}: A probabilistic precipitation forecasting scheme which merges an extrapolation nowcast with downscaled {NWP}",
+  INSTITUTION = "UK Met Office",
+  TYPE = "Forecasting Research Technical Report",
+  NUMBER = 433,
+  ADDRESS = "Wallingford, United Kingdom",
+  YEAR = 2004,
+}
+
 @ARTICLE{BPS2006,
   AUTHOR = "N. E. Bowler and C. E. Pierce and A. W. Seed",
   TITLE = "{STEPS}: A probabilistic precipitation forecasting scheme which merges an extrapolation nowcast with downscaled {NWP}",
diff --git a/environment.yml b/environment.yml
index b71cd8de3..20609195b 100644
--- a/environment.yml
+++ b/environment.yml
@@ -13,3 +13,4 @@ dependencies:
   - pillow
   - pyproj
   - scipy
+  - xarray
diff --git a/environment_dev.yml b/environment_dev.yml
index b29e347fe..337a567d3 100644
--- a/environment_dev.yml
+++ b/environment_dev.yml
@@ -30,3 +30,5 @@ dependencies:
   - cartopy>=0.18
   - scikit-image
   - pandas
+  - xarray
+  - rasterio
diff --git a/examples/LK_buffer_mask.py b/examples/LK_buffer_mask.py
index 35efd5297..821f54f19 100644
--- a/examples/LK_buffer_mask.py
+++ b/examples/LK_buffer_mask.py
@@ -55,7 +55,7 @@
 
 # Read the radar composites
 importer = io.get_method(importer_name, "importer")
-R, quality, metadata = io.read_timeseries(fns, importer, **importer_kwargs)
+R, quality, metadata = io.read_timeseries(fns, importer, legacy=True, **importer_kwargs)
 
 del quality  # Not used
 
@@ -210,7 +210,7 @@
 )
 
 # Read and convert the radar composites
-R_o, _, metadata_o = io.read_timeseries(fns, importer, **importer_kwargs)
+R_o, _, metadata_o = io.read_timeseries(fns, importer, legacy=True, **importer_kwargs)
 R_o, metadata_o = conversion.to_rainrate(R_o, metadata_o)
 
 # Compute Spearman correlation
diff --git a/examples/advection_correction.py b/examples/advection_correction.py
index 5bab1d9bd..706807dbb 100644
--- a/examples/advection_correction.py
+++ b/examples/advection_correction.py
@@ -60,7 +60,7 @@
 
 # Read the radar composites
 importer = io.get_method(importer_name, "importer")
-R, __, metadata = io.read_timeseries(fns, importer, **importer_kwargs)
+R, __, metadata = io.read_timeseries(fns, importer, legacy=True, **importer_kwargs)
 
 # Convert to mm/h
 R, metadata = conversion.to_rainrate(R, metadata)
diff --git a/examples/anvil_nowcast.py b/examples/anvil_nowcast.py
index 57802ad4e..fe0f6ad0a 100644
--- a/examples/anvil_nowcast.py
+++ b/examples/anvil_nowcast.py
@@ -48,7 +48,7 @@
 # Read the input time series
 importer = io.get_method(importer_name, "importer")
 rainrate_field, quality, metadata = io.read_timeseries(
-    filenames, importer, **importer_kwargs
+    filenames, importer, legacy=True, **importer_kwargs
 )
 
 # Convert to rain rate (mm/h)
@@ -118,8 +118,8 @@
     date, root_path, path_fmt, fn_pattern, fn_ext, timestep=5, num_next_files=3
 )
 
-refobs_field, quality, metadata = io.read_timeseries(
-    filenames, importer, **importer_kwargs
+refobs_field, _, metadata = io.read_timeseries(
+    filenames, importer, legacy=True, **importer_kwargs
 )
 
 refobs_field, metadata = utils.to_rainrate(refobs_field[-1], metadata)
diff --git a/examples/blended_forecast.py b/examples/blended_forecast.py
new file mode 100644
index 000000000..2bf7806d0
--- /dev/null
+++ b/examples/blended_forecast.py
@@ -0,0 +1,284 @@
+# -*- coding: utf-8 -*-
+"""
+Blended forecast
+====================
+
+This tutorial shows how to construct a blended forecast from an ensemble nowcast
+using the STEPS approach and a Numerical Weather Prediction (NWP) rainfall 
+forecast. The used datasets are from the Royal Meteorological Insitute of Belgium.  
+
+"""
+
+from matplotlib import pyplot as plt
+import numpy as np
+import os
+from datetime import datetime
+
+import pysteps
+from pysteps import io, rcparams, cascade, blending
+from pysteps.visualization import plot_precip_field
+
+
+################################################################################
+# Read the radar images and the NWP forecast
+# ------------------------------------------
+#
+# First, we import a sequence of 3 images of 10-minute radar composites
+# and the corresponding NWP rainfall forecast that was available at that time.
+#
+# You need the pysteps-data archive downloaded and the pystepsrc file
+# configured with the data_source paths pointing to data folders.
+
+# Selected case
+date_radar = datetime.strptime("202010310400", "%Y%m%d%H%M")
+# The last NWP forecast was issued at 00:00
+date_nwp = datetime.strptime("202010310000", "%Y%m%d%H%M")
+radar_data_source = rcparams.data_sources["bom"]
+nwp_data_source = rcparams.data_sources["bom_nwp"]
+
+
+###############################################################################
+# Load the data from the archive
+# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+root_path = radar_data_source["root_path"]
+path_fmt = "prcp-c10/66/%Y/%m/%d"
+fn_pattern = "66_%Y%m%d_%H%M00.prcp-c10"
+fn_ext = radar_data_source["fn_ext"]
+importer_name = radar_data_source["importer"]
+importer_kwargs = radar_data_source["importer_kwargs"]
+timestep = 10.0
+
+# Find the radar files in the archive
+fns = io.find_by_date(
+    date_radar, root_path, path_fmt, fn_pattern, fn_ext, timestep, num_prev_files=2
+)
+
+# Read the radar composites
+importer = io.get_method(importer_name, "importer")
+r_radar = io.read_timeseries(fns, importer, legacy=False, **importer_kwargs)
+radar_data_xr = r_radar[-1, :, :]
+
+# Get the metadata
+radar_metadata = radar_data_xr.x.attrs.copy()
+radar_metadata.update(**radar_data_xr.y.attrs)
+radar_metadata.update(**radar_data_xr.attrs)
+
+# Import the NWP data
+filename = os.path.join(
+    nwp_data_source["root_path"],
+    datetime.strftime(date_nwp, nwp_data_source["path_fmt"]),
+    datetime.strftime(date_nwp, nwp_data_source["fn_pattern"])
+    + "."
+    + nwp_data_source["fn_ext"],
+)
+
+nwp_data_xr = io.import_bom_nwp_xr(filename)
+nwp_metadata = nwp_data_xr.x.attrs.copy()
+nwp_metadata.update(**nwp_data_xr.y.attrs)
+nwp_metadata.update(**nwp_data_xr.attrs)
+
+# Only keep the NWP forecasts from the last radar observation time (2020-10-31 04:00)
+# onwards
+r_nwp = nwp_data_xr.sel(
+    t=slice(np.datetime64("2020-10-31T04:00"), np.datetime64("2020-10-31T07:00"))
+)
+
+
+################################################################################
+# Pre-processing steps
+# --------------------
+
+# Make sure the units are in mm/h
+converter = pysteps.utils.get_method("mm/h")
+r_radar, radar_metadata = converter(r_radar, radar_metadata)
+r_nwp, nwp_metadata = converter(r_nwp, nwp_metadata)
+
+# Threshold the data
+r_radar.data[r_radar.data < 0.1] = 0.0
+r_nwp.data[r_nwp.data < 0.1] = 0.0
+
+# Plot the radar rainfall field and the first time step of the NWP forecast.
+# For the initial time step (t=0), the NWP rainfall forecast is not that different
+# from the observed radar rainfall, but it misses some of the locations and
+# shapes of the observed rainfall fields. Therefore, the NWP rainfall forecast will
+# initially get a low weight in the blending process.
+date_str = datetime.strftime(date_radar, "%Y-%m-%d %H:%M")
+plt.figure(figsize=(10, 5))
+plt.subplot(121)
+plot_precip_field(
+    r_radar[-1, :, :], geodata=radar_metadata, title=f"Radar observation at {date_str}"
+)
+plt.subplot(122)
+plot_precip_field(
+    r_nwp[0, :, :], geodata=nwp_metadata, title=f"NWP forecast at {date_str}"
+)
+plt.tight_layout()
+plt.show()
+
+# transform the data to dB
+transformer = pysteps.utils.get_method("dB")
+r_radar, radar_metadata = transformer(r_radar.values, radar_metadata, threshold=0.1)
+transformer = pysteps.utils.get_method("dB")
+r_nwp, nwp_metadata = transformer(r_nwp.values, nwp_metadata, threshold=0.1)
+
+# Initial decomposition settings
+decomp_method, recomp_method = cascade.get_method("fft")
+bandpass_filter_method = "gaussian"
+M, N = r_radar.shape[1:]
+n_cascade_levels = 8
+n_models = 1  # The number of NWP models to blend with the radar rainfall nowcast
+filter_method = cascade.get_method(bandpass_filter_method)
+filter = filter_method((M, N), n_cascade_levels)
+
+# r_nwp has to be four dimentional (n_models, time, y, x).
+# If we only use one model:
+if r_nwp.ndim == 3:
+    r_nwp = r_nwp[None, :]
+
+
+################################################################################
+# Determine the velocity fields
+# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+oflow_method = pysteps.motion.get_method("lucaskanade")
+
+# First for the radar images
+v_radar = oflow_method(r_radar)
+
+# Then for the NWP forecast
+v_nwp = []
+# Loop through the models
+for n_model in range(r_nwp.shape[0]):
+    # Loop through the timesteps. We need two images to construct a motion
+    # field, so we can start from timestep 1. Timestep 0 will be the same
+    # as timestep 1.
+    _v_nwp_ = []
+    for t in range(1, r_nwp.shape[1]):
+        v_nwp_ = oflow_method(r_nwp[n_model, t - 1 : t + 1, :])
+        _v_nwp_.append(v_nwp_)
+        v_nwp_ = None
+    # Add the velocity field at time step 1 to time step 0.
+    _v_nwp_ = np.insert(_v_nwp_, 0, _v_nwp_[0], axis=0)
+    v_nwp.append(_v_nwp_)
+v_nwp = np.stack(v_nwp)
+
+
+################################################################################
+# The blended forecast
+# --------------------
+
+precip_forecast = blending.steps.forecast(
+    R=r_radar,
+    R_d_models=r_nwp,
+    V=v_radar,
+    V_models=v_nwp,
+    timesteps=18,
+    timestep=timestep,
+    issuetime=date_radar,
+    n_ens_members=1,
+    n_cascade_levels=n_cascade_levels,
+    blend_nwp_members=False,
+    R_thr=radar_metadata["threshold"],
+    kmperpixel=radar_metadata["xpixelsize"] / 1000.0,
+    extrap_method="semilagrangian",
+    decomp_method="fft",
+    bandpass_filter_method="gaussian",
+    noise_method="nonparametric",
+    noise_stddev_adj="auto",
+    ar_order=2,
+    vel_pert_method=None,
+    weights_method="bps",
+    conditional=False,
+    probmatching_method="cdf",
+    mask_method="incremental",
+    callback=None,
+    return_output=True,
+    seed=None,
+    num_workers=1,
+    fft_method="numpy",
+    domain="spatial",
+    outdir_path_skill="./tmp",
+    extrap_kwargs=None,
+    filter_kwargs=None,
+    noise_kwargs=None,
+    vel_pert_kwargs=None,
+    clim_kwargs=None,
+    mask_kwargs=None,
+    measure_time=False,
+)
+
+# Transform the data back into mm/h
+precip_forecast, _ = converter(precip_forecast, radar_metadata)
+r_radar, _ = converter(r_radar, radar_metadata)
+r_nwp, _ = converter(r_nwp, nwp_metadata)
+
+
+################################################################################
+# Visualize the output
+# ~~~~~~~~~~~~~~~~~~~~
+#
+# The NWP rainfall forecast has a lower weight than the radar-based extrapolation
+# forecast at the issue time of the forecast (t=0). Therefore, the first time
+# steps consist mostly of the extrapolation.
+# However, near the end of the forecast (t=+3h), the NWP share in the blended
+# forecast has become more important and the forecast starts to resemble the
+# NWP forecast more.
+
+# Plot the blended forecast
+plt.figure(figsize=(10, 5))
+plt.subplot(131)
+plot_precip_field(
+    precip_forecast[0, 2, :, :],
+    geodata=radar_metadata,
+    title="Blended forecast at t + 30 min",
+)
+plt.subplot(132)
+plot_precip_field(
+    precip_forecast[0, 5, :, :],
+    geodata=radar_metadata,
+    title="Blended forecast at t + 60 min",
+)
+plt.subplot(133)
+plot_precip_field(
+    precip_forecast[0, 17, :, :],
+    geodata=radar_metadata,
+    title="Blended forecast at t + 180 min",
+)
+plt.tight_layout()
+plt.show()
+
+# Plot the NWP forecast for comparison
+plt.figure(figsize=(10, 5))
+plt.subplot(131)
+plot_precip_field(
+    r_nwp[0, 3, :, :], geodata=nwp_metadata, title="NWP forecast at t + 30 min"
+)
+plt.subplot(132)
+plot_precip_field(
+    r_nwp[0, 6, :, :], geodata=nwp_metadata, title="NWP forecast at t + 60 min"
+)
+plt.subplot(133)
+plot_precip_field(
+    r_nwp[0, 18, :, :], geodata=nwp_metadata, title="NWP forecast at t + 180 min"
+)
+plt.tight_layout()
+plt.show()
+
+
+################################################################################
+# References
+# ~~~~~~~~~~
+#
+# Bowler, N. E., and C. E. Pierce, and A. W. Seed. 2004. "STEPS: A probabilistic
+# precipitation forecasting scheme which merges an extrapolation nowcast with
+# downscaled NWP." Forecasting Research Technical Report No. 433. Wallingford, UK.
+#
+# Bowler, N. E., and C. E. Pierce, and A. W. Seed. 2006. "STEPS: A probabilistic
+# precipitation forecasting scheme which merges an extrapolation nowcast with
+# downscaled NWP." Quarterly Journal of the Royal Meteorological Society 132(16):
+# 2127-2155. https://doi.org/10.1256/qj.04.100
+#
+# Seed, A. W., and C. E. Pierce, and K. Norman. 2013. "Formulation and evaluation
+# of a scale decomposition-based stochastic precipitation nowcast scheme." Water
+# Resources Research 49(10): 6624-664. https://doi.org/10.1002/wrcr.20536
diff --git a/examples/data_transformations.py b/examples/data_transformations.py
index 24962ec6f..8b601ac14 100644
--- a/examples/data_transformations.py
+++ b/examples/data_transformations.py
@@ -58,7 +58,7 @@
 
 # Read the radar composites
 importer = io.get_method(importer_name, "importer")
-Z, _, metadata = io.read_timeseries(fns, importer, **importer_kwargs)
+Z, _, metadata = io.read_timeseries(fns, importer, legacy=True, **importer_kwargs)
 
 # Keep only positive rainfall values
 Z = Z[Z > metadata["zerovalue"]].flatten()
diff --git a/examples/optical_flow_methods_convergence.py b/examples/optical_flow_methods_convergence.py
index 8dd7522fc..f6289450a 100644
--- a/examples/optical_flow_methods_convergence.py
+++ b/examples/optical_flow_methods_convergence.py
@@ -62,7 +62,7 @@
 # Read the reference radar composite
 importer = io.get_method(importer_name, "importer")
 reference_field, quality, metadata = io.read_timeseries(
-    fns, importer, **importer_kwargs
+    fns, importer, legacy=True, **importer_kwargs
 )
 
 del quality  # Not used
diff --git a/examples/plot_cascade_decomposition.py b/examples/plot_cascade_decomposition.py
index 06e884156..49a3798d8 100644
--- a/examples/plot_cascade_decomposition.py
+++ b/examples/plot_cascade_decomposition.py
@@ -30,7 +30,7 @@
 filename = os.path.join(
     root_path, "20160928", "201609281600_fmi.radar.composite.lowest_FIN_SUOMI1.pgm.gz"
 )
-R, _, metadata = io.import_fmi_pgm(filename, gzipped=True)
+R, _, metadata = io.import_fmi_pgm(filename, gzipped=True, legacy=True)
 
 # Convert to rain rate
 R, metadata = conversion.to_rainrate(R, metadata)
diff --git a/examples/plot_ensemble_verification.py b/examples/plot_ensemble_verification.py
index 54d2500c8..799c43899 100644
--- a/examples/plot_ensemble_verification.py
+++ b/examples/plot_ensemble_verification.py
@@ -56,7 +56,7 @@
 
 # Read the data from the archive
 importer = io.get_method(importer_name, "importer")
-R, _, metadata = io.read_timeseries(fns, importer, **importer_kwargs)
+R, _, metadata = io.read_timeseries(fns, importer, legacy=True, **importer_kwargs)
 
 # Convert to rain rate
 R, metadata = conversion.to_rainrate(R, metadata)
@@ -142,7 +142,7 @@
 )
 
 # Read the observations
-R_o, _, metadata_o = io.read_timeseries(fns, importer, **importer_kwargs)
+R_o, _, metadata_o = io.read_timeseries(fns, importer, legacy=True, **importer_kwargs)
 
 # Convert to mm/h
 R_o, metadata_o = conversion.to_rainrate(R_o, metadata_o)
diff --git a/examples/plot_extrapolation_nowcast.py b/examples/plot_extrapolation_nowcast.py
index 203a5909d..2e76eeeef 100644
--- a/examples/plot_extrapolation_nowcast.py
+++ b/examples/plot_extrapolation_nowcast.py
@@ -48,7 +48,7 @@
 
 # Read the radar composites
 importer = io.get_method(importer_name, "importer")
-Z, _, metadata = io.read_timeseries(fns, importer, **importer_kwargs)
+Z, _, metadata = io.read_timeseries(fns, importer, legacy=True, **importer_kwargs)
 
 # Convert to rain rate
 R, metadata = conversion.to_rainrate(Z, metadata)
@@ -113,7 +113,7 @@
     num_next_files=n_leadtimes,
 )
 # Read the radar composites
-R_o, _, metadata_o = io.read_timeseries(fns, importer, **importer_kwargs)
+R_o, _, metadata_o = io.read_timeseries(fns, importer, legacy=True, **importer_kwargs)
 R_o, metadata_o = conversion.to_rainrate(R_o, metadata_o, 223.0, 1.53)
 
 # Compute fractions skill score (FSS) for all lead times, a set of scales and 1 mm/h
diff --git a/examples/plot_linear_blending.py b/examples/plot_linear_blending.py
new file mode 100644
index 000000000..dada60256
--- /dev/null
+++ b/examples/plot_linear_blending.py
@@ -0,0 +1,190 @@
+# -*- coding: utf-8 -*-
+
+"""
+Plot linear blending
+====================
+
+This tutorial shows how to construct a simple linear blending between an ensemble 
+nowcast and a dummy Numerical Weather Prediction (NWP) rainfall forecast.
+"""
+
+# TODO: Make xarray ready for pysteps-v2
+
+from matplotlib import pyplot as plt
+import numpy as np
+from pprint import pprint
+from datetime import datetime
+from pysteps.motion.lucaskanade import dense_lucaskanade
+from pysteps import io, rcparams
+from pysteps.utils import conversion, transformation
+from pysteps.visualization import plot_precip_field
+from pysteps.utils import dimension
+from pysteps.blending.linear_blending import forecast
+
+
+def gaussian(x, max, mean, sigma):
+    return max * np.exp(-(x - mean) * (x - mean) / sigma / sigma / 2)
+
+
+def dummy_nwp(precip, n_leadtimes, max=20, mean=0, sigma=0.25, speed=100):
+    """Generates dummy NWP data with the same dimension as the input
+    precipitation field precip. The NWP data is a vertical line with a Gaussian
+    profile moving to the left"""
+
+    # precip is original radar image
+    rows = precip.shape[0]
+    cols = precip.shape[1]
+
+    # Initialise the dummy NWP data
+    precip_nwp = np.zeros((n_leadtimes, rows, cols))
+    x = np.linspace(-5, 5, cols)
+
+    for n in range(n_leadtimes):
+        for i in range(rows):
+            precip_nwp[n, i, :] = gaussian(x, max, mean, sigma)
+        mean -= speed / rows
+
+    return precip_nwp
+
+
+###############################################################################
+# Set nowcast parameters
+n_ens_members = 10
+n_leadtimes = 18
+start_blending = 30  # in minutes
+end_blending = 60  # in minutes
+seed = 24
+
+# Read precipitation field
+# ------------------------
+#
+# First thing, the sequence of Swiss radar composites is imported, converted and
+# transformed into units of dBR.
+
+date = datetime.strptime("201701311200", "%Y%m%d%H%M")
+data_source = "mch"
+
+# Load data source config
+root_path = rcparams.data_sources[data_source]["root_path"]
+path_fmt = rcparams.data_sources[data_source]["path_fmt"]
+fn_pattern = rcparams.data_sources[data_source]["fn_pattern"]
+fn_ext = rcparams.data_sources[data_source]["fn_ext"]
+importer_name = rcparams.data_sources[data_source]["importer"]
+importer_kwargs = rcparams.data_sources[data_source]["importer_kwargs"]
+timestep = rcparams.data_sources[data_source]["timestep"]
+
+# Find the radar files in the archive
+fns = io.find_by_date(
+    date, root_path, path_fmt, fn_pattern, fn_ext, timestep, num_prev_files=2
+)
+
+# Read the data from the archive
+importer = io.get_method(importer_name, "importer")
+precip = io.read_timeseries(fns, importer, legacy=False, **importer_kwargs)
+
+# Get the metadata
+metadata = precip.x.attrs.copy()
+metadata.update(**precip.y.attrs)
+metadata.update(**precip.attrs)
+
+# Convert to rain rate
+precip, metadata = conversion.to_rainrate(precip, metadata)
+
+# Upscale data to 2 km to limit memory usage
+precip, metadata = dimension.aggregate_fields_space(precip, metadata, 2000)
+
+# Import the dummy NWP data (vertical line moving to the left)
+precip_nwp = dummy_nwp(
+    precip[-1, :, :], n_leadtimes + 1, max=7, mean=4, speed=0.2 * 350
+)
+metadata_nwp = metadata.copy()
+
+# Plot the radar rainfall field and the first time step of the dummy NWP forecast.
+date_str = datetime.strftime(date, "%Y-%m-%d %H:%M")
+plt.figure(figsize=(10, 5))
+plt.subplot(121)
+plot_precip_field(
+    precip[-1, :, :], geodata=metadata, title=f"Radar observation at {date_str}"
+)
+plt.subplot(122)
+plot_precip_field(
+    precip_nwp[0, :, :], geodata=metadata_nwp, title=f"Dummy NWP forecast at {date_str}"
+)
+plt.tight_layout()
+plt.show()
+
+# Log-transform the data to unit of dBR, set the threshold to 0.1 mm/h,
+# set the fill value to -15 dBR
+precip, metadata = transformation.dB_transform(
+    precip, metadata, threshold=0.1, zerovalue=-15.0
+)
+
+# Nicely print the metadata
+pprint(metadata)
+
+###############################################################################
+# Linear blending of nowcast and NWP data
+
+# Estimate the motion field
+velocity = dense_lucaskanade(precip)
+
+# Define nowcast keyword arguments
+nowcast_kwargs = {
+    "n_ens_members": n_ens_members,
+    "n_cascade_levels": 6,
+    "R_thr": -10.0,
+    "kmperpixel": 2.0,
+    "timestep": timestep,
+    "noise_method": "nonparametric",
+    "vel_pert_method": "bps",
+    "mask_method": "incremental",
+}
+
+# Calculate the blended precipitation field
+precip_blended = forecast(
+    precip=precip[-3:, :, :],
+    precip_metadata=metadata,
+    velocity=velocity,
+    timesteps=n_leadtimes,
+    timestep=timestep,
+    nowcast_method="steps",
+    precip_nwp=precip_nwp[1:, :, :],
+    precip_nwp_metadata=metadata_nwp,
+    start_blending=start_blending,
+    end_blending=end_blending,
+    nowcast_kwargs=nowcast_kwargs,
+)
+
+"""
+extrap_kwargs = {"allow_nonfinite_values": True}
+nowcast_kwargs = {"extrap_kwargs": extrap_kwargs}
+
+# Calculate the blended precipitation field
+precip_blended = forecast(
+    precip[-1, :, :],
+    V,
+    n_leadtimes,
+    timestep,
+    "extrapolation",
+    precip_nwp=precip_nwp[1: :, :],
+    start_blending=start_blending,
+    end_blending=end_blending,
+    nowcast_kwargs=nowcast_kwargs,
+)
+"""
+
+# Calculate the ensemble average
+if len(precip_blended.shape) == 4:
+    precip_blended_mean = np.mean(precip_blended[:, :, :, :], axis=0)
+else:
+    precip_blended_mean = np.copy(precip_blended)
+
+# Plot the blended field
+for i in range(0, n_leadtimes, 3):
+    plot_precip_field(
+        precip_blended_mean[i, :, :],
+        geodata=metadata,
+        title="Blended field (+ %i min)" % ((i + 1) * timestep),
+    )
+    plt.show()
+    plt.close()
diff --git a/examples/plot_noise_generators.py b/examples/plot_noise_generators.py
index dd392ca4f..a809eefa1 100644
--- a/examples/plot_noise_generators.py
+++ b/examples/plot_noise_generators.py
@@ -34,7 +34,9 @@
 # Import the example radar composite
 root_path = rcparams.data_sources["mch"]["root_path"]
 filename = os.path.join(root_path, "20160711", "AQC161932100V_00005.801.gif")
-R, _, metadata = io.import_mch_gif(filename, product="AQC", unit="mm", accutime=5.0)
+R, _, metadata = io.import_mch_gif(
+    filename, product="AQC", unit="mm", accutime=5.0, legacy=True
+)
 
 # Convert to mm/h
 R, metadata = conversion.to_rainrate(R, metadata)
diff --git a/examples/plot_optical_flow.py b/examples/plot_optical_flow.py
index e1bc6035b..89ed3b37e 100644
--- a/examples/plot_optical_flow.py
+++ b/examples/plot_optical_flow.py
@@ -47,7 +47,7 @@
 
 # Read the radar composites
 importer = io.get_method(importer_name, "importer")
-R, quality, metadata = io.read_timeseries(fns, importer, **importer_kwargs)
+R, quality, metadata = io.read_timeseries(fns, importer, legacy=True, **importer_kwargs)
 
 del quality  # Not used
 
diff --git a/examples/plot_steps_nowcast.py b/examples/plot_steps_nowcast.py
index 43b01a6b4..38139b848 100644
--- a/examples/plot_steps_nowcast.py
+++ b/examples/plot_steps_nowcast.py
@@ -51,7 +51,7 @@
 
 # Read the data from the archive
 importer = io.get_method(importer_name, "importer")
-R, _, metadata = io.read_timeseries(fns, importer, **importer_kwargs)
+R, _, metadata = io.read_timeseries(fns, importer, legacy=True, **importer_kwargs)
 
 # Convert to rain rate
 R, metadata = conversion.to_rainrate(R, metadata)
diff --git a/examples/probability_forecast.py b/examples/probability_forecast.py
index ee996c9ea..059115bc1 100644
--- a/examples/probability_forecast.py
+++ b/examples/probability_forecast.py
@@ -74,7 +74,7 @@
 date = datetime.strptime("201607112100", "%Y%m%d%H%M")
 fns = io.find_by_date(date, root, fmt, pattern, ext, timestep, num_prev_files=2)
 importer = io.get_method(importer_name, "importer")
-precip, __, metadata = io.read_timeseries(fns, importer, **importer_kwargs)
+precip, __, metadata = io.read_timeseries(fns, importer, legacy=True, **importer_kwargs)
 precip, metadata = utils.to_rainrate(precip, metadata)
 # precip[np.isnan(precip)] = 0
 
@@ -124,7 +124,7 @@
 fns = io.find_by_date(
     date, root, fmt, pattern, ext, timestep, num_next_files=nleadtimes
 )
-obs, __, metadata = io.read_timeseries(fns, importer, **importer_kwargs)
+obs, __, metadata = io.read_timeseries(fns, importer, legacy=True, **importer_kwargs)
 obs, metadata = utils.to_rainrate(obs, metadata)
 obs[np.isnan(obs)] = 0
 
diff --git a/examples/rainfarm_downscale.py b/examples/rainfarm_downscale.py
index 1f6a635f1..be4033989 100644
--- a/examples/rainfarm_downscale.py
+++ b/examples/rainfarm_downscale.py
@@ -33,7 +33,7 @@
 root_path = rcparams.data_sources["mch"]["root_path"]
 filename = os.path.join(root_path, "20160711", "AQC161932100V_00005.801.gif")
 precip, _, metadata = io.import_mch_gif(
-    filename, product="AQC", unit="mm", accutime=5.0
+    filename, product="AQC", unit="mm", accutime=5.0, legacy=True
 )
 
 # Convert to mm/h
diff --git a/examples/thunderstorm_detection_and_tracking.py b/examples/thunderstorm_detection_and_tracking.py
index 1c82ded82..de3cc39e9 100644
--- a/examples/thunderstorm_detection_and_tracking.py
+++ b/examples/thunderstorm_detection_and_tracking.py
@@ -63,7 +63,7 @@
     date, root_path, path_fmt, fn_pattern, fn_ext, timestep, num_next_files=20
 )
 importer = io.get_method(importer_name, "importer")
-R, _, metadata = io.read_timeseries(fns, importer, **importer_kwargs)
+R, _, metadata = io.read_timeseries(fns, importer, legacy=True, **importer_kwargs)
 
 # Convert to reflectivity (it is possible to give the a- and b- parameters of the
 # Marshall-Palmer relationship here: zr_a = and zr_b =).
diff --git a/pysteps/blending/__init__.py b/pysteps/blending/__init__.py
new file mode 100644
index 000000000..640d81d01
--- /dev/null
+++ b/pysteps/blending/__init__.py
@@ -0,0 +1,7 @@
+# -*- coding: utf-8 -*-
+"""Methods for blending NWP model(s) with nowcasts."""
+
+from pysteps.blending.interface import get_method
+from .clim import *
+from .skill_scores import *
+from .utils import *
diff --git a/pysteps/blending/clim.py b/pysteps/blending/clim.py
new file mode 100644
index 000000000..0bb9ecabb
--- /dev/null
+++ b/pysteps/blending/clim.py
@@ -0,0 +1,209 @@
+"""
+pysteps.blending.clim
+=====================
+
+Module with methods to read, write and compute past and climatological NWP model 
+skill scores. The module stores the average daily skill score for the past t days
+and updates it every day. The resulting average climatological skill score is
+the skill the NWP model skill regresses to during the blended forecast. If no
+climatological values are present, the default skill from :cite:`BPS2006` is used.
+
+.. autosummary::
+    :toctree: ../generated/
+
+    get_default_skill    
+    save_skill
+    calc_clim_skill
+"""
+
+import pickle
+from pathlib import Path
+
+import numpy as np
+
+
+def get_default_skill(n_cascade_levels=8, n_models=1):
+    """
+    Get the default climatological skill values as given in :cite:`BPS2006`.
+    Take subset of n_cascade_levels or add entries with small values (1e-4) if
+    n_cascade_levels differs from 8.
+
+    Parameters
+    ----------
+    n_cascade_levels: int, optional
+      Number of cascade levels. Defaults to 8.
+    n_models: int, optional
+      Number of NWP models. Defaults to 1.
+
+    Returns
+    -------
+    default_skill: array-like
+      Array of shape [model, scale_level] containing the climatological skill
+      values.
+
+    """
+
+    default_skill = np.array(
+        [0.848, 0.537, 0.237, 0.065, 0.020, 0.0044, 0.0052, 0.0040]
+    )
+    n_skill = default_skill.shape[0]
+    if n_cascade_levels < n_skill:
+        default_skill = default_skill[0:n_cascade_levels]
+    elif n_cascade_levels > n_skill:
+        default_skill = np.append(
+            default_skill, np.repeat(1e-4, n_cascade_levels - n_skill)
+        )
+    return np.resize(default_skill, (n_models, n_cascade_levels))
+
+
+def save_skill(
+    current_skill,
+    validtime,
+    outdir_path,
+    n_models=1,
+    window_length=30,
+):
+    """
+    Add the current NWP skill to update today's daily average skill. If the day
+    is over, update the list of daily average skill covering a rolling window.
+
+    Parameters
+    ----------
+    current_skill: array-like
+      Array of shape [model, scale_level, ...]
+      containing the current skill of the different NWP models per cascade
+      level.
+    validtime: datetime
+      Datetime object containing the date and time for which the current
+      skill are valid.
+    outdir_path: string
+      Path to folder where the historical skill are stored. Defaults to
+      path_workdir from rcparams.
+    n_models: int, optional
+      Number of NWP models. Defaults to 1.
+    window_length: int, optional
+      Length of window (in days) of daily skill that should be retained.
+      Defaults to 30.
+
+    Returns
+    -------
+    None
+
+    """
+
+    n_cascade_levels = current_skill.shape[1]
+
+    # Load skill_today, a dictionary containing {mean_skill, n, last_validtime}
+    new_skill_today_file = False
+
+    skill_today_file = Path(outdir_path) / "NWP_skill_today.pkl"
+    if skill_today_file.is_file():
+        with open(skill_today_file, "rb") as f:
+            skill_today = pickle.load(f)
+        if skill_today["mean_skill"].shape != current_skill.shape:
+            new_skill_today_file = True
+    else:
+        new_skill_today_file = True
+
+    if new_skill_today_file:
+        skill_today = {
+            "mean_skill": np.copy(current_skill),
+            "n": 0,
+            "last_validtime": validtime,
+        }
+
+    # Load the past skill which is an array with dimensions day x model x scale_level
+    past_skill_file = Path(outdir_path) / "NWP_skill_window.npy"
+    past_skill = None
+    if past_skill_file.is_file():
+        past_skill = np.load(past_skill_file)
+    # First check if we have started a new day wrt the last written skill, in which
+    # case we should update the daily skill file and reset daily statistics.
+    if skill_today["last_validtime"].date() < validtime.date():
+        # Append skill to the list of the past X daily averages.
+        if (
+            past_skill is not None
+            and past_skill.shape[2] == n_cascade_levels
+            and past_skill.shape[1] == n_models
+        ):
+            past_skill = np.append(past_skill, [skill_today["mean_skill"]], axis=0)
+        else:
+            past_skill = np.array([skill_today["mean_skill"]])
+
+        # Remove oldest if the number of entries exceeds the window length.
+        if past_skill.shape[0] > window_length:
+            past_skill = np.delete(past_skill, 0, axis=0)
+        # FIXME also write out last_validtime.date() in this file?
+        # In that case it will need pickling or netcdf.
+        # Write out the past skill within the rolling window.
+        np.save(past_skill_file, past_skill)
+        # Reset statistics for today.
+        skill_today["n"] = 0
+        skill_today["mean_skill"] = 0
+
+    # Reset today's skill if needed and/or compute online average from the
+    # current skill using numerically stable algorithm
+    skill_today["n"] += 1
+    skill_today["mean_skill"] += (
+        current_skill - skill_today["mean_skill"]
+    ) / skill_today["n"]
+    skill_today["last_validtime"] = validtime
+    # Make path if path does not exist
+    skill_today_file.parent.mkdir(exist_ok=True, parents=True)
+    # Open and write to skill file
+    with open(skill_today_file, "wb") as f:
+        pickle.dump(skill_today, f)
+
+    return None
+
+
+def calc_clim_skill(
+    outdir_path,
+    n_cascade_levels=8,
+    n_models=1,
+    window_length=30,
+):
+    """
+    Return the climatological skill based on the daily average skill in the
+    rolling window. This is done using a geometric mean.
+
+    Parameters
+    ----------
+    n_cascade_levels: int, optional
+      Number of cascade levels.
+    outdir_path: string
+      Path to folder where the historical skill are stored. Defaults to
+      path_workdir from rcparams.
+    n_models: int, optional
+      Number of NWP models. Defaults to 1.
+    window_length: int, optional
+      Length of window (in days) over which to compute the climatological
+      skill. Defaults to 30.
+
+    Returns
+    -------
+    climatological_mean_skill: array-like
+      Array of shape [model, scale_level, ...] containing the climatological
+      (geometric) mean skill.
+
+    """
+    past_skill_file = Path(outdir_path) / "NWP_skill_window.npy"
+    # past_skill has dimensions date x model x scale_level  x ....
+    if past_skill_file.is_file():
+        past_skill = np.load(past_skill_file)
+    else:
+        past_skill = np.array(None)
+    # check if there is enough data to compute the climatological skill
+    if not past_skill.any():
+        return get_default_skill(n_cascade_levels, n_models)
+    elif past_skill.shape[0] < window_length:
+        return get_default_skill(n_cascade_levels, n_models)
+    # reduce window if necessary
+    else:
+        past_skill = past_skill[-window_length:]
+
+    # Calculate climatological skill from the past_skill using the
+    # geometric mean.
+    geomean_skill = np.exp(np.log(past_skill).mean(axis=0))
+
+    return geomean_skill
diff --git a/pysteps/blending/interface.py b/pysteps/blending/interface.py
new file mode 100644
index 000000000..ad0314f96
--- /dev/null
+++ b/pysteps/blending/interface.py
@@ -0,0 +1,56 @@
+# -*- coding: utf-8 -*-
+"""
+pysteps.blending.interface
+==========================
+Interface for the blending module. It returns a callable function for computing
+blended nowcasts with NWP models.
+
+.. autosummary::
+    :toctree: ../generated/
+    get_method
+"""
+
+from pysteps.blending import linear_blending
+from pysteps.blending import steps
+
+_blending_methods = dict()
+_blending_methods["linear_blending"] = linear_blending.forecast
+_blending_methods["steps"] = steps.forecast
+
+
+def get_method(name):
+    """Return a callable function for computing nowcasts.
+    Description:
+    Return a callable function for computing deterministic or ensemble
+    precipitation nowcasts.
+    Implemented methods:
+    +-----------------+-------------------------------------------------------+
+    |     Name        |              Description                              |
+    +=================+=======================================================+
+    +-----------------+-------------------------------------------------------+
+    |  linear         | the linear blending of a nowcast method with other    |
+    |  blending       | data (e.g. NWP data).                                 |
+    +-----------------+-------------------------------------------------------+
+    |  steps          | the STEPS stochastic nowcasting blending method       |
+    |                 | described in :cite:`Seed2003`, :cite:`BPS2006` and    |
+    |                 | :cite:`SPN2013`. The blending weights approach        |
+    |                 | currently follows :cite:`BPS2006`.                    |
+    +-----------------+-------------------------------------------------------+
+    """
+    if isinstance(name, str):
+        name = name.lower()
+    else:
+        raise TypeError(
+            "Only strings supported for the method's names.\n"
+            + "Available names:"
+            + str(list(_blending_methods.keys()))
+        ) from None
+
+    try:
+        return _blending_methods[name]
+    except KeyError:
+        raise ValueError(
+            "Unknown blending method {}\n".format(name)
+            + "The available methods are:"
+            + str(list(_blending_methods.keys()))
+        ) from None
diff --git a/pysteps/blending/linear_blending.py b/pysteps/blending/linear_blending.py
new file mode 100644
index 000000000..5b8ea5a21
--- /dev/null
+++ b/pysteps/blending/linear_blending.py
@@ -0,0 +1,200 @@
+# -*- coding: utf-8 -*-
+"""
+pysteps.nowcasts.linear_blending
+================================
+
+Implementation of the linear blending between nowcast and NWP data.
+
+.. autosummary::
+    :toctree: ../generated/
+
+    forecast
+"""
+
+import numpy as np
+from pysteps import nowcasts
+from pysteps.utils import conversion
+
+# TODO: Make xarray ready for pysteps-v2
+
+
+def forecast(
+    precip,
+    precip_metadata,
+    velocity,
+    timesteps,
+    timestep,
+    nowcast_method,
+    precip_nwp=None,
+    precip_nwp_metadata=None,
+    start_blending=120,
+    end_blending=240,
+    fill_nwp=True,
+    nowcast_kwargs=dict(),
+):
+
+    """Generate a forecast by linearly blending nowcasts with NWP data
+
+    Parameters
+    ----------
+    precip: array_like
+      Array containing the input precipitation field(s) ordered by timestamp
+      from oldest to newest. The time steps between the inputs are assumed
+      to be regular.
+    precip_metadata: dict
+        Metadata dictionary containing (at least) the transform, unit and threshold
+        attributes as described in the documentation of :py:mod:`pysteps.io.importers`.
+    velocity; array_like
+      Array of shape (2, m, n) containing the x- and y-components of the advection
+      field. The velocities are assumed to represent one time step between the
+      inputs. All values are required to be finite.
+    timesteps: int
+      Number of time steps to forecast.
+    timestep: int or float
+      The time difference (in minutes) between consecutive forecast fields.
+    nowcast_method: {'anvil', 'eulerian', 'lagrangian', 'extrapolation', 'lagrangian_probability', 'sprog', 'steps', 'sseps'}
+      Name of the nowcast method used to forecast the precipitation.
+    precip_nwp: array_like or NoneType, optional
+      Array of shape (timesteps, m, n) in the case of no ensemble or
+      of shape (n_ens_members, timesteps, m, n) in the case of an ensemble
+      containing the NWP precipitation fields ordered by timestamp from oldest
+      to newest. The time steps between the inputs are assumed to be regular
+      (and identical to the time step between the nowcasts). If no NWP
+      data is given the value of precip_nwp is None and no blending will be performed.
+    precip_nwp_metadata: dict or NoneType, optional
+        NWP metadata dictionary containing (at least) the transform, unit and threshold
+        attributes as described in the documentation of :py:mod:`pysteps.io.importers`.
+    start_blending: int, optional
+      Time stamp (in minutes) after which the blending should start. Before this
+      only the nowcast data is used.
+    end_blending: int, optional
+      Time stamp (in minutes) after which the blending should end. Between
+      start_blending and end_blending the nowcasts and NWP data are linearly
+      merged with each other. After end_blending only the NWP data is used.
+    fill_nwp: bool, optional
+      Standard value is True. If True, the NWP data will be used to fill in the
+      no data mask of the nowcast.
+    nowcast_kwargs: dict, optional
+      Dictionary containing keyword arguments for the nowcast method.
+
+    Returns
+    -------
+    R_blended: ndarray
+      Array of shape (timesteps, m, n) in the case of no ensemble or
+      of shape (n_ens_members, timesteps, m, n) in the case of an ensemble
+      containing the precipation forecast generated by linearly blending
+      the nowcasts and the NWP data. n_ens_members equals the maximum no. of
+      ensemble members in either the nowcast or nwp model(s).
+    """
+
+    # Calculate the nowcasts
+    nowcast_method_func = nowcasts.get_method(nowcast_method)
+    precip_nowcast = nowcast_method_func(
+        precip,
+        velocity,
+        timesteps,
+        **nowcast_kwargs,
+    )
+
+    # Make sure that precip_nowcast and precip_nwp are in mm/h
+    precip_nowcast, _ = conversion.to_rainrate(precip_nowcast, metadata=precip_metadata)
+    precip_nwp, _ = conversion.to_rainrate(precip_nwp, metadata=precip_nwp_metadata)
+
+    # Check if NWP data is given as input
+    if precip_nwp is not None:
+
+        if len(precip_nowcast.shape) == 4:
+            n_ens_members_nowcast = precip_nowcast.shape[0]
+            if n_ens_members_nowcast == 1:
+                precip_nowcast = np.squeeze(precip_nowcast)
+        else:
+            n_ens_members_nowcast = 1
+
+        if len(precip_nwp.shape) == 4:
+            n_ens_members_nwp = precip_nwp.shape[0]
+            if n_ens_members_nwp == 1:
+                precip_nwp = np.squeeze(precip_nwp)
+        else:
+            n_ens_members_nwp = 1
+
+        # Now, repeat the nowcast ensemble members or the nwp models/members until
+        # it has the same amount of members as n_ens_members_max. For instance, if
+        # you have 10 ensemble nowcasts members and 3 NWP members, the output will
+        # be an ensemble of 10 members. Hence, the three NWP members are blended
+        # with the first three members of the nowcast (member one with member one,
+        # two with two, etc.), subsequently, the same NWP members are blended with
+        # the next three members (NWP member one with member 4, NWP member 2 with
+        # member 5, etc.), until 10 is reached.
+        n_ens_members_max = max(n_ens_members_nowcast, n_ens_members_nwp)
+        n_ens_members_min = min(n_ens_members_nowcast, n_ens_members_nwp)
+
+        if n_ens_members_min != n_ens_members_max:
+            if n_ens_members_nwp == 1:
+                precip_nwp = np.repeat(
+                    precip_nwp[np.newaxis, :, :], n_ens_members_max, axis=0
+                )
+            elif n_ens_members_nowcast == 1:
+                precip_nowcast = np.repeat(
+                    precip_nowcast[np.newaxis, :, :], n_ens_members_max, axis=0
+                )
+            else:
+                repeats = [
+                    (n_ens_members_max + i) // n_ens_members_min
+                    for i in range(n_ens_members_min)
+                ]
+
+                if n_ens_members_nwp == n_ens_members_min:
+                    precip_nwp = np.repeat(precip_nwp, repeats, axis=0)
+                elif n_ens_members_nowcast == n_ens_members_min:
+                    precip_nowcast = np.repeat(precip_nowcast, repeats, axis=0)
+
+        # Check if dimensions are correct
+        assert (
+            precip_nwp.shape == precip_nowcast.shape
+        ), "The dimensions of precip_nowcast and precip_nwp need to be identical: dimension of precip_nwp = {} and dimension of precip_nowcast = {}".format(
+            precip_nwp.shape, precip_nowcast.shape
+        )
+
+        # Initialise output
+        R_blended = np.zeros_like(precip_nowcast)
+
+        # Calculate the weights
+        for i in range(timesteps):
+            # Calculate what time we are at
+            t = (i + 1) * timestep
+
+            # Calculate the weight with a linear relation (weight_nwp at start_blending = 0.0)
+            # and (weight_nwp at end_blending = 1.0)
+            weight_nwp = (t - start_blending) / (end_blending - start_blending)
+
+            # Set weights at times before start_blending and after end_blending
+            if weight_nwp < 0.0:
+                weight_nwp = 0.0
+            elif weight_nwp > 1.0:
+                weight_nwp = 1.0
+
+            # Calculate weight_nowcast
+            weight_nowcast = 1.0 - weight_nwp
+
+            # Calculate output by combining precip_nwp and precip_nowcast,
+            # while distinguishing between ensemble and non-ensemble methods
+            if n_ens_members_max == 1:
+                R_blended[i, :, :] = (
+                    weight_nwp * precip_nwp[i, :, :]
+                    + weight_nowcast * precip_nowcast[i, :, :]
+                )
+            else:
+                R_blended[:, i, :, :] = (
+                    weight_nwp * precip_nwp[:, i, :, :]
+                    + weight_nowcast * precip_nowcast[:, i, :, :]
+                )
+
+            # Find where the NaN values are and replace them with NWP data
+            if fill_nwp:
+                nan_indices = np.isnan(R_blended)
+                R_blended[nan_indices] = precip_nwp[nan_indices]
+    else:
+        # If no NWP data is given, the blended field is simply equal to the nowcast field
+        R_blended = precip_nowcast
+
+    return R_blended
diff --git a/pysteps/blending/skill_scores.py b/pysteps/blending/skill_scores.py
new file mode 100644
index 000000000..1f02ba141
--- /dev/null
+++ b/pysteps/blending/skill_scores.py
@@ -0,0 +1,279 @@
+# -*- coding: utf-8 -*-
+"""
+pysteps.blending.skill_scores
+==============================
+
+Methods for computing skill scores, needed for the blending weights, of two-
+dimensional model fields with the latest observation field.
+
+.. autosummary::
+    :toctree: ../generated/
+
+    spatial_correlation
+    lt_dependent_cor_nwp
+    lt_dependent_cor_extrapolation
+    clim_regr_values
+"""
+
+import numpy as np
+from pysteps.blending import clim
+
+
+def spatial_correlation(obs, mod, domain_mask):
+    """Determine the spatial correlation between the cascade of the latest
+    available observed (radar) rainfall field and a time-synchronous cascade
+    derived from a model (generally NWP) field. Both fields are assumed to use
+    the same grid.
+
+
+    Parameters
+    ----------
+    obs : array-like
+        Array of shape [cascade_level, y, x] with per cascade_level the
+        normalized cascade of the observed (radar) rainfall field.
+    mod : array-like
+        Array of shape [cascade_level, y, x] with per cascade_level the
+        normalized cascade of the model field.
+    domain_mask : array-like
+        Boolean array of shape [y, x] indicating which cells fall outside the
+        radar domain.
+
+    Returns
+    -------
+    rho : array-like
+        Array of shape [n_cascade_levels] containing per cascade_level the
+        correlation between the normalized cascade of the observed (radar)
+        rainfall field and the normalized cascade of the model field.
+
+    References
+    ----------
+    :cite:`BPS2006`
+    :cite:`SPN2013`
+
+    """
+    rho = []
+    # Fill rho per cascade level, so loop through the cascade levels
+    for cascade_level in range(0, obs.shape[0]):
+        # Only calculate the skill for the pixels within the radar domain
+        # (as that is where there are observations)
+        obs_cascade_level = obs[cascade_level, :, :]
+        mod_cascade_level = mod[cascade_level, :, :]
+        obs_cascade_level[domain_mask] = np.nan
+        mod_cascade_level[domain_mask] = np.nan
+
+        # Flatten both arrays
+        obs_1d = obs_cascade_level.flatten()
+        mod_1d = mod_cascade_level.flatten()
+        # Calculate the correlation between the two
+        cov = np.nansum(
+            (mod_1d - np.nanmean(mod_1d)) * (obs_1d - np.nanmean(obs_1d))
+        )  # Without 1/n, as this cancels out (same for stdx and -y)
+        std_obs = np.sqrt(np.nansum((obs_1d - np.nanmean(obs_1d)) ** 2.0))
+        std_mod = np.sqrt(np.nansum((mod_1d - np.nanmean(mod_1d)) ** 2.0))
+        rho.append(cov / (std_mod * std_obs))
+
+    return rho
+
+
+def lt_dependent_cor_nwp(lt, correlations, outdir_path, n_model=0, skill_kwargs=dict()):
+    """Determine the correlation of a model field for lead time lt and
+    cascade k, by assuming that the correlation determined at t=0 regresses
+    towards the climatological values.
+
+
+    Parameters
+    ----------
+    lt : int
+        The lead time of the forecast in minutes.
+    correlations : array-like
+        Array of shape [n_cascade_levels] containing per cascade_level the
+        correlation between the normalized cascade of the observed (radar)
+        rainfall field and the normalized cascade of the model field.
+    outdir_path: string
+      Path to folder where the historical skill are stored. Defaults to
+      path_workdir from rcparams.
+    n_model: int, optional
+        The index number of the (NWP) model when the climatological skill of
+        multiple (NWP) models is stored. For calculations with one model, or
+        when n_model is not provided, n_model = 0.
+    skill_kwargs : dict, optional
+        Dictionary containing e.g. the outdir_path, nmodels and window_length
+        parameters.
+
+    Returns
+    -------
+    rho : array-like
+        Array of shape [n_cascade_levels] containing, for lead time lt, per
+        cascade_level the correlation between the normalized cascade of the
+        observed (radar) rainfall field and the normalized cascade of the
+        model field.
+
+    References
+    ----------
+    :cite:`BPS2004`
+    :cite:`BPS2006`
+    """
+    # Obtain the climatological values towards which the correlations will
+    # regress
+    clim_cor_values, regr_pars = clim_regr_values(
+        n_cascade_levels=len(correlations),
+        outdir_path=outdir_path,
+        n_model=n_model,
+        skill_kwargs=skill_kwargs,
+    )
+    # Determine the speed of the regression (eq. 24 in BPS2004)
+    qm = np.exp(-lt / regr_pars[0, :]) * (2 - np.exp(-lt / regr_pars[1, :]))
+    # Determine the correlation for lead time lt
+    rho = qm * correlations + (1 - qm) * clim_cor_values
+
+    return rho
+
+
+def lt_dependent_cor_extrapolation(PHI, correlations=None, correlations_prev=None):
+    """Determine the correlation of the extrapolation (nowcast) component for
+    lead time lt and cascade k, by assuming that the correlation determined at
+    t=0 regresses towards the climatological values.
+
+
+    Parameters
+    ----------
+    PHI : array-like
+        Array of shape [n_cascade_levels, ar_order + 1] containing per
+        cascade level the autoregression parameters.
+    correlations : array-like, optional
+        Array of shape [n_cascade_levels] containing per cascade_level the
+        latest available correlation from the extrapolation component that can
+        be found from the AR-2 model.
+    correlations_prev : array-like, optional
+        Similar to correlations, but from the timestep before that.
+
+    Returns
+    -------
+    rho : array-like
+        Array of shape [n_cascade_levels] containing, for lead time lt, per
+        cascade_level the correlation of the extrapolation component.
+
+    References
+    ----------
+    :cite:`BPS2004`
+    :cite:`BPS2006`
+
+    """
+    # Check if correlations_prev exists, if not, we set it to 1.0
+    if correlations_prev is None:
+        correlations_prev = np.repeat(1.0, PHI.shape[0])
+    # Same for correlations at first time step, we set it to
+    # phi1 / (1 - phi2), see BPS2004
+    if correlations is None:
+        correlations = PHI[:, 0] / (1.0 - PHI[:, 1])
+
+    # Calculate the correlation for lead time lt
+    rho = PHI[:, 0] * correlations + PHI[:, 1] * correlations_prev
+
+    # Finally, set the current correlations array as the previous one for the
+    # next time step
+    rho_prev = correlations
+
+    return rho, rho_prev
+
+
+def clim_regr_values(n_cascade_levels, outdir_path, n_model=0, skill_kwargs=dict()):
+    """Obtains the climatological correlation values and regression parameters
+    from a file called NWP_weights_window.bin in the outdir_path. If this file
+    is not present yet, the values from :cite:`BPS2004` are used.
+
+
+    Parameters
+    ----------
+    n_cascade_levels : int
+        The number of cascade levels to use.
+    outdir_path: string
+      Path to folder where the historical skill are stored. Defaults to
+      path_workdir from rcparams.
+    n_model: int, optional
+        The index number of the (NWP) model when the climatological skill of
+        multiple (NWP) models is stored. For calculations with one model, or
+        when n_model is not provided, n_model = 0.
+    skill_kwargs : dict, optional
+        Dictionary containing e.g. the outdir_path, nmodels and window_length
+        parameters.
+
+    Returns
+    -------
+    clim_cor_values : array-like
+        Array of shape [n_cascade_levels] containing the
+        climatological values of the lag 1 and lag 2 auto-correlation
+        coefficients, obtained by calling a method implemented in
+        pysteps.blending.skill_scores.get_clim_skill_scores.
+    regr_pars : array-like
+        Array of shape [2, n_cascade_levels] containing the regression
+        parameters. These are fixed values that should be hard-coded in this
+        function. Unless changed by the user, the standard values from
+        `BPS2004` are used.
+
+    Notes
+    -----
+    The literature climatological values assume 8 cascade levels. In case
+    less or more cascade levels are used, the script will handle this by taking
+    the first n values or extending the array with a small value. This is not
+    ideal, but will be fixed once the clim_regr_file is made. Hence, this
+    requires a warm-up period of the model.
+    In addition, the regression parameter values (eq. 24 in BPS2004) are hard-
+    coded and can only be optimized by the user after (re)fitting of the
+    equation.
+
+    """
+
+    # First, obtain climatological skill values
+    try:
+        clim_cor_values = clim.calc_clim_skill(
+            outdir_path=outdir_path, n_cascade_levels=n_cascade_levels, **skill_kwargs
+        )
+    except FileNotFoundError:
+        # The climatological skill values file does not exist yet, so we'll
+        # use the default values from BPS2004.
+        clim_cor_values = np.array(
+            [[0.848, 0.537, 0.237, 0.065, 0.020, 0.0044, 0.0052, 0.0040]]
+        )
+
+    clim_cor_values = clim_cor_values[n_model, :]
+
+    # Check if clim_cor_values has only one model, otherwise it has
+    # returned the skill values for multiple models
+    if clim_cor_values.ndim != 1:
+        raise IndexError(
+            "The climatological cor. values of multiple models were returned, but only one model should be specified. Make sure to pass the argument nmodels in the function"
+        )
+
+    # Also check whether the number of cascade_levels is correct
+    if clim_cor_values.shape[0] > n_cascade_levels:
+        clim_cor_values = clim_cor_values[0:n_cascade_levels]
+    elif clim_cor_values.shape[0] < n_cascade_levels:
+        # Get the number of cascade levels that is missing
+        n_extra_lev = n_cascade_levels - clim_cor_values.shape[0]
+        # Append the array with correlation values of 10e-4
+        clim_cor_values = np.append(clim_cor_values, np.repeat(1e-4, n_extra_lev))
+
+    # Get the regression parameters (L in eq. 24 in BPS2004) - Hard coded,
+    # change to own values when present.
+    regr_pars = np.array(
+        [
+            [130.0, 165.0, 120.0, 55.0, 50.0, 15.0, 15.0, 10.0],
+            [155.0, 220.0, 200.0, 75.0, 10e4, 10e4, 10e4, 10e4],
+        ]
+    )
+
+    # Check whether the number of cascade_levels is correct
+    if regr_pars.shape[1] > n_cascade_levels:
+        regr_pars = regr_pars[:, 0:n_cascade_levels]
+    elif regr_pars.shape[1] < n_cascade_levels:
+        # Get the number of cascade levels that is missing
+        n_extra_lev = n_cascade_levels - regr_pars.shape[1]
+        # Append the array with correlation values of 10e-4
+        regr_pars = np.append(
+            regr_pars,
+            [np.repeat(10.0, n_extra_lev), np.repeat(10e4, n_extra_lev)],
+            axis=1,
+        )
+
+    return clim_cor_values, regr_pars
diff --git a/pysteps/blending/steps.py b/pysteps/blending/steps.py
new file mode 100644
index 000000000..99a5c66cc
--- /dev/null
+++ b/pysteps/blending/steps.py
@@ -0,0 +1,2006 @@
+# -*- coding: utf-8 -*-
+"""
+pysteps.blending.steps
+======================
+
+Implementation of the STEPS stochastic blending method as described in
+:cite:`BPS2004`, :cite:`BPS2006` and :cite:`SPN2013`. The STEPS blending method
+consists of the following main steps:
+    1. Set the radar rainfall fields in a Lagrangian space.
+    2. Initialize the noise method.
+    3. Perform the cascade decomposition for the input radar rainfall fields. 
+    The method assumes that the cascade decomposition of the NWP model fields is
+    already done prior to calling the function, as the NWP model fields are 
+    generally not updated with the same frequency (which is more efficient). A 
+    method to decompose and store the NWP model fields whenever a new NWP model
+    field is present, is present in pysteps.blending.utils.decompose_NWP.
+    4. Estimate AR parameters for the extrapolation nowcast and noise cascade.
+    5. Before starting the forecast loop, determine which NWP models will be 
+    combined with which nowcast ensemble members. The number of output ensemble
+    members equals the maximum number of (ensemble) members in the input, which 
+    can be either the defined number of (nowcast) ensemble members or the number
+    of NWP models/members.
+    6. Initialize all the random generators.
+    7. Calculate the initial skill of the NWP model forecasts at t=0.
+    8. Start the forecasting loop
+        8.1 Determine the skill and weights of the forecasting components (
+            extrapolation, NWP and noise) for that lead time. 
+        8.2 Regress the extrapolation and noise cascades separately to the 
+            subsequent time step.
+        8.3 Extrapolate the extrapolation and noise cascades to the current time
+            step.
+        8.4 Blend the cascades.
+        8.5 Recompose the cascade to a rainfall field. 
+        8.6 Post-processing steps (masking and probability matching, which are
+                                   different from the original blended STEPS
+                                   implementation).
+
+
+.. autosummary::
+    :toctree: ../generated/
+
+    forecast
+    calculate_ratios
+    calculate_weights_bps
+    calculate_weights_spn
+    blend_means_sigmas
+    _check_inputs
+    _compute_incremental_mask
+    _transform_to_lagrangian
+    _init_noise
+    _compute_cascade_decomposition_radar
+    _compute_cascade_decomposition_nwp
+    _estimate_ar_parameters_radar
+    _find_nwp_combination
+    _init_random_generators
+    _prepare_forecast_loop
+    _compute_initial_nwp_skill
+
+"""
+
+import time
+
+import numpy as np
+import scipy.ndimage
+
+from pysteps import cascade
+from pysteps import extrapolation
+from pysteps import noise
+from pysteps import utils
+from pysteps.nowcasts import utils as nowcast_utils
+from pysteps.postprocessing import probmatching
+from pysteps.timeseries import autoregression, correlation
+from pysteps import blending
+
+try:
+    import dask
+
+    DASK_IMPORTED = True
+except ImportError:
+    DASK_IMPORTED = False
+
+
+def forecast(
+    R,
+    R_d_models,
+    V,
+    V_models,
+    timesteps,
+    timestep,
+    issuetime,
+    n_ens_members=24,
+    n_cascade_levels=8,
+    blend_nwp_members=False,
+    R_thr=None,
+    kmperpixel=None,
+    extrap_method="semilagrangian",
+    decomp_method="fft",
+    bandpass_filter_method="gaussian",
+    noise_method="nonparametric",
+    noise_stddev_adj=None,
+    ar_order=2,
+    vel_pert_method="bps",
+    weights_method="bps",
+    conditional=False,
+    probmatching_method="cdf",
+    mask_method="incremental",
+    callback=None,
+    return_output=True,
+    seed=None,
+    num_workers=1,
+    fft_method="numpy",
+    domain="spatial",
+    outdir_path_skill="./tmp/",
+    extrap_kwargs=None,
+    filter_kwargs=None,
+    noise_kwargs=None,
+    vel_pert_kwargs=None,
+    clim_kwargs=None,
+    mask_kwargs=None,
+    measure_time=False,
+):
+    """Generate a blended nowcast ensemble by using the Short-Term Ensemble
+    Prediction System (STEPS) method.
+
+    Parameters
+    ----------
+    R: array-like
+      Array of shape (ar_order+1,m,n) containing the input precipitation fields
+      ordered by timestamp from oldest to newest. The time steps between the
+      inputs are assumed to be regular.
+    R_d_models: array-like
+        Array of shape (n_models,timesteps+1) containing, per timestep (t=0 to
+        lead time here) and per (NWP) model or model ensemble member, a
+        dictionary with a list of cascades obtained by calling a method
+        implemented in pysteps.cascade.decomposition. In case of one
+        (deterministic) model as input, add an extra dimension to make sure
+        R_models is five dimensional prior to calling this function.
+        It is also possible to supply the original (NWP) model forecasts containing
+        rainfall fields as an array of shape (n_models,timestep+1,m,n), which will
+        then be decomposed in this function. Note that for an operational application
+        or for testing with multiple model runs, it is recommended to decompose
+        the model forecasts outside of the pysteps.blending.steps function, as this
+        reduces calculation times. This is possible with pysteps.blending.utils.decompose_NWP,
+        pysteps.blending.utils.compute_store_nwp_motion and pysteps.blending.utils.load_NWP.
+    V: array-like
+      Array of shape (2,m,n) containing the x- and y-components of the advection
+      field. The velocities are assumed to represent one time step between the
+      inputs. All values are required to be finite.
+    V_models: array-like
+      Array of shape (n_models,timestep,2,m,n) containing the x- and y-components
+      of the advection field for the (NWP) model field per forecast lead time.
+      All values are required to be finite.
+    timesteps: int or list of floats
+      Number of time steps to forecast or a list of time steps for which the
+      forecasts are computed (relative to the input time step). The elements of
+      the list are required to be in ascending order.
+    timestep: float
+      Time step of the motion vectors (minutes). Required if vel_pert_method is
+      not None or mask_method is 'incremental'.
+    issuetime: datetime
+      Datetime object containing the date and time for which the forecast
+      is issued.
+    n_ens_members: int, optional
+      The number of ensemble members to generate.
+    n_cascade_levels: int, optional
+      The number of cascade levels to use. Default set to 8 due to default
+      climatological skill values on 8 levels.
+    blend_nwp_members: bool
+        Check if NWP models/members should be used individually, or if all of
+        them are blended together per nowcast ensemble member. Standard set to
+        false.
+    R_thr: float, optional
+      Specifies the threshold value for minimum observable precipitation
+      intensity. Required if mask_method is not None or conditional is True.
+    kmperpixel: float, optional
+      Spatial resolution of the input data (kilometers/pixel). Required if
+      vel_pert_method is not None or mask_method is 'incremental'.
+    extrap_method: str, optional
+      Name of the extrapolation method to use. See the documentation of
+      pysteps.extrapolation.interface.
+    decomp_method: {'fft'}, optional
+      Name of the cascade decomposition method to use. See the documentation
+      of pysteps.cascade.interface.
+    bandpass_filter_method: {'gaussian', 'uniform'}, optional
+      Name of the bandpass filter method to use with the cascade decomposition.
+      See the documentation of pysteps.cascade.interface.
+    noise_method: {'parametric','nonparametric','ssft','nested',None}, optional
+      Name of the noise generator to use for perturbating the precipitation
+      field. See the documentation of pysteps.noise.interface. If set to None,
+      no noise is generated.
+    noise_stddev_adj: {'auto','fixed',None}, optional
+      Optional adjustment for the standard deviations of the noise fields added
+      to each cascade level. This is done to compensate incorrect std. dev.
+      estimates of casace levels due to presence of no-rain areas. 'auto'=use
+      the method implemented in pysteps.noise.utils.compute_noise_stddev_adjs.
+      'fixed'= use the formula given in :cite:`BPS2006` (eq. 6), None=disable
+      noise std. dev adjustment.
+    ar_order: int, optional
+      The order of the autoregressive model to use. Must be >= 1.
+    vel_pert_method: {'bps',None}, optional
+      Name of the noise generator to use for perturbing the advection field. See
+      the documentation of pysteps.noise.interface. If set to None, the advection
+      field is not perturbed.
+    weights_method: {'bps','spn'}, optional
+        The calculation method of the blending weights. Options are the method
+        by :cite:`BPS2006` and the covariance-based method by :cite:`SPN2013`.
+        Defaults to bps.
+    conditional: bool, optional
+      If set to True, compute the statistics of the precipitation field
+      conditionally by excluding pixels where the values are below the threshold
+      R_thr.
+    mask_method: {'obs','incremental',None}, optional
+      The method to use for masking no precipitation areas in the forecast field.
+      The masked pixels are set to the minimum value of the observations.
+      'obs' = apply R_thr to the most recently observed precipitation intensity
+      field, 'incremental' = iteratively buffer the mask with a certain rate
+      (currently it is 1 km/min), None=no masking.
+    probmatching_method: {'cdf','mean',None}, optional
+      Method for matching the statistics of the forecast field with those of
+      the most recently observed one. 'cdf'=map the forecast CDF to the observed
+      one, 'mean'=adjust only the conditional mean value of the forecast field
+      in precipitation areas, None=no matching applied. Using 'mean' requires
+      that mask_method is not None.
+    callback: function, optional
+      Optional function that is called after computation of each time step of
+      the nowcast. The function takes one argument: a three-dimensional array
+      of shape (n_ens_members,h,w), where h and w are the height and width
+      of the input field R, respectively. This can be used, for instance,
+      writing the outputs into files.
+    return_output: bool, optional
+      Set to False to disable returning the outputs as numpy arrays. This can
+      save memory if the intermediate results are written to output files using
+      the callback function.
+    seed: int, optional
+      Optional seed number for the random generators.
+    num_workers: int, optional
+      The number of workers to use for parallel computation. Applicable if dask
+      is enabled or pyFFTW is used for computing the FFT. When num_workers>1, it
+      is advisable to disable OpenMP by setting the environment variable
+      OMP_NUM_THREADS to 1. This avoids slowdown caused by too many simultaneous
+      threads.
+    fft_method: str, optional
+      A string defining the FFT method to use (see utils.fft.get_method).
+      Defaults to 'numpy' for compatibility reasons. If pyFFTW is installed,
+      the recommended method is 'pyfftw'.
+    domain: {"spatial", "spectral"}
+      If "spatial", all computations are done in the spatial domain (the
+      classical STEPS model). If "spectral", the AR(2) models and stochastic
+      perturbations are applied directly in the spectral domain to reduce
+      memory footprint and improve performance :cite:`PCH2019b`.
+    outdir_path_skill: string, optional
+      Path to folder where the historical skill are stored. Defaults to
+      path_workdir from rcparams. If no path is given, './tmp' will be used.
+    extrap_kwargs: dict, optional
+      Optional dictionary containing keyword arguments for the extrapolation
+      method. See the documentation of pysteps.extrapolation.
+    filter_kwargs: dict, optional
+      Optional dictionary containing keyword arguments for the filter method.
+      See the documentation of pysteps.cascade.bandpass_filters.py.
+    noise_kwargs: dict, optional
+      Optional dictionary containing keyword arguments for the initializer of
+      the noise generator. See the documentation of pysteps.noise.fftgenerators.
+    vel_pert_kwargs: dict, optional
+      Optional dictionary containing keyword arguments 'p_par' and 'p_perp' for
+      the initializer of the velocity perturbator. The choice of the optimal
+      parameters depends on the domain and the used optical flow method.
+
+      Default parameters from :cite:`BPS2006`:
+      p_par  = [10.88, 0.23, -7.68]
+      p_perp = [5.76, 0.31, -2.72]
+
+      Parameters fitted to the data (optical flow/domain):
+
+      darts/fmi:
+      p_par  = [13.71259667, 0.15658963, -16.24368207]
+      p_perp = [8.26550355, 0.17820458, -9.54107834]
+
+      darts/mch:
+      p_par  = [24.27562298, 0.11297186, -27.30087471]
+      p_perp = [-7.80797846e+01, -3.38641048e-02, 7.56715304e+01]
+
+      darts/fmi+mch:
+      p_par  = [16.55447057, 0.14160448, -19.24613059]
+      p_perp = [14.75343395, 0.11785398, -16.26151612]
+
+      lucaskanade/fmi:
+      p_par  = [2.20837526, 0.33887032, -2.48995355]
+      p_perp = [2.21722634, 0.32359621, -2.57402761]
+
+      lucaskanade/mch:
+      p_par  = [2.56338484, 0.3330941, -2.99714349]
+      p_perp = [1.31204508, 0.3578426, -1.02499891]
+
+      lucaskanade/fmi+mch:
+      p_par  = [2.31970635, 0.33734287, -2.64972861]
+      p_perp = [1.90769947, 0.33446594, -2.06603662]
+
+      vet/fmi:
+      p_par  = [0.25337388, 0.67542291, 11.04895538]
+      p_perp = [0.02432118, 0.99613295, 7.40146505]
+
+      vet/mch:
+      p_par  = [0.5075159, 0.53895212, 7.90331791]
+      p_perp = [0.68025501, 0.41761289, 4.73793581]
+
+      vet/fmi+mch:
+      p_par  = [0.29495222, 0.62429207, 8.6804131 ]
+      p_perp = [0.23127377, 0.59010281, 5.98180004]
+
+      fmi=Finland, mch=Switzerland, fmi+mch=both pooled into the same data set
+
+      The above parameters have been fitten by using run_vel_pert_analysis.py
+      and fit_vel_pert_params.py located in the scripts directory.
+
+      See pysteps.noise.motion for additional documentation.
+    clim_kwargs: dict, optional
+        Optional dictionary containing keyword arguments for the climatological
+        skill file. Arguments can consist of: 'outdir_path', 'n_models'
+        (the number of NWP models) and 'window_length' (the minimum number of
+        days the clim file should have, otherwise the default is used).
+    mask_kwargs: dict
+      Optional dictionary containing mask keyword arguments 'mask_f' and
+      'mask_rim', the factor defining the the mask increment and the rim size,
+      respectively.
+      The mask increment is defined as mask_f*timestep/kmperpixel.
+    measure_time: bool
+      If set to True, measure, print and return the computation time.
+
+    Returns
+    -------
+    out: ndarray
+      If return_output is True, a four-dimensional array of shape
+      (n_ens_members,num_timesteps,m,n) containing a time series of forecast
+      precipitation fields for each ensemble member. Otherwise, a None value
+      is returned. The time series starts from t0+timestep, where timestep is
+      taken from the input precipitation fields R. If measure_time is True, the
+      return value is a three-element tuple containing the nowcast array, the
+      initialization time of the nowcast generator and the time used in the
+      main loop (seconds).
+
+    See also
+    --------
+    pysteps.extrapolation.interface, pysteps.cascade.interface,
+    pysteps.noise.interface, pysteps.noise.utils.compute_noise_stddev_adjs
+
+    References
+    ----------
+    :cite:`Seed2003`, :cite:`BPS2004`, :cite:`BPS2006`, :cite:`SPN2013`, :cite:`PCH2019b`
+
+    Notes
+    -----
+    1. The blending currently does not blend the beta-parameters in the parametric
+    noise method. It is recommended to use the non-parameteric noise method.
+
+    2. If blend_nwp_members is True, the BPS2006 method for the weights is
+    suboptimal. It is recommended to use the SPN2013 method instead.
+    """
+
+    # 0.1 Start with some checks
+    _check_inputs(R, R_d_models, V, V_models, timesteps, ar_order)
+
+    if extrap_kwargs is None:
+        extrap_kwargs = dict()
+
+    if filter_kwargs is None:
+        filter_kwargs = dict()
+
+    if noise_kwargs is None:
+        noise_kwargs = dict()
+
+    if vel_pert_kwargs is None:
+        vel_pert_kwargs = dict()
+
+    if clim_kwargs is None:
+        clim_kwargs = dict()
+
+    if mask_kwargs is None:
+        mask_kwargs = dict()
+
+    if np.any(~np.isfinite(V)):
+        raise ValueError("V contains non-finite values")
+
+    if mask_method not in ["obs", "incremental", None]:
+        raise ValueError(
+            "unknown mask method %s: must be 'obs', 'incremental' or None" % mask_method
+        )
+
+    if conditional and R_thr is None:
+        raise ValueError("conditional=True but R_thr is not set")
+
+    if mask_method is not None and R_thr is None:
+        raise ValueError("mask_method!=None but R_thr=None")
+
+    if noise_stddev_adj not in ["auto", "fixed", None]:
+        raise ValueError(
+            "unknown noise_std_dev_adj method %s: must be 'auto', 'fixed', or None"
+            % noise_stddev_adj
+        )
+
+    if kmperpixel is None:
+        if vel_pert_method is not None:
+            raise ValueError("vel_pert_method is set but kmperpixel=None")
+        if mask_method == "incremental":
+            raise ValueError("mask_method='incremental' but kmperpixel=None")
+
+    if timestep is None:
+        if vel_pert_method is not None:
+            raise ValueError("vel_pert_method is set but timestep=None")
+        if mask_method == "incremental":
+            raise ValueError("mask_method='incremental' but timestep=None")
+
+    # 0.2 Log some settings
+    print("Computing STEPS blended nowcast:")
+    print("------------------------")
+    print("")
+
+    print("Inputs:")
+    print("-------")
+    print("Forecast issue time: %s" % issuetime)
+    print("input dimensions: %dx%d" % (R.shape[1], R.shape[2]))
+    if kmperpixel is not None:
+        print("km/pixel:         %g" % kmperpixel)
+    if timestep is not None:
+        print("time step:        %d minutes" % timestep)
+
+    print("NWP and blending inputs:")
+    print("--------")
+    print("Number of (NWP) models: %d" % R_d_models.shape[0])
+    print("Blend (NWP) model members: %s" % blend_nwp_members)
+    print("Decompose (NWP) models: %s" % ("yes" if R_d_models.ndim == 4 else "no"))
+
+    print("Methods:")
+    print("--------")
+    print("extrapolation:               %s" % extrap_method)
+    print("bandpass filter:             %s" % bandpass_filter_method)
+    print("decomposition:               %s" % decomp_method)
+    print("noise generator:             %s" % noise_method)
+    print("noise adjustment:            %s" % ("yes" if noise_stddev_adj else "no"))
+    print("velocity perturbator:        %s" % vel_pert_method)
+    print("blending weights method:     %s" % weights_method)
+    print("conditional statistics:      %s" % ("yes" if conditional else "no"))
+    print("precip. mask method:         %s" % mask_method)
+    print("probability matching:        %s" % probmatching_method)
+    print("FFT method:                  %s" % fft_method)
+    print("domain:                      %s" % domain)
+    print("")
+
+    print("Parameters:")
+    print("-----------")
+    if isinstance(timesteps, int):
+        print("number of time steps:     %d" % timesteps)
+    else:
+        print("time steps:               %s" % timesteps)
+    print("ensemble size:            %d" % n_ens_members)
+    print("parallel threads:         %d" % num_workers)
+    print("number of cascade levels: %d" % n_cascade_levels)
+    print("order of the AR(p) model: %d" % ar_order)
+    if vel_pert_method == "bps":
+        vp_par = vel_pert_kwargs.get("p_par", noise.motion.get_default_params_bps_par())
+        vp_perp = vel_pert_kwargs.get(
+            "p_perp", noise.motion.get_default_params_bps_perp()
+        )
+        print(
+            "velocity perturbations, parallel:      %g,%g,%g"
+            % (vp_par[0], vp_par[1], vp_par[2])
+        )
+        print(
+            "velocity perturbations, perpendicular: %g,%g,%g"
+            % (vp_perp[0], vp_perp[1], vp_perp[2])
+        )
+    else:
+        vp_par, vp_perp = None, None
+
+    if conditional or mask_method is not None:
+        print("precip. intensity threshold: %g" % R_thr)
+
+    # 0.3 Get the methods that will be used
+    num_ensemble_workers = n_ens_members if num_workers > n_ens_members else num_workers
+
+    if measure_time:
+        starttime_init = time.time()
+
+    fft = utils.get_method(fft_method, shape=R.shape[1:], n_threads=num_workers)
+
+    M, N = R.shape[1:]
+
+    # initialize the band-pass filter
+    filter_method = cascade.get_method(bandpass_filter_method)
+    filter = filter_method((M, N), n_cascade_levels, **filter_kwargs)
+
+    decompositor, recompositor = cascade.get_method(decomp_method)
+
+    extrapolator = extrapolation.get_method(extrap_method)
+
+    x_values, y_values = np.meshgrid(np.arange(R.shape[2]), np.arange(R.shape[1]))
+
+    xy_coords = np.stack([x_values, y_values])
+
+    R = R[-(ar_order + 1) :, :, :].copy()
+
+    # determine the domain mask from non-finite values
+    domain_mask = np.logical_or.reduce(
+        [~np.isfinite(R[i, :]) for i in range(R.shape[0])]
+    )
+
+    # determine the precipitation threshold mask
+    if conditional:
+        MASK_thr = np.logical_and.reduce(
+            [R[i, :, :] >= R_thr for i in range(R.shape[0])]
+        )
+    else:
+        MASK_thr = None
+
+    # 1. Start with the radar rainfall fields. We want the fields in a
+    # Lagrangian space
+    R = _transform_to_lagrangian(
+        R, V, ar_order, xy_coords, extrapolator, extrap_kwargs, num_workers
+    )
+
+    # 2. Initialize the noise method
+    pp, generate_noise, noise_std_coeffs = _init_noise(
+        R,
+        R_thr,
+        n_cascade_levels,
+        filter,
+        decompositor,
+        fft,
+        noise_method,
+        noise_kwargs,
+        noise_stddev_adj,
+        measure_time,
+        num_workers,
+    )
+
+    # 3. Perform the cascade decomposition for the input precip fields and
+    # and, if necessary, for the (NWP) model fields
+
+    # 3.1 Compute the cascade decompositions of the input precipitation fields
+    (
+        R_c,
+        R_d,
+        mu_extrapolation,
+        sigma_extrapolation,
+    ) = _compute_cascade_decomposition_radar(
+        R,
+        ar_order,
+        n_cascade_levels,
+        n_ens_members,
+        MASK_thr,
+        domain,
+        filter,
+        decompositor,
+        fft,
+    )
+
+    # 3.2 If necessary, decompose (NWP) model forecasts and stack cascades
+    R_models, mu_models, sigma_models, R_models_pm = _compute_cascade_decomposition_nwp(
+        R_d_models, filter, decompositor, recompositor, fft, domain
+    )
+
+    # 4. Estimate AR parameters for the radar rainfall field
+    PHI = _estimate_ar_parameters_radar(R_c, ar_order, n_cascade_levels, MASK_thr)
+
+    # 5. Before calling the worker for the forecast loop, determine which (NWP)
+    # models will be combined with which nowcast ensemble members. With the
+    # way it is implemented at this moment: n_ens_members of the output equals
+    # the maximum number of (ensemble) members in the input (either the nowcasts or NWP).
+    (
+        R_c,
+        R_models,
+        R_models_pm,
+        V_models,
+        mu_models,
+        sigma_models,
+        n_ens_members,
+        n_model_indices,
+    ) = _find_nwp_combination(
+        R_c,
+        R_models,
+        R_models_pm,
+        V_models,
+        mu_models,
+        sigma_models,
+        n_ens_members,
+        ar_order,
+        n_cascade_levels,
+        blend_nwp_members,
+    )
+
+    # Also initialize the cascade of temporally correlated noise, which has the
+    # same shape as R_c, but starts with value zero.
+    Yn_c = np.zeros(R_c.shape)
+
+    # 6. Initialize all the random generators and prepare for the forecast loop
+    randgen_prec, vps, generate_vel_noise = _init_random_generators(
+        V,
+        noise_method,
+        vel_pert_method,
+        vp_par,
+        vp_perp,
+        seed,
+        n_ens_members,
+        kmperpixel,
+        timestep,
+    )
+    D, D_Yn, D_pb, R_f, R_m, mask_rim, struct, fft_objs = _prepare_forecast_loop(
+        R_c,
+        noise_method,
+        fft_method,
+        n_cascade_levels,
+        n_ens_members,
+        mask_method,
+        mask_kwargs,
+        timestep,
+        kmperpixel,
+    )
+
+    R = R[-1, :, :]
+
+    # 7. Calculate the initial skill of the (NWP) model forecasts at t=0
+    rho_nwp_models = _compute_initial_nwp_skill(
+        R_c, R_models, domain_mask, issuetime, outdir_path_skill, clim_kwargs
+    )
+
+    # Also initizalize the current and previous extrapolation forecast scale
+    # for the nowcasting component
+    rho_extr_prev = np.repeat(1.0, PHI.shape[0])
+    rho_extr = PHI[:, 0] / (1.0 - PHI[:, 1])  # phi1 / (1 - phi2), see BPS2004
+
+    if measure_time:
+        init_time = time.time() - starttime_init
+
+    ###
+    # 8. Start the forecasting loop
+    ###
+    print("Starting blended nowcast computation.")
+
+    if measure_time:
+        starttime_mainloop = time.time()
+
+    if isinstance(timesteps, int):
+        timesteps = range(timesteps + 1)
+        timestep_type = "int"
+    else:
+        original_timesteps = [0] + list(timesteps)
+        timesteps = nowcast_utils.binned_timesteps(original_timesteps)
+        timestep_type = "list"
+
+    extrap_kwargs["return_displacement"] = True
+    R_f_prev = R_c
+    Yn_prev = Yn_c
+    t_prev = [0.0 for j in range(n_ens_members)]
+    t_total = [0.0 for j in range(n_ens_members)]
+
+    # iterate each time step
+    for t, subtimestep_idx in enumerate(timesteps):
+        if timestep_type == "list":
+            subtimesteps = [original_timesteps[t_] for t_ in subtimestep_idx]
+        else:
+            subtimesteps = [t]
+
+        if (timestep_type == "list" and subtimesteps) or (
+            timestep_type == "int" and t > 0
+        ):
+            is_nowcast_time_step = True
+        else:
+            is_nowcast_time_step = False
+
+        if is_nowcast_time_step:
+            print(
+                "Computing nowcast for time step %d... " % t,
+                end="",
+                flush=True,
+            )
+
+        if measure_time:
+            starttime = time.time()
+
+        # 8.1.1 Determine the skill of the components for lead time (t0 + t)
+        # First for the extrapolation component. Only calculate it when t > 0.
+        if t > 0:
+            (
+                rho_extr,
+                rho_extr_prev,
+            ) = blending.skill_scores.lt_dependent_cor_extrapolation(
+                PHI=PHI, correlations=rho_extr, correlations_prev=rho_extr_prev
+            )
+
+        # the nowcast iteration for each ensemble member
+        def worker(j):
+            # 8.1.2 Determine the skill of the nwp components for lead time (t0 + t)
+            # Then for the model components
+            if blend_nwp_members:
+                rho_nwp_fc = [
+                    blending.skill_scores.lt_dependent_cor_nwp(
+                        lt=(t * int(timestep)),
+                        correlations=rho_nwp_models[n_model],
+                        outdir_path=outdir_path_skill,
+                        n_model=n_model,
+                        skill_kwargs=clim_kwargs,
+                    )
+                    for n_model in range(rho_nwp_models.shape[0])
+                ]
+                rho_nwp_fc = np.stack(rho_nwp_fc)
+                # Concatenate rho_extr and rho_nwp
+                rho_fc = np.concatenate((rho_extr[None, :], rho_nwp_fc), axis=0)
+            else:
+                rho_nwp_fc = blending.skill_scores.lt_dependent_cor_nwp(
+                    lt=(t * int(timestep)),
+                    correlations=rho_nwp_models[j],
+                    outdir_path=outdir_path_skill,
+                    n_model=n_model_indices[j],
+                    skill_kwargs=clim_kwargs,
+                )
+                # Concatenate rho_extr and rho_nwp
+                rho_fc = np.concatenate(
+                    (rho_extr[None, :], rho_nwp_fc[None, :]), axis=0
+                )
+
+            # 8.2 Determine the weights per component
+
+            # Weights following the bps method. These are needed for the velocity
+            # weights prior to the advection step. If weights method spn is
+            # selected, weights will be overwritten with those weights prior to
+            # blending step.
+            # weight = [(extr_field, n_model_fields, noise), n_cascade_levels, ...]
+            weights = calculate_weights_bps(rho_fc)
+
+            # The model only weights
+            if weights_method == "bps":
+                # Determine the weights of the components without the extrapolation
+                # cascade, in case this is no data or outside the mask.
+                weights_model_only = calculate_weights_bps(rho_fc[1:, :])
+            elif weights_method == "spn":
+                # Only the weights of the components without the extrapolation
+                # cascade will be determined here. The full set of weights are
+                # determined after the extrapolation step in this method.
+                if blend_nwp_members and R_models.shape[0] > 1:
+                    weights_model_only = np.zeros(
+                        (R_models.shape[0] + 1, n_cascade_levels)
+                    )
+                    for i in range(n_cascade_levels):
+                        # Determine the normalized covariance matrix (containing)
+                        # the cross-correlations between the models
+                        cov = np.corrcoef(
+                            np.stack(
+                                [
+                                    R_models[n_model, t, i, :, :].flatten()
+                                    for n_model in range(R_models.shape[0])
+                                ]
+                            )
+                        )
+                        # Determine the weights for this cascade level
+                        weights_model_only[:, i] = calculate_weights_spn(
+                            correlations=rho_fc[1:, i], cov=cov
+                        )
+                else:
+                    # Same as correlation and noise is 1 - correlation
+                    weights_model_only = calculate_weights_bps(rho_fc[1:, :])
+            else:
+                raise ValueError(
+                    "Unknown weights method %s: must be 'bps' or 'spn'" % weights_method
+                )
+
+            # 8.3 Determine the noise cascade and regress this to the subsequent
+            # time step + regress the extrapolation component to the subsequent
+            # time step
+
+            # 8.3.1 Determine the epsilon, a cascade of temporally independent
+            # but spatially correlated noise
+            if noise_method is not None:
+                # generate noise field
+                EPS = generate_noise(
+                    pp, randstate=randgen_prec[j], fft_method=fft_objs[j], domain=domain
+                )
+
+                # decompose the noise field into a cascade
+                EPS = decompositor(
+                    EPS,
+                    filter,
+                    fft_method=fft_objs[j],
+                    input_domain=domain,
+                    output_domain=domain,
+                    compute_stats=True,
+                    normalize=True,
+                    compact_output=True,
+                )
+            else:
+                EPS = None
+
+            # 8.3.2 regress the extrapolation component to the subsequent time
+            # step
+            # iterate the AR(p) model for each cascade level
+            for i in range(n_cascade_levels):
+                # apply AR(p) process to extrapolation cascade level
+                if EPS is not None or vel_pert_method is not None:
+                    R_c[j][i] = autoregression.iterate_ar_model(R_c[j][i], PHI[i, :])
+
+                else:
+                    # use the deterministic AR(p) model computed above if
+                    # perturbations are disabled
+                    R_c[j][i] = R_m[i]
+
+            # 8.3.3 regress the noise component to the subsequent time step
+            # iterate the AR(p) model for each cascade level
+            for i in range(n_cascade_levels):
+                # normalize the noise cascade
+                if EPS is not None:
+                    EPS_ = EPS["cascade_levels"][i]
+                    EPS_ *= noise_std_coeffs[i]
+                else:
+                    EPS_ = None
+                # apply AR(p) process to noise cascade level
+                # (Returns zero noise if EPS is None)
+                Yn_c[j][i] = autoregression.iterate_ar_model(
+                    Yn_c[j][i], PHI[i, :], eps=EPS_
+                )
+
+            EPS = None
+            EPS_ = None
+
+            # 8.4 Perturb and blend the advection fields + advect the
+            # extrapolation and noise cascade to the current time step
+            # (or subtimesteps if non-integer time steps are given)
+
+            # Settings and initialize the output
+            extrap_kwargs_ = extrap_kwargs.copy()
+            extrap_kwargs_noise = extrap_kwargs.copy()
+            extrap_kwargs_pb = extrap_kwargs.copy()
+            V_pert = V
+            R_f_ep_out = []
+            Yn_ep_out = []
+            R_pm_ep = []
+
+            # Extrapolate per sub time step
+            for t_sub in subtimesteps:
+                if t_sub > 0:
+                    t_diff_prev_int = t_sub - int(t_sub)
+                    if t_diff_prev_int > 0.0:
+                        R_f_ip = [
+                            (1.0 - t_diff_prev_int) * R_f_prev[j][i][-1, :]
+                            + t_diff_prev_int * R_c[j][i][-1, :]
+                            for i in range(n_cascade_levels)
+                        ]
+                        Yn_ip = [
+                            (1.0 - t_diff_prev_int) * Yn_prev[j][i][-1, :]
+                            + t_diff_prev_int * Yn_c[j][i][-1, :]
+                            for i in range(n_cascade_levels)
+                        ]
+
+                    else:
+                        R_f_ip = [
+                            R_f_prev[j][i][-1, :] for i in range(n_cascade_levels)
+                        ]
+                        Yn_ip = [Yn_prev[j][i][-1, :] for i in range(n_cascade_levels)]
+
+                    R_f_ip = np.stack(R_f_ip)
+                    Yn_ip = np.stack(Yn_ip)
+
+                    t_diff_prev = t_sub - t_prev[j]
+                    t_total[j] += t_diff_prev
+
+                    # compute the perturbed motion field - include the NWP
+                    # velocities and the weights. Note that we only perturb
+                    # the extrapolation velocity field, as the NWP velocity
+                    # field is present per time step
+                    if vel_pert_method is not None:
+                        V_pert = V + generate_vel_noise(vps[j], t_total[j] * timestep)
+
+                    # Stack the perturbed extrapolation and the NWP velocities
+                    if blend_nwp_members:
+                        V_stack = np.concatenate(
+                            (V_pert[None, :, :, :], V_models[:, t, :, :, :]), axis=0
+                        )
+                    else:
+                        V_model_ = V_models[j, t, :, :, :]
+                        V_stack = np.concatenate(
+                            (V_pert[None, :, :, :], V_model_[None, :, :, :]), axis=0
+                        )
+                        V_model_ = None
+
+                    # Obtain a blended optical flow, using the weights of the
+                    # second cascade following eq. 24 in BPS2006
+                    V_blended = blending.utils.blend_optical_flows(
+                        flows=V_stack,
+                        weights=weights[
+                            :-1, 1
+                        ],  # [(extr_field, n_model_fields), cascade_level=2]
+                    )
+
+                    # Extrapolate both cascades to the next time step
+                    R_f_ep = np.zeros(R_f_ip.shape)
+                    Yn_ep = np.zeros(Yn_ip.shape)
+
+                    for i in range(n_cascade_levels):
+                        extrap_kwargs_["displacement_prev"] = D[j][i]
+                        extrap_kwargs_noise["displacement_prev"] = D_Yn[j][i]
+                        extrap_kwargs_noise["map_coordinates_mode"] = "wrap"
+
+                        # First, extrapolate the extrapolation component
+                        # Apply the domain mask to the extrapolation component
+                        R_f_ip[i][domain_mask] = np.NaN
+                        R_f_ep_, D[j][i] = extrapolator(
+                            R_f_ip[i],
+                            V_blended,
+                            [t_diff_prev],
+                            allow_nonfinite_values=True,
+                            **extrap_kwargs_,
+                        )
+                        R_f_ep[i] = R_f_ep_[0]
+
+                        # Then, extrapolate the noise component
+                        Yn_ep_, D_Yn[j][i] = extrapolator(
+                            Yn_ip[i],
+                            V_blended,
+                            [t_diff_prev],
+                            allow_nonfinite_values=True,
+                            **extrap_kwargs_noise,
+                        )
+                        Yn_ep[i] = Yn_ep_[0]
+
+                    # Append the results to the output lists
+                    R_f_ep_out.append(R_f_ep)
+                    Yn_ep_out.append(Yn_ep)
+                    R_f_ep_ = None
+                    Yn_ep_ = None
+
+                    # Finally, also extrapolate the initial radar rainfall
+                    # field. This will be blended with the rainfall field(s)
+                    # of the (NWP) model(s) for Lagrangian blended prob. matching
+                    # min_R = np.min(R)
+                    extrap_kwargs_pb["displacement_prev"] = D_pb[j]
+                    # Apply the domain mask to the extrapolation component
+                    R_ = R.copy()
+                    R_[domain_mask] = np.NaN
+                    R_pm_ep_, D_pb[j] = extrapolator(
+                        R_,
+                        V_blended,
+                        [t_diff_prev],
+                        allow_nonfinite_values=True,
+                        **extrap_kwargs_pb,
+                    )
+                    R_pm_ep.append(R_pm_ep_[0])
+
+                    t_prev[j] = t_sub
+
+                R_f_prev[j] = R_c[j]
+
+            if len(R_f_ep_out) > 0:
+                R_f_ep_out = np.stack(R_f_ep_out)
+                Yn_ep_out = np.stack(Yn_ep_out)
+                R_pm_ep = np.stack(R_pm_ep)
+
+            # advect the forecast field by one time step if no subtimesteps in the
+            # current interval were found
+            if not subtimesteps:
+                t_diff_prev = t + 1 - t_prev[j]
+                t_total[j] += t_diff_prev
+
+                # compute the perturbed motion field - include the NWP
+                # velocities and the weights
+                if vel_pert_method is not None:
+                    V_pert = V + generate_vel_noise(vps[j], t_total[j] * timestep)
+
+                # Stack the perturbed extrapolation and the NWP velocities
+                if blend_nwp_members:
+                    V_stack = np.concatenate(
+                        (V_pert[None, :, :, :], V_models[:, t, :, :, :]), axis=0
+                    )
+                else:
+                    V_model_ = V_models[j, t, :, :, :]
+                    V_stack = np.concatenate(
+                        (V_pert[None, :, :, :], V_model_[None, :, :, :]), axis=0
+                    )
+                    V_model_ = None
+
+                # Obtain a blended optical flow, using the weights of the
+                # second cascade following eq. 24 in BPS2006
+                V_blended = blending.utils.blend_optical_flows(
+                    flows=V_stack,
+                    weights=weights[
+                        :-1, 1
+                    ],  # [(extr_field, n_model_fields), cascade_level=2]
+                )
+
+                # Extrapolate the extrapolation and noise cascade
+                for i in range(n_cascade_levels):
+                    extrap_kwargs_["displacement_prev"] = D[j][i]
+                    extrap_kwargs_noise["displacement_prev"] = D_Yn[j][i]
+                    extrap_kwargs_noise["map_coordinates_mode"] = "wrap"
+
+                    _, D[j][i] = extrapolator(
+                        None,
+                        V_blended,
+                        [t_diff_prev],
+                        allow_nonfinite_values=True,
+                        **extrap_kwargs_,
+                    )
+
+                    _, D_Yn[j][i] = extrapolator(
+                        None,
+                        V_blended,
+                        [t_diff_prev],
+                        allow_nonfinite_values=True,
+                        **extrap_kwargs_noise,
+                    )
+
+                # Also extrapolate the radar observation, used for the probability
+                # matching and post-processing steps
+                extrap_kwargs_pb["displacement_prev"] = D_pb[j]
+                _, D_pb[j] = extrapolator(
+                    None,
+                    V_blended,
+                    [t_diff_prev],
+                    allow_nonfinite_values=True,
+                    **extrap_kwargs_pb,
+                )
+
+                t_prev[j] = t + 1
+
+            # 8.5 Blend the cascades
+            R_f_out = []
+
+            for t_sub in subtimesteps:
+                # TODO: does it make sense to use sub time steps - check if it works?
+                if t_sub > 0:
+                    t_index = np.where(np.array(subtimesteps) == t_sub)[0][0]
+                    # First concatenate the cascades and the means and sigmas
+                    # R_models = [n_models,timesteps,n_cascade_levels,m,n]
+                    if blend_nwp_members:
+                        cascades_stacked = np.concatenate(
+                            (
+                                R_f_ep_out[None, t_index],
+                                R_models[:, t],
+                                Yn_ep_out[None, t_index],
+                            ),
+                            axis=0,
+                        )  # [(extr_field, n_model_fields, noise), n_cascade_levels, ...]
+                        means_stacked = np.concatenate(
+                            (mu_extrapolation[None, :], mu_models[:, t]), axis=0
+                        )
+                        sigmas_stacked = np.concatenate(
+                            (sigma_extrapolation[None, :], sigma_models[:, t]),
+                            axis=0,
+                        )
+                    else:
+                        cascades_stacked = np.concatenate(
+                            (
+                                R_f_ep_out[None, t_index],
+                                R_models[None, j, t],
+                                Yn_ep_out[None, t_index],
+                            ),
+                            axis=0,
+                        )  # [(extr_field, n_model_fields, noise), n_cascade_levels, ...]
+                        means_stacked = np.concatenate(
+                            (mu_extrapolation[None, :], mu_models[None, j, t]), axis=0
+                        )
+                        sigmas_stacked = np.concatenate(
+                            (sigma_extrapolation[None, :], sigma_models[None, j, t]),
+                            axis=0,
+                        )
+
+                    # First determine the blending weights if method is spn. The
+                    # weights for method bps have already been determined.
+                    if weights_method == "spn":
+                        weights = np.zeros(
+                            (cascades_stacked.shape[0], n_cascade_levels)
+                        )
+                        for i in range(n_cascade_levels):
+                            # Determine the normalized covariance matrix (containing)
+                            # the cross-correlations between the models
+                            cascades_stacked_ = np.stack(
+                                [
+                                    cascades_stacked[n_model, i, :, :].flatten()
+                                    for n_model in range(cascades_stacked.shape[0] - 1)
+                                ]
+                            )  # -1 to exclude the noise component
+                            cov = np.ma.corrcoef(
+                                np.ma.masked_invalid(cascades_stacked_)
+                            )
+                            # Determine the weights for this cascade level
+                            weights[:, i] = calculate_weights_spn(
+                                correlations=rho_fc[:, i], cov=cov
+                            )
+
+                    # Blend the extrapolation, (NWP) model(s) and noise cascades
+                    R_f_blended = blending.utils.blend_cascades(
+                        cascades_norm=cascades_stacked, weights=weights
+                    )
+
+                    # Also blend the cascade without the extrapolation component
+                    R_f_blended_mod_only = blending.utils.blend_cascades(
+                        cascades_norm=cascades_stacked[1:, :],
+                        weights=weights_model_only,
+                    )
+
+                    # Blend the means and standard deviations
+                    # Input is array of shape [number_components, scale_level, ...]
+                    means_blended, sigmas_blended = blend_means_sigmas(
+                        means=means_stacked, sigmas=sigmas_stacked, weights=weights
+                    )
+                    # Also blend the means and sigmas for the cascade without extrapolation
+                    (
+                        means_blended_mod_only,
+                        sigmas_blended_mod_only,
+                    ) = blend_means_sigmas(
+                        means=means_stacked[1:, :],
+                        sigmas=sigmas_stacked[1:, :],
+                        weights=weights_model_only,
+                    )
+
+                    # 8.6 Recompose the cascade to a precipitation field
+                    # (The function first normalizes the blended cascade, R_f_blended
+                    # again)
+                    R_f_new = blending.utils.recompose_cascade(
+                        combined_cascade=R_f_blended,
+                        combined_mean=means_blended,
+                        combined_sigma=sigmas_blended,
+                    )
+                    # The recomposed cascade without the extrapolation (for NaN filling
+                    # outside the radar domain)
+                    R_f_new_mod_only = blending.utils.recompose_cascade(
+                        combined_cascade=R_f_blended_mod_only,
+                        combined_mean=means_blended_mod_only,
+                        combined_sigma=sigmas_blended_mod_only,
+                    )
+                    if domain == "spectral":
+                        # TODO: Check this! (Only tested with domain == 'spatial')
+                        R_f_new = fft_objs[j].irfft2(R_f_new)
+                        R_f_new_mod_only = fft_objs[j].irfft2(R_f_new_mod_only)
+
+                    # 8.7 Post-processing steps - use the mask and fill no data with
+                    # the blended NWP forecast. Probability matching following
+                    # Lagrangian blended probability matching which uses the
+                    # latest extrapolated radar rainfall field blended with the
+                    # nwp model(s) rainfall forecast fields as 'benchmark'.
+
+                    # TODO: Check probability matching method
+                    # 8.7.1 first blend the extrapolated rainfall field (the field
+                    # that is only used for post-processing steps) with the NWP
+                    # rainfall forecast for this time step using the weights
+                    # at scale level 2.
+                    weights_pm = weights[:-1, 1]  # Weights without noise, level 2
+                    weights_pm_normalized = weights_pm / np.sum(weights_pm)
+                    # And the weights for outside the radar domain
+                    weights_pm_mod_only = weights_model_only[
+                        :-1, 1
+                    ]  # Weights without noise, level 2
+                    weights_pm_normalized_mod_only = weights_pm_mod_only / np.sum(
+                        weights_pm_mod_only
+                    )
+                    # Stack the fields
+                    if blend_nwp_members:
+                        R_pm_stacked = np.concatenate(
+                            (
+                                R_pm_ep[None, t_index],
+                                R_models_pm[:, t],
+                            ),
+                            axis=0,
+                        )
+                    else:
+                        R_pm_stacked = np.concatenate(
+                            (
+                                R_pm_ep[None, t_index],
+                                R_models_pm[None, j, t],
+                            ),
+                            axis=0,
+                        )
+                    # Blend it
+                    R_pm_blended = np.sum(
+                        weights_pm_normalized.reshape(
+                            weights_pm_normalized.shape[0], 1, 1
+                        )
+                        * R_pm_stacked,
+                        axis=0,
+                    )
+                    if blend_nwp_members:
+                        R_pm_blended_mod_only = np.sum(
+                            weights_pm_normalized_mod_only.reshape(
+                                weights_pm_normalized_mod_only.shape[0], 1, 1
+                            )
+                            * R_models_pm[:, t],
+                            axis=0,
+                        )
+                    else:
+                        R_pm_blended_mod_only = R_models_pm[j, t]
+
+                    # The extrapolation components are NaN outside the advected
+                    # radar domain. This results in NaN values in the blended
+                    # forecast outside the radar domain. Therefore, fill these
+                    # areas with the "..._mod_only" blended forecasts, consisting
+                    # of the NWP and noise components.
+                    nan_indices = np.isnan(R_f_new)
+                    R_f_new[nan_indices] = R_f_new_mod_only[nan_indices]
+                    nan_indices = np.isnan(R_pm_blended)
+                    R_pm_blended[nan_indices] = R_pm_blended_mod_only[nan_indices]
+                    # Finally, fill the remaining nan values, if present, with
+                    # the minimum value in the forecast
+                    nan_indices = np.isnan(R_f_new)
+                    R_f_new[nan_indices] = np.nanmin(R_f_new)
+                    nan_indices = np.isnan(R_pm_blended)
+                    R_pm_blended[nan_indices] = np.nanmin(R_pm_blended)
+
+                    # 8.7.2. Apply the masking and prob. matching
+                    if mask_method is not None:
+                        # apply the precipitation mask to prevent generation of new
+                        # precipitation into areas where it was not originally
+                        # observed
+                        R_cmin = R_f_new.min()
+                        if mask_method == "incremental":
+                            # The incremental mask is slightly different from
+                            # the implementation in the non-blended steps.py, as
+                            # it is not based on the last forecast, but instead
+                            # on R_pm_blended. Therefore, the buffer does not
+                            # increase over time.
+                            # Get the mask for this forecast
+                            MASK_prec = R_pm_blended >= R_thr
+                            # Buffer the mask
+                            MASK_prec = _compute_incremental_mask(
+                                MASK_prec, struct, mask_rim
+                            )
+                            # Get the final mask
+                            R_f_new = R_cmin + (R_f_new - R_cmin) * MASK_prec
+                            MASK_prec_ = R_f_new > R_cmin
+                        elif mask_method == "obs":
+                            # The mask equals the most recent benchmark
+                            # rainfall field
+                            MASK_prec_ = R_pm_blended >= R_thr
+
+                        # Set to min value outside of mask
+                        R_f_new[~MASK_prec_] = R_cmin
+
+                    if probmatching_method == "cdf":
+                        # adjust the CDF of the forecast to match the most recent
+                        # benchmark rainfall field (R_pm_blended)
+                        R_f_new = probmatching.nonparam_match_empirical_cdf(
+                            R_f_new, R_pm_blended
+                        )
+                    elif probmatching_method == "mean":
+                        # Use R_pm_blended as benchmark field and
+                        mu_0 = np.mean(R_pm_blended[R_pm_blended >= R_thr])
+                        MASK = R_f_new >= R_thr
+                        mu_fct = np.mean(R_f_new[MASK])
+                        R_f_new[MASK] = R_f_new[MASK] - mu_fct + mu_0
+
+                    R_f_out.append(R_f_new)
+
+            return R_f_out
+
+        res = []
+        for j in range(n_ens_members):
+            if not DASK_IMPORTED or n_ens_members == 1:
+                res.append(worker(j))
+            else:
+                res.append(dask.delayed(worker)(j))
+
+        R_f_ = (
+            dask.compute(*res, num_workers=num_ensemble_workers)
+            if DASK_IMPORTED and n_ens_members > 1
+            else res
+        )
+        res = None
+
+        if is_nowcast_time_step:
+            if measure_time:
+                print("%.2f seconds." % (time.time() - starttime))
+            else:
+                print("done.")
+
+        if callback is not None:
+            R_f_stacked = np.stack(R_f_)
+            if R_f_stacked.shape[1] > 0:
+                callback(R_f_stacked.squeeze())
+
+        if return_output:
+            for j in range(n_ens_members):
+                R_f[j].extend(R_f_[j])
+
+        R_f_ = None
+
+    if measure_time:
+        mainloop_time = time.time() - starttime_mainloop
+
+    if return_output:
+        outarr = np.stack([np.stack(R_f[j]) for j in range(n_ens_members)])
+        if measure_time:
+            return outarr, init_time, mainloop_time
+        else:
+            return outarr
+    else:
+        return None
+
+
+def calculate_ratios(correlations):
+    """Calculate explained variance ratios from correlation.
+
+    Parameters
+    ----------
+    Array of shape [component, scale_level, ...]
+      containing correlation (skills) for each component (NWP and nowcast),
+      scale level, and optionally along [y, x] dimensions.
+
+    Returns
+    -------
+    out : numpy array
+      An array containing the ratios of explain variance for each
+      component, scale level, ...
+    """
+    # correlations: [component, scale, ...]
+    square_corrs = np.square(correlations)
+    # Calculate the ratio of the explained variance to the unexplained
+    # variance of the nowcast and NWP model components
+    out = square_corrs / (1 - square_corrs)
+    # out: [component, scale, ...]
+    return out
+
+
+def calculate_weights_bps(correlations):
+    """Calculate BPS blending weights for STEPS blending from correlation.
+
+    Parameters
+    ----------
+    correlations : array-like
+      Array of shape [component, scale_level, ...]
+      containing correlation (skills) for each component (NWP and nowcast),
+      scale level, and optionally along [y, x] dimensions.
+
+    Returns
+    -------
+    weights : array-like
+      Array of shape [component+1, scale_level, ...]
+      containing the weights to be used in STEPS blending for
+      each original component plus an addtional noise component, scale level,
+      and optionally along [y, x] dimensions.
+
+    References
+    ----------
+    :cite:`BPS2006`
+
+    Notes
+    -----
+    The weights in the BPS method can sum op to more than 1.0.
+    """
+    # correlations: [component, scale, ...]
+    # Check if the correlations are positive, otherwise rho = 10e-5
+    correlations = np.where(correlations < 10e-5, 10e-5, correlations)
+
+    # If we merge more than one component with the noise cascade, we follow
+    # the weights impolementation in either :cite:`BPS2006` or :cite:`SPN2013`.
+    if correlations.shape[0] > 1:
+        # Calculate weights for each source
+        ratios = calculate_ratios(correlations)
+        # ratios: [component, scale, ...]
+        total_ratios = np.sum(ratios, axis=0)
+        # total_ratios: [scale, ...] - the denominator of eq. 11 & 12 in BPS2006
+        weights = correlations * np.sqrt(ratios / total_ratios)
+        # weights: [component, scale, ...]
+        # Calculate the weight of the noise component.
+        # Original BPS2006 method in the following two lines (eq. 13)
+        total_square_weights = np.sum(np.square(weights), axis=0)
+        noise_weight = np.sqrt(1.0 - total_square_weights)
+        # Finally, add the noise_weights to the weights variable.
+        weights = np.concatenate((weights, noise_weight[None, ...]), axis=0)
+
+    # Otherwise, the weight equals the correlation on that scale level and
+    # the noise component weight equals 1 - this weight. This only occurs for
+    # the weights calculation outside the radar domain where in the case of 1
+    # NWP model or ensemble member, no blending of multiple models has to take
+    # place
+    else:
+        noise_weight = 1.0 - correlations
+        weights = np.concatenate((correlations, noise_weight), axis=0)
+
+    return weights
+
+
+def calculate_weights_spn(correlations, cov):
+    """Calculate SPN blending weights for STEPS blending from correlation.
+
+    Parameters
+    ----------
+    correlations : array-like
+      Array of shape [n_components]
+      containing correlation (skills) for each component (NWP models and nowcast).
+    cov : array-like
+        Array of shape [n_components, n_components] containing the covariance
+        matrix of the models that will be blended. If cov is set to None and
+        correlations only contains one model, the weight equals the correlation
+        on that scale level and the noise component weight equals 1 - this weight.
+
+    Returns
+    -------
+    weights : array-like
+      Array of shape [component+1]
+      containing the weights to be used in STEPS blending for each original
+      component plus an addtional noise component.
+
+    References
+    ----------
+    :cite:`SPN2013`
+    """
+    # Check if the correlations are positive, otherwise rho = 10e-5
+    correlations = np.where(correlations < 10e-5, 10e-5, correlations)
+
+    if correlations.shape[0] > 1:
+        if isinstance(cov, type(None)):
+            raise ValueError("cov must contain a covariance matrix")
+        else:
+            # Make a numpy matrix out of cov and get the inverse
+            cov_matrix = np.asmatrix(cov)
+            cov_matrix_inv = cov_matrix.getI()
+            # The component weights are the dot product between cov_matrix_inv
+            # and cor_vec
+            weights = cov_matrix_inv.dot(correlations)
+            # If the dot product of the weights with the correlations is
+            # larger than 1.0, we assign a weight of 0.0 to the noise (to make
+            # it numerically stable)
+            if weights.dot(correlations) > 1.0:
+                noise_weight = np.array([0])
+            # Calculate the noise weight
+            else:
+                noise_weight = np.asarray(np.sqrt(1 - weights.dot(correlations)))[0]
+            # Make sure the weights are positive, otherwise weight = 0.0
+            weights = np.where(weights < 0.0, 0.0, weights)[0]
+            # Finally, add the noise_weights to the weights variable.
+            weights = np.concatenate((weights, noise_weight), axis=0)
+
+    # Otherwise, the weight equals the correlation on that scale level and
+    # the noise component weight equals 1 - this weight. This only occurs for
+    # the weights calculation outside the radar domain where in the case of 1
+    # NWP model or ensemble member, no blending of multiple models has to take
+    # place
+    else:
+        noise_weight = 1.0 - correlations
+        weights = np.concatenate((correlations, noise_weight), axis=0)
+
+    return weights
+
+
+def blend_means_sigmas(means, sigmas, weights):
+    """Calculate the blended means and sigmas, the normalization parameters
+    needed to recompose the cascade. This procedure uses the weights of the
+    blending of the normalized cascades and follows eq. 32 and 33 in BPS2004.
+
+
+    Parameters
+    ----------
+    means : array-like
+      Array of shape [number_components, scale_level, ...]
+      with the mean for each component (NWP, nowcasts, noise).
+    sigmas : array-like
+      Array of shape [number_components, scale_level, ...]
+      with the standard deviation for each component.
+    weights : array-like
+      An array of shape [number_components + 1, scale_level, ...]
+      containing the weights to be used in this routine
+      for each component plus noise, scale level, and optionally [y, x]
+      dimensions, obtained by calling a method implemented in
+      pysteps.blending.steps.calculate_weights_...
+
+    Returns
+    -------
+    combined_means : array-like
+      An array of shape [scale_level, ...]
+      containing per scale level (cascade) the weighted combination of
+      means from multiple components (NWP, nowcasts and noise).
+    combined_sigmas : array-like
+      An array of shape [scale_level, ...]
+      similar to combined_means, but containing the standard deviations.
+
+    """
+    # Check if the dimensions are the same
+    diff_dims = weights.ndim - means.ndim
+    if diff_dims:
+        for i in range(diff_dims):
+            means = np.expand_dims(means, axis=means.ndim)
+    diff_dims = weights.ndim - sigmas.ndim
+    if diff_dims:
+        for i in range(diff_dims):
+            sigmas = np.expand_dims(sigmas, axis=sigmas.ndim)
+    # Weight should have one component more (the noise component) than the
+    # means and sigmas. Check this
+    if (
+        weights.shape[0] - means.shape[0] != 1
+        or weights.shape[0] - sigmas.shape[0] != 1
+    ):
+        raise ValueError(
+            "The weights array does not have one (noise) component more than mu and sigma"
+        )
+    else:
+        # Throw away the last component, which is the noise component
+        weights = weights[:-1]
+
+    # Combine (blend) the means and sigmas
+    combined_means = np.zeros(weights.shape[1])
+    combined_sigmas = np.zeros(weights.shape[1])
+    total_weight = np.sum((weights), axis=0)
+    for i in range(weights.shape[0]):
+        combined_means += (weights[i] / total_weight) * means[i]
+        combined_sigmas += (weights[i] / total_weight) * sigmas[i]
+    # TODO: substract covariances to weigthed sigmas - still necessary?
+
+    return combined_means, combined_sigmas
+
+
+def _check_inputs(R, R_d_models, V, V_models, timesteps, ar_order):
+    if R.ndim != 3:
+        raise ValueError("R must be a three-dimensional array")
+    if R.shape[0] < ar_order + 1:
+        raise ValueError("R.shape[0] < ar_order+1")
+    if R_d_models.ndim != 2 and R_d_models.ndim != 4:
+        raise ValueError(
+            "R_d_models must be either a two-dimensional array containing dictionaries with decomposed model fields or a four-dimensional array containing the original (NWP) model forecasts"
+        )
+    if V.ndim != 3:
+        raise ValueError("V must be a three-dimensional array")
+    if V_models.ndim != 5:
+        raise ValueError("V_models must be a five-dimensional array")
+    if V.shape[0] != 2 or V_models.shape[2] != 2:
+        raise ValueError(
+            "V and V_models must have an x- and y-component, check the shape"
+        )
+    if R.shape[1:3] != V.shape[1:3]:
+        raise ValueError(
+            "dimension mismatch between R and V: shape(R)=%s, shape(V)=%s"
+            % (str(R.shape), str(V.shape))
+        )
+    if R_d_models.shape[0] != V_models.shape[0]:
+        raise ValueError(
+            "R_d_models and V_models must consist of the same number of models"
+        )
+    if isinstance(timesteps, list) and not sorted(timesteps) == timesteps:
+        raise ValueError("timesteps is not in ascending order")
+    if isinstance(timesteps, list):
+        if R_d_models.shape[1] != len(timesteps) + 1:
+            raise ValueError(
+                "R_models does not contain sufficient lead times for this forecast"
+            )
+    else:
+        if R_d_models.shape[1] != timesteps + 1:
+            raise ValueError(
+                "R_models does not contain sufficient lead times for this forecast"
+            )
+
+
+def _compute_incremental_mask(Rbin, kr, r):
+    # buffer the observation mask Rbin using the kernel kr
+    # add a grayscale rim r (for smooth rain/no-rain transition)
+
+    # buffer observation mask
+    Rbin = np.ndarray.astype(Rbin.copy(), "uint8")
+    Rd = scipy.ndimage.morphology.binary_dilation(Rbin, kr)
+
+    # add grayscale rim
+    kr1 = scipy.ndimage.generate_binary_structure(2, 1)
+    mask = Rd.astype(float)
+    for n in range(r):
+        Rd = scipy.ndimage.morphology.binary_dilation(Rd, kr1)
+        mask += Rd
+    # normalize between 0 and 1
+    return mask / mask.max()
+
+
+def _transform_to_lagrangian(
+    R, V, ar_order, xy_coords, extrapolator, extrap_kwargs, num_workers
+):
+    """Advect the previous precipitation fields to the same position with the
+    most recent one (i.e. transform them into the Lagrangian coordinates).
+    """
+    extrap_kwargs = extrap_kwargs.copy()
+    extrap_kwargs["xy_coords"] = xy_coords
+    res = list()
+
+    def f(R, i):
+        return extrapolator(
+            R[i, :, :],
+            V,
+            ar_order - i,
+            "min",
+            allow_nonfinite_values=True,
+            **extrap_kwargs,
+        )[-1]
+
+    for i in range(ar_order):
+        if not DASK_IMPORTED:
+            R[i, :, :] = f(R, i)
+        else:
+            res.append(dask.delayed(f)(R, i))
+
+    if DASK_IMPORTED:
+        num_workers_ = len(res) if num_workers > len(res) else num_workers
+        R = np.stack(list(dask.compute(*res, num_workers=num_workers_)) + [R[-1, :, :]])
+
+    # replace non-finite values with the minimum value
+    R = R.copy()
+    for i in range(R.shape[0]):
+        R[i, ~np.isfinite(R[i, :])] = np.nanmin(R[i, :])
+    return R
+
+
+def _init_noise(
+    R,
+    R_thr,
+    n_cascade_levels,
+    filter,
+    decompositor,
+    fft,
+    noise_method,
+    noise_kwargs,
+    noise_stddev_adj,
+    measure_time,
+    num_workers,
+):
+    """Initialize the noise method."""
+    if noise_method is None:
+        return None, None, None
+
+    # get methods for perturbations
+    init_noise, generate_noise = noise.get_method(noise_method)
+
+    # initialize the perturbation generator for the precipitation field
+    pp = init_noise(R, fft_method=fft, **noise_kwargs)
+
+    if noise_stddev_adj == "auto":
+        print("Computing noise adjustment coefficients... ", end="", flush=True)
+        if measure_time:
+            starttime = time.time()
+
+        R_min = np.min(R)
+        noise_std_coeffs = noise.utils.compute_noise_stddev_adjs(
+            R[-1, :, :],
+            R_thr,
+            R_min,
+            filter,
+            decompositor,
+            pp,
+            generate_noise,
+            20,
+            conditional=True,
+            num_workers=num_workers,
+        )
+
+        if measure_time:
+            print("%.2f seconds." % (time.time() - starttime))
+        else:
+            print("done.")
+    elif noise_stddev_adj == "fixed":
+        f = lambda k: 1.0 / (0.75 + 0.09 * k)
+        noise_std_coeffs = [f(k) for k in range(1, n_cascade_levels + 1)]
+    else:
+        noise_std_coeffs = np.ones(n_cascade_levels)
+
+    if noise_stddev_adj is not None:
+        print("noise std. dev. coeffs:   %s" % str(noise_std_coeffs))
+
+    return pp, generate_noise, noise_std_coeffs
+
+
+def _compute_cascade_decomposition_radar(
+    R,
+    ar_order,
+    n_cascade_levels,
+    n_ens_members,
+    MASK_thr,
+    domain,
+    filter,
+    decompositor,
+    fft,
+):
+    """Compute the cascade decompositions of the input precipitation fields."""
+    R_d = []
+    for i in range(ar_order + 1):
+        R_ = decompositor(
+            R[i, :, :],
+            filter,
+            mask=MASK_thr,
+            fft_method=fft,
+            output_domain=domain,
+            normalize=True,
+            compute_stats=True,
+            compact_output=True,
+        )
+        R_d.append(R_)
+
+    # Rearrange the cascaded into a four-dimensional array of shape
+    # (n_cascade_levels,ar_order+1,m,n) for the autoregressive model
+    R_c = nowcast_utils.stack_cascades(R_d, n_cascade_levels)
+
+    R_d = R_d[-1]
+    mu_extrapolation = np.array(R_d["means"])
+    sigma_extrapolation = np.array(R_d["stds"])
+    R_d = [R_d.copy() for j in range(n_ens_members)]
+    return R_c, R_d, mu_extrapolation, sigma_extrapolation
+
+
+def _compute_cascade_decomposition_nwp(
+    R_d_models, filter, decompositor, recompositor, fft, domain
+):
+    """If necessary, decompose (NWP) model forecasts and stack cascades."""
+    if R_d_models.ndim == 4:
+        # Keep the model fields for the probability matching later on
+        R_models_pm = R_d_models.copy()
+        # Decompose the (NWP) model forecasts
+        R_d_models_ = []
+        # Loop through the n_models
+        for i in range(R_d_models.shape[0]):
+            R_d_models__ = []
+            # Loop through the time steps
+            for j in range(R_d_models.shape[1]):
+                R_ = decompositor(
+                    field=R_d_models[i, j, :, :],
+                    bp_filter=filter,
+                    fft_method=fft,
+                    output_domain=domain,
+                    normalize=True,
+                    compute_stats=True,
+                    compact_output=True,
+                )
+                R_d_models__.append(R_)
+            R_d_models_.append(R_d_models__)
+        R_d_models = np.array(R_d_models_)
+
+        R_d_models_, R_d_models__ = None, None
+
+    # Stack the (NWP) model cascades in separate normalized cascades and return
+    # the means and sigmas.
+    # The normalized model cascade should have the shape:
+    # [n_models, n_timesteps, n_cascade_levels, m, n]
+    R_models = []
+    mu_models = []
+    sigma_models = []
+    # Stack it per model and combine that
+    for i in range(R_d_models.shape[0]):
+        R_models_, mu_models_, sigma_models_ = [], [], []
+        R_models_, mu_models_, sigma_models_ = blending.utils.stack_cascades(
+            R_d=R_d_models[i, :], donorm=False
+        )
+        R_models.append(R_models_)
+        mu_models.append(mu_models_)
+        sigma_models.append(sigma_models_)
+
+    R_models = np.stack(R_models)
+    mu_models = np.stack(mu_models)
+    sigma_models = np.stack(sigma_models)
+
+    R_models_, mu_models_, sigma_models_ = None, None, None
+
+    if R_d_models.ndim == 2:
+        # Finally, recompose the (NWP) model cascades to have rainfall fields per
+        # model and time step, which will be used in the probability matching steps.
+        # Recomposed cascade will have shape: [n_models, n_timesteps, m, n]
+        R_models_pm = []
+        for i in range(R_d_models.shape[0]):
+            R_models_pm_ = []
+            for time_step in range(R_d_models.shape[1]):
+                R_models_pm_.append(recompositor(R_d_models[i, time_step]))
+            R_models_pm.append(R_models_pm_)
+
+        R_models_pm = np.stack(R_models_pm)
+        R_models_pm_ = None
+    return R_models, mu_models, sigma_models, R_models_pm
+
+
+def _estimate_ar_parameters_radar(R_c, ar_order, n_cascade_levels, MASK_thr):
+    """Estimate AR parameters for the radar rainfall field."""
+    # compute lag-l temporal autocorrelation coefficients for each cascade level
+    GAMMA = np.empty((n_cascade_levels, ar_order))
+    for i in range(n_cascade_levels):
+        GAMMA[i, :] = correlation.temporal_autocorrelation(R_c[i], mask=MASK_thr)
+
+    nowcast_utils.print_corrcoefs(GAMMA)
+
+    if ar_order == 2:
+        # adjust the lag-2 correlation coefficient to ensure that the AR(p)
+        # process is stationary
+        for i in range(n_cascade_levels):
+            GAMMA[i, 1] = autoregression.adjust_lag2_corrcoef2(GAMMA[i, 0], GAMMA[i, 1])
+
+    # estimate the parameters of the AR(p) model from the autocorrelation
+    # coefficients
+    PHI = np.empty((n_cascade_levels, ar_order + 1))
+    for i in range(n_cascade_levels):
+        PHI[i, :] = autoregression.estimate_ar_params_yw(GAMMA[i, :])
+
+    nowcast_utils.print_ar_params(PHI)
+    return PHI
+
+
+def _find_nwp_combination(
+    R_c,
+    R_models,
+    R_models_pm,
+    V_models,
+    mu_models,
+    sigma_models,
+    n_ens_members,
+    ar_order,
+    n_cascade_levels,
+    blend_nwp_members,
+):
+    """Determine which (NWP) models will be combined with which nowcast ensemble members.
+    With the way it is implemented at this moment: n_ens_members of the output equals
+    the maximum number of (ensemble) members in the input (either the nowcasts or NWP).
+    """
+    ###
+    # First, discard all except the p-1 last cascades because they are not needed
+    # for the AR(p) model
+    R_c = [R_c[i][-ar_order:] for i in range(n_cascade_levels)]
+
+    # Check if NWP models/members should be used individually, or if all of
+    # them are blended together per nowcast ensemble member.
+    if blend_nwp_members:
+        # stack the extrapolation cascades into a list containing all ensemble members
+        R_c = [
+            [R_c[j].copy() for j in range(n_cascade_levels)]
+            for i in range(n_ens_members)
+        ]
+        R_c = np.stack(R_c)
+        n_model_indices = None
+
+    else:
+        # Start with determining the maximum and mimimum number of members/models
+        # in both input products
+        n_model_members = R_models.shape[0]
+        n_ens_members_max = max(n_ens_members, n_model_members)
+        n_ens_members_min = min(n_ens_members, n_model_members)
+        # Also make a list of the model index numbers. These indices are needed
+        # for indexing the right climatological skill file when pysteps calculates
+        # the blended forecast in parallel.
+        if n_model_members > 1:
+            n_model_indices = np.arange(n_model_members)
+        else:
+            n_model_indices = [0]
+
+        # Now, repeat the nowcast ensemble members or the nwp models/members until
+        # it has the same amount of members as n_ens_members_max. For instance, if
+        # you have 10 ensemble nowcasts members and 3 NWP members, the output will
+        # be an ensemble of 10 members. Hence, the three NWP members are blended
+        # with the first three members of the nowcast (member one with member one,
+        # two with two, etc.), subsequently, the same NWP members are blended with
+        # the next three members (NWP member one with member 4, NWP member 2 with
+        # member 5, etc.), until 10 is reached.
+        if n_ens_members_min != n_ens_members_max:
+            if n_model_members == 1:
+                R_models = np.repeat(R_models[:, :, :, :, :], n_ens_members_max, axis=0)
+                mu_models = np.repeat(mu_models[:, :, :], n_ens_members_max, axis=0)
+                sigma_models = np.repeat(
+                    sigma_models[:, :, :], n_ens_members_max, axis=0
+                )
+                V_models = np.repeat(V_models[:, :, :, :], n_ens_members_max, axis=0)
+                # For the prob. matching
+                R_models_pm = np.repeat(
+                    R_models_pm[:, :, :, :], n_ens_members_max, axis=0
+                )
+                # Finally, for the model indices
+                n_model_indices = np.repeat(n_model_indices, n_ens_members_max, axis=0)
+
+            elif n_model_members == n_ens_members_min:
+                repeats = [
+                    (n_ens_members_max + i) // n_ens_members_min
+                    for i in range(n_ens_members_min)
+                ]
+                if n_model_members == n_ens_members_min:
+                    R_models = np.repeat(R_models, repeats, axis=0)
+                    mu_models = np.repeat(mu_models, repeats, axis=0)
+                    sigma_models = np.repeat(sigma_models, repeats, axis=0)
+                    V_models = np.repeat(V_models, repeats, axis=0)
+                    # For the prob. matching
+                    R_models_pm = np.repeat(R_models_pm, repeats, axis=0)
+                    # Finally, for the model indices
+                    n_model_indices = np.repeat(n_model_indices, repeats, axis=0)
+
+        R_c = [
+            [R_c[j].copy() for j in range(n_cascade_levels)]
+            for i in range(n_ens_members_max)
+        ]
+        R_c = np.stack(R_c)
+
+        n_ens_members = n_ens_members_max
+
+    return (
+        R_c,
+        R_models,
+        R_models_pm,
+        V_models,
+        mu_models,
+        sigma_models,
+        n_ens_members,
+        n_model_indices,
+    )
+
+
+def _init_random_generators(
+    V,
+    noise_method,
+    vel_pert_method,
+    vp_par,
+    vp_perp,
+    seed,
+    n_ens_members,
+    kmperpixel,
+    timestep,
+):
+    """Initialize all the random generators."""
+    if noise_method is not None:
+        randgen_prec = []
+        randgen_motion = []
+        np.random.seed(seed)
+        for j in range(n_ens_members):
+            rs = np.random.RandomState(seed)
+            randgen_prec.append(rs)
+            seed = rs.randint(0, high=1e9)
+            rs = np.random.RandomState(seed)
+            randgen_motion.append(rs)
+            seed = rs.randint(0, high=1e9)
+
+    if vel_pert_method is not None:
+        init_vel_noise, generate_vel_noise = noise.get_method(vel_pert_method)
+
+        # initialize the perturbation generators for the motion field
+        vps = []
+        for j in range(n_ens_members):
+            kwargs = {
+                "randstate": randgen_motion[j],
+                "p_par": vp_par,
+                "p_perp": vp_perp,
+            }
+            vp_ = init_vel_noise(V, 1.0 / kmperpixel, timestep, **kwargs)
+            vps.append(vp_)
+    else:
+        vps, generate_vel_noise = None, None
+
+    return randgen_prec, vps, generate_vel_noise
+
+
+def _prepare_forecast_loop(
+    R_c,
+    noise_method,
+    fft_method,
+    n_cascade_levels,
+    n_ens_members,
+    mask_method,
+    mask_kwargs,
+    timestep,
+    kmperpixel,
+):
+    """Prepare for the forecast loop."""
+    # Empty arrays for the previous displacements and the forecast cascade
+    D = np.stack([np.full(n_cascade_levels, None) for j in range(n_ens_members)])
+    D_Yn = np.stack([np.full(n_cascade_levels, None) for j in range(n_ens_members)])
+    D_pb = np.stack([None for j in range(n_ens_members)])
+    R_f = [[] for j in range(n_ens_members)]
+
+    if mask_method == "incremental":
+        # get mask parameters
+        mask_rim = mask_kwargs.get("mask_rim", 10)
+        mask_f = mask_kwargs.get("mask_f", 1.0)
+        # initialize the structuring element
+        struct = scipy.ndimage.generate_binary_structure(2, 1)
+        # iterate it to expand it nxn
+        n = mask_f * timestep / kmperpixel
+        struct = scipy.ndimage.iterate_structure(struct, int((n - 1) / 2.0))
+    else:
+        mask_rim, struct = None, None
+
+    if noise_method is None:
+        R_m = [R_c[0][i].copy() for i in range(n_cascade_levels)]
+    else:
+        R_m = None
+
+    fft_objs = []
+    for i in range(n_ens_members):
+        fft_objs.append(utils.get_method(fft_method, shape=R_c.shape[-2:]))
+
+    return D, D_Yn, D_pb, R_f, R_m, mask_rim, struct, fft_objs
+
+
+def _compute_initial_nwp_skill(
+    R_c, R_models, domain_mask, issuetime, outdir_path_skill, clim_kwargs
+):
+    """Calculate the initial skill of the (NWP) model forecasts at t=0."""
+    rho_nwp_models = [
+        blending.skill_scores.spatial_correlation(
+            obs=R_c[0, :, -1, :, :],
+            mod=R_models[n_model, 0, :, :, :],
+            domain_mask=domain_mask,
+        )
+        for n_model in range(R_models.shape[0])
+    ]
+    rho_nwp_models = np.stack(rho_nwp_models)
+
+    # Ensure that the model skill decreases with increasing scale level.
+    for n_model in range(R_models.shape[0]):
+        for i in range(1, R_models.shape[2]):
+            if rho_nwp_models[n_model, i] > rho_nwp_models[n_model, i - 1]:
+                # Set it equal to the previous scale level
+                rho_nwp_models[n_model, i] = rho_nwp_models[n_model, i - 1]
+
+    # Save this in the climatological skill file
+    blending.clim.save_skill(
+        current_skill=rho_nwp_models,
+        validtime=issuetime,
+        outdir_path=outdir_path_skill,
+        **clim_kwargs,
+    )
+    return rho_nwp_models
diff --git a/pysteps/blending/utils.py b/pysteps/blending/utils.py
new file mode 100644
index 000000000..f7d05ae30
--- /dev/null
+++ b/pysteps/blending/utils.py
@@ -0,0 +1,511 @@
+# -*- coding: utf-8 -*-
+"""
+pysteps.blending.utils
+======================
+
+Module with common utilities used by the blending methods.
+
+.. autosummary::
+    :toctree: ../generated/
+
+    stack_cascades
+    blend_cascades
+    recompose_cascade
+    blend_optical_flows
+    decompose_NWP
+    compute_store_nwp_motion
+    load_NWP
+"""
+
+import datetime
+from pathlib import Path
+
+import numpy as np
+import pandas as pd
+import netCDF4
+
+from pysteps.cascade import get_method as cascade_get_method
+from pysteps.cascade.bandpass_filters import filter_gaussian
+from pysteps import rcparams
+from pysteps.utils import get_method as utils_get_method
+
+
+def stack_cascades(R_d, donorm=True):
+    """Stack the given cascades into a larger array.
+
+    Parameters
+    ----------
+    R_d : dict
+      Dictionary containing a list of cascades obtained by calling a method
+      implemented in pysteps.cascade.decomposition.
+    donorm : bool
+      If True, normalize the cascade levels before stacking.
+
+    Returns
+    -------
+    out : tuple
+      A three-element tuple containing a four-dimensional array of stacked
+      cascade levels and arrays of mean values and standard deviations for each
+      cascade level.
+    """
+    R_c = []
+    mu_c = []
+    sigma_c = []
+
+    for cascade in R_d:
+        R_ = []
+        R_i = cascade["cascade_levels"]
+        n_levels = R_i.shape[0]
+        mu_ = np.asarray(cascade["means"])
+        sigma_ = np.asarray(cascade["stds"])
+        if donorm:
+            for j in range(n_levels):
+                R__ = (R_i[j, :, :] - mu_[j]) / sigma_[j]
+                R_.append(R__)
+        else:
+            R_ = R_i
+        R_c.append(np.stack(R_))
+        mu_c.append(mu_)
+        sigma_c.append(sigma_)
+    return np.stack(R_c), np.stack(mu_c), np.stack(sigma_c)
+
+
+def blend_cascades(cascades_norm, weights):
+    """Calculate blended normalized cascades using STEPS weights following eq.
+    10 in :cite:`BPS2006`.
+
+    Parameters
+    ----------
+    cascades_norm : array-like
+      Array of shape [number_components + 1, scale_level, ...]
+      with the cascade for each component (NWP, nowcasts, noise) and scale level,
+      obtained by calling a method implemented in pysteps.blending.utils.stack_cascades
+
+    weights : array-like
+      An array of shape [number_components + 1, scale_level, ...]
+      containing the weights to be used in this routine
+      for each component plus noise, scale level, and optionally [y, x]
+      dimensions, obtained by calling a method implemented in
+      pysteps.blending.steps.calculate_weights
+
+    Returns
+    -------
+    combined_cascade : array-like
+      An array of shape [scale_level, y, x]
+      containing per scale level (cascade) the weighted combination of
+      cascades from multiple components (NWP, nowcasts and noise) to be used
+      in STEPS blending.
+    """
+    # check inputs
+    if isinstance(cascades_norm, (list, tuple)):
+        cascades_norm = np.stack(cascades_norm)
+
+    if isinstance(weights, (list, tuple)):
+        weights = np.asarray(weights)
+
+    # check weights dimensions match number of sources
+    num_sources = cascades_norm.shape[0]
+    num_sources_klevels = cascades_norm.shape[1]
+    num_weights = weights.shape[0]
+    num_weights_klevels = weights.shape[1]
+
+    if num_weights != num_sources:
+        raise ValueError(
+            "dimension mismatch between cascades and weights.\n"
+            "weights dimension must match the number of components in cascades.\n"
+            f"number of models={num_sources}, number of weights={num_weights}"
+        )
+    if num_weights_klevels != num_sources_klevels:
+        raise ValueError(
+            "dimension mismatch between cascades and weights.\n"
+            "weights cascade levels dimension must match the number of cascades in cascades_norm.\n"
+            f"number of cascade levels={num_sources_klevels}, number of weights={num_weights_klevels}"
+        )
+
+    # cascade_norm component, scales, y, x
+    # weights component, scales, ....
+    # Reshape weights to make the calculation possible with numpy
+    all_c_wn = weights.reshape(num_weights, num_weights_klevels, 1, 1) * cascades_norm
+    combined_cascade = np.sum(all_c_wn, axis=0)
+    # combined_cascade [scale, ...]
+    return combined_cascade
+
+
+def recompose_cascade(combined_cascade, combined_mean, combined_sigma):
+    """Recompose the cascades into a transformed rain rate field.
+
+
+    Parameters
+    ----------
+    combined_cascade : array-like
+      An array of shape [scale_level, y, x]
+      containing per scale level (cascade) the weighted combination of
+      cascades from multiple components (NWP, nowcasts and noise) to be used
+      in STEPS blending.
+    combined_mean : array-like
+      An array of shape [scale_level, ...]
+      similar to combined_cascade, but containing the normalization parameter
+      mean.
+    combined_sigma : array-like
+      An array of shape [scale_level, ...]
+      similar to combined_cascade, but containing the normalization parameter
+      standard deviation.
+
+    Returns
+    -------
+    out: array-like
+        A two-dimensional array containing the recomposed cascade.
+
+    """
+    # Renormalize with the blended sigma and mean values
+    renorm = (
+        combined_cascade * combined_sigma.reshape(combined_cascade.shape[0], 1, 1)
+    ) + combined_mean.reshape(combined_mean.shape[0], 1, 1)
+    # print(renorm.shape)
+    out = np.sum(renorm, axis=0)
+    # print(out.shape)
+    return out
+
+
+def blend_optical_flows(flows, weights):
+    """Combine advection fields using given weights. Following :cite:`BPS2006`
+    the second level of the cascade is used for the weights
+
+    Parameters
+    ----------
+    flows : array-like
+      A stack of multiple advenction fields having shape
+      (S, 2, m, n), where flows[N, :, :, :] contains the motion vectors
+      for source N.
+      Advection fields for each source can be obtanined by
+      calling any of the methods implemented in
+      pysteps.motion and then stack all together
+    weights : array-like
+      An array of shape [number_sources]
+      containing the weights to be used to combine
+      the advection fields of each source.
+      weights are modified to make their sum equal to one.
+    Returns
+    -------
+    out: ndarray_
+        Return the blended advection field having shape
+        (2, m, n), where out[0, :, :] contains the x-components of
+        the blended motion vectors and out[1, :, :] contains the y-components.
+        The velocities are in units of pixels / timestep.
+    """
+
+    # check inputs
+    if isinstance(flows, (list, tuple)):
+        flows = np.stack(flows)
+
+    if isinstance(weights, (list, tuple)):
+        weights = np.asarray(weights)
+
+    # check weights dimensions match number of sources
+    num_sources = flows.shape[0]
+    num_weights = weights.shape[0]
+
+    if num_weights != num_sources:
+        raise ValueError(
+            "dimension mismatch between flows and weights.\n"
+            "weights dimension must match the number of flows.\n"
+            f"number of flows={num_sources}, number of weights={num_weights}"
+        )
+    # normalize weigths
+    weights = weights / np.sum(weights)
+
+    # flows dimension sources, 2, m, n
+    # weights dimension sources
+    # move source axis to last to allow broadcasting
+    # TODO: Check if broadcasting has worked well
+    all_c_wn = weights * np.moveaxis(flows, 0, -1)
+    # sum uses last axis
+    combined_flows = np.sum(all_c_wn, axis=-1)
+    # combined_flows [2, m, n]
+    return combined_flows
+
+
+def decompose_NWP(
+    R_NWP,
+    NWP_model,
+    analysis_time,
+    timestep,
+    valid_times,
+    output_path,
+    num_cascade_levels=8,
+    num_workers=1,
+    decomp_method="fft",
+    fft_method="numpy",
+    domain="spatial",
+    normalize=True,
+    compute_stats=True,
+    compact_output=True,
+):
+    """Decomposes the NWP forecast data into cascades and saves it in
+    a netCDF file
+
+    Parameters
+    ----------
+    R_NWP: array-like
+      Array of dimension (n_timesteps, x, y) containing the precipitation forecast
+      from some NWP model.
+    NWP_model: str
+      The name of the NWP model
+    analysis_time: numpy.datetime64
+      The analysis time of the NWP forecast. The analysis time is assumed to be a
+      numpy.datetime64 type as imported by the pysteps importer
+    timestep: int
+      Timestep in minutes between subsequent NWP forecast fields
+    valid_times: array_like
+      Array containing the valid times of the NWP forecast fields. The times are
+      assumed to be numpy.datetime64 types as imported by the pysteps importer.
+    output_path: str
+      The location where to save the file with the NWP cascade. Defaults to the
+      path_workdir specified in the rcparams file.
+    num_cascade_levels: int, optional
+      The number of frequency bands to use. Must be greater than 2. Defaults to 8.
+    num_workers: int, optional
+      The number of workers to use for parallel computation. Applicable if dask
+      is enabled or pyFFTW is used for computing the FFT. When num_workers>1, it
+      is advisable to disable OpenMP by setting the environment variable
+      OMP_NUM_THREADS to 1. This avoids slowdown caused by too many simultaneous
+      threads.
+
+    Other Parameters
+    ----------------
+    decomp_method: str, optional
+      A string defining the decomposition method to use. Defaults to "fft".
+    fft_method: str or tuple, optional
+      A string or a (function,kwargs) tuple defining the FFT method to use
+      (see :py:func:`pysteps.utils.interface.get_method`).
+      Defaults to "numpy". This option is not used if input_domain and
+      output_domain are both set to "spectral".
+    domain: {"spatial", "spectral"}, optional
+      If "spatial", the output cascade levels are transformed back to the
+      spatial domain by using the inverse FFT. If "spectral", the cascade is
+      kept in the spectral domain. Defaults to "spatial".
+    normalize: bool, optional
+      If True, normalize the cascade levels to zero mean and unit variance.
+      Requires that compute_stats is True. Implies that compute_stats is True.
+      Defaults to False.
+    compute_stats: bool, optional
+      If True, the output dictionary contains the keys "means" and "stds"
+      for the mean and standard deviation of each output cascade level.
+      Defaults to False.
+    compact_output: bool, optional
+      Applicable if output_domain is "spectral". If set to True, only the
+      parts of the Fourier spectrum with non-negligible filter weights are
+      stored. Defaults to False.
+
+
+    Returns
+    -------
+    Nothing
+    """
+
+    # Make a NetCDF file
+    output_date = datetime.datetime.strftime(
+        pd.to_datetime(analysis_time), "%Y%m%d%H%M%S"
+    )
+    outfn = Path(output_path) / f"cascade_{NWP_model}_{output_date}.nc"
+    ncf = netCDF4.Dataset(outfn, "w", format="NETCDF4")
+
+    # Express times relative to the zero time
+    zero_time = np.datetime64("1970-01-01T00:00:00", "ns")
+    valid_times = np.array(valid_times) - zero_time
+    analysis_time = analysis_time - zero_time
+
+    # Set attributes of decomposition method
+    ncf.domain = domain
+    ncf.normalized = int(normalize)
+    ncf.compact_output = int(compact_output)
+    ncf.analysis_time = int(analysis_time)
+    ncf.timestep = int(timestep)
+
+    # Create dimensions
+    time_dim = ncf.createDimension("time", R_NWP.shape[0])
+    casc_dim = ncf.createDimension("cascade_levels", num_cascade_levels)
+    x_dim = ncf.createDimension("x", R_NWP.shape[2])
+    y_dim = ncf.createDimension("y", R_NWP.shape[1])
+
+    # Create variables (decomposed cascade, means and standard deviations)
+    R_d = ncf.createVariable(
+        "pr_decomposed",
+        np.float32,
+        ("time", "cascade_levels", "y", "x"),
+        zlib=True,
+        complevel=4,
+    )
+    means = ncf.createVariable("means", np.float64, ("time", "cascade_levels"))
+    stds = ncf.createVariable("stds", np.float64, ("time", "cascade_levels"))
+    v_times = ncf.createVariable("valid_times", np.float64, ("time",))
+    v_times.units = "nanoseconds since 1970-01-01 00:00:00"
+
+    # The valid times are saved as an array of floats, because netCDF files can't handle datetime types
+    v_times[:] = np.array([np.float64(valid_times[i]) for i in range(len(valid_times))])
+
+    # Decompose the NWP data
+    filter_g = filter_gaussian(R_NWP.shape[1:], num_cascade_levels)
+    fft = utils_get_method(fft_method, shape=R_NWP.shape[1:], n_threads=num_workers)
+    decomp_method, _ = cascade_get_method(decomp_method)
+
+    for i in range(R_NWP.shape[0]):
+        R_ = decomp_method(
+            field=R_NWP[i, :, :],
+            bp_filter=filter_g,
+            fft_method=fft,
+            input_domain=domain,
+            output_domain=domain,
+            normalize=normalize,
+            compute_stats=compute_stats,
+            compact_output=compact_output,
+        )
+
+        # Save data to netCDF file
+        # print(R_["cascade_levels"])
+        R_d[i, :, :, :] = R_["cascade_levels"]
+        means[i, :] = R_["means"]
+        stds[i, :] = R_["stds"]
+
+    # Close the file
+    ncf.close()
+
+
+def compute_store_nwp_motion(
+    precip_nwp,
+    oflow_method,
+    analysis_time,
+    nwp_model,
+    output_path,
+):
+    """Computes, per forecast lead time, the velocity field of an NWP model field.
+
+    Parameters
+    ----------
+    precip_nwp: array-like
+      Array of dimension (n_timesteps, x, y) containing the precipitation forecast
+      from some NWP model.
+    oflow_method: {'constant', 'darts', 'lucaskanade', 'proesmans', 'vet'}, optional
+      An optical flow method from pysteps.motion.get_method.
+    analysis_time: numpy.datetime64
+      The analysis time of the NWP forecast. The analysis time is assumed to be a
+      numpy.datetime64 type as imported by the pysteps importer.
+    nwp_model: str
+      The name of the NWP model.
+    output_path: str, optional
+      The location where to save the file with the NWP velocity fields. Defaults
+      to the path_workdir specified in the rcparams file.
+
+    Returns
+    -------
+    Nothing
+    """
+
+    # Set the output file
+    output_date = datetime.datetime.strftime(
+        pd.to_datetime(analysis_time), "%Y%m%d%H%M%S"
+    )
+    outfn = Path(output_path) / f"cascade_{nwp_model}_{output_date}.npy"
+
+    # Get the velocity field per time step
+    v_nwp = np.zeros((precip_nwp.shape[0], 2, precip_nwp.shape[1], precip_nwp.shape[2]))
+    # Loop through the timesteps. We need two images to construct a motion
+    # field, so we can start from timestep 1.
+    for t in range(1, precip_nwp.shape[0]):
+        v_nwp[t] = oflow_method(precip_nwp[t - 1 : t + 1, :, :])
+
+    # Make timestep 0 the same as timestep 1.
+    v_nwp[0] = v_nwp[1]
+
+    assert v_nwp.ndim == 4, "v_nwp must be a four-dimensional array"
+
+    # Save it as a numpy array
+    np.save(outfn, v_nwp)
+
+
+def load_NWP(input_nc_path_decomp, input_path_velocities, start_time, n_timesteps):
+    """Loads the decomposed NWP and velocity data from the netCDF files
+
+    Parameters
+    ----------
+    input_nc_path_decomp: str
+      Path to the saved netCDF file containing the decomposed NWP data.
+    input_path_velocities: str
+        Path to the saved numpy binary file containing the estimated velocity
+        fields from the NWP data.
+    start_time: numpy.datetime64
+      The start time of the nowcasting. Assumed to be a numpy.datetime64 type
+    n_timesteps: int
+      Number of time steps to forecast
+
+    Returns
+    -------
+    R_d: list
+      A list of dictionaries with each element in the list corresponding to
+      a different time step. Each dictionary has the same structure as the
+      output of the decomposition function
+    uv: array-like
+        Array of shape (timestep,2,m,n) containing the x- and y-components
+      of the advection field for the (NWP) model field per forecast lead time.
+    """
+
+    # Open the file
+    ncf_decomp = netCDF4.Dataset(input_nc_path_decomp, "r", format="NETCDF4")
+    velocities = np.load(input_path_velocities)
+
+    # Initialise the decomposition dictionary
+    decomp_dict = dict()
+    decomp_dict["domain"] = ncf_decomp.domain
+    decomp_dict["normalized"] = bool(ncf_decomp.normalized)
+    decomp_dict["compact_output"] = bool(ncf_decomp.compact_output)
+
+    # Convert the start time and the timestep to datetime64 and timedelta64 type
+    zero_time = np.datetime64("1970-01-01T00:00:00", "ns")
+    analysis_time = np.timedelta64(int(ncf_decomp.analysis_time), "ns") + zero_time
+
+    timestep = ncf_decomp.timestep
+    timestep = np.timedelta64(timestep, "m")
+
+    valid_times = ncf_decomp.variables["valid_times"][:]
+    valid_times = np.array(
+        [np.timedelta64(int(valid_times[i]), "ns") for i in range(len(valid_times))]
+    )
+    valid_times = valid_times + zero_time
+
+    # Find the indices corresponding with the required start and end time
+    start_i = (start_time - analysis_time) // timestep
+    assert analysis_time + start_i * timestep == start_time
+    end_i = start_i + n_timesteps + 1
+
+    # Add the valid times to the output
+    decomp_dict["valid_times"] = valid_times[start_i:end_i]
+
+    # Slice the velocity fields with the start and end indices
+    uv = velocities[start_i:end_i, :, :, :]
+
+    # Initialise the list of dictionaries which will serve as the output (cf: the STEPS function)
+    R_d = list()
+
+    for i in range(start_i, end_i):
+        decomp_dict_ = decomp_dict.copy()
+
+        cascade_levels = ncf_decomp.variables["pr_decomposed"][i, :, :, :]
+
+        # In the netcdf file this is saved as a masked array, so we're checking if there is no mask
+        assert not cascade_levels.mask
+
+        means = ncf_decomp.variables["means"][i, :]
+        assert not means.mask
+
+        stds = ncf_decomp.variables["stds"][i, :]
+        assert not stds.mask
+
+        # Save the values in the dictionary as normal arrays with the filled method
+        decomp_dict_["cascade_levels"] = np.ma.filled(cascade_levels)
+        decomp_dict_["means"] = np.ma.filled(means)
+        decomp_dict_["stds"] = np.ma.filled(stds)
+
+        # Append the output list
+        R_d.append(decomp_dict_)
+
+    return R_d, uv
diff --git a/pysteps/decorators.py b/pysteps/decorators.py
index 7afefe846..6c50a134a 100644
--- a/pysteps/decorators.py
+++ b/pysteps/decorators.py
@@ -16,6 +16,7 @@
 """
 import inspect
 import uuid
+import xarray as xr
 from collections import defaultdict
 from functools import wraps
 
@@ -67,7 +68,11 @@ def _postprocess_import(importer):
         @wraps(importer)
         def _import_with_postprocessing(*args, **kwargs):
 
-            precip, *other_args = importer(*args, **kwargs)
+            data_array = importer(*args, **kwargs)
+
+            if not isinstance(data_array, xr.DataArray):
+                array, _, metadata = data_array
+                data_array = _to_xarray(array, metadata)
 
             _dtype = kwargs.get("dtype", dtype)
 
@@ -78,18 +83,15 @@ def _import_with_postprocessing(*args, **kwargs):
                     "The accepted values are: " + str(accepted_precisions)
                 )
 
-            if isinstance(precip, np.ma.MaskedArray):
-                invalid_mask = np.ma.getmaskarray(precip)
-                precip.data[invalid_mask] = fillna
-            else:
-                # If plain numpy arrays are used, the importers should indicate
-                # the invalid values with np.nan.
-                _fillna = kwargs.get("fillna", fillna)
-                if _fillna is not np.nan:
-                    mask = ~np.isfinite(precip)
-                    precip[mask] = _fillna
+            _fillna = kwargs.get("fillna", fillna)
+            if _fillna is not np.nan:
+                data_array = data_array.fillna(_fillna)
+
+            data_array = data_array.astype(_dtype)
 
-            return (precip.astype(_dtype),) + tuple(other_args)
+            if kwargs.get("legacy", False):
+                return _xarray2legacy(data_array)
+            return data_array
 
         extra_kwargs_doc = """
             Other Parameters
@@ -283,3 +285,85 @@ def _func_with_cache(*args, **kwargs):
         return _func_with_cache
 
     return _memoize
+
+
+def _to_xarray(array, metadata):
+    """Convert to xarray DataArray."""
+    x1, x2 = metadata["x1"], metadata["x2"]
+    y1, y2 = metadata["y1"], metadata["y2"]
+    xsize, ysize = metadata["xpixelsize"], metadata["ypixelsize"]
+    # x_coords = np.arange(x1, x2, xsize) + xsize / 2
+    # y_coords = np.arange(y1, y2, ysize) + ysize / 2
+    x_coords = np.arange(x1, x1 + xsize * array.shape[1], xsize) + xsize / 2
+    y_coords = np.arange(y1, y1 + ysize * array.shape[0], ysize) + ysize / 2
+
+    data_array = xr.DataArray(
+        data=array,
+        dims=("y", "x"),
+        coords=dict(
+            x=("x", x_coords),
+            y=("y", y_coords),
+        ),
+    )
+
+    data_array.attrs.update(
+        {
+            "unit": metadata["unit"],
+            "accutime": metadata["accutime"],
+            "transform": metadata["transform"],
+            "zerovalue": metadata["zerovalue"],
+            "threshold": metadata["threshold"],
+            "zr_a": metadata.get("zr_a", None),
+            "zr_b": metadata.get("zr_b", None),
+            "institution": metadata["institution"],
+            "projection": metadata["projection"],
+            "cartesian_unit": metadata["cartesian_unit"],
+            #
+            # TODO: Remove before final 2.0 version
+            "yorigin": metadata["yorigin"],
+            "xpixelsize": metadata["xpixelsize"],
+            "ypixelsize": metadata["ypixelsize"],
+        }
+    )
+
+    data_array.x.attrs.update(
+        {
+            "standard_name": "projection_x_coordinate",
+            "units": metadata["cartesian_unit"],
+            #
+            # TODO: Remove before final 2.0 version
+            "x1": x1,
+            "x2": x2,
+        }
+    )
+
+    data_array.y.attrs.update(
+        {
+            "standard_name": "projection_y_coordinate",
+            "units": metadata["cartesian_unit"],
+            #
+            # TODO: Remove before final 2.0 version
+            "y1": y1,
+            "y2": y2,
+        }
+    )
+
+    return data_array
+
+
+# TODO: Remove before final 2.0 version
+def _xarray2legacy(data_array):
+    """
+    Convert the new DataArrays to the legacy format used in pysteps v1.*
+    """
+    _array = data_array.values
+
+    metadata = data_array.x.attrs.copy()
+    metadata.update(**data_array.y.attrs)
+    metadata.update(**data_array.attrs)
+
+    if "t" in data_array.coords:
+        print(data_array["t"])
+        metadata["timestamps"] = data_array["t"]
+
+    return _array, None, metadata
diff --git a/pysteps/extrapolation/semilagrangian.py b/pysteps/extrapolation/semilagrangian.py
index d565428ed..425fcfbf9 100644
--- a/pysteps/extrapolation/semilagrangian.py
+++ b/pysteps/extrapolation/semilagrangian.py
@@ -132,6 +132,7 @@ def extrapolate(
     n_iter = kwargs.get("n_iter", 1)
     return_displacement = kwargs.get("return_displacement", False)
     interp_order = kwargs.get("interp_order", 1)
+    map_coordinates_mode = kwargs.get("map_coordinates_mode", "constant")
 
     if precip is None and not return_displacement:
         raise ValueError("precip is None but return_displacement is False")
@@ -223,7 +224,7 @@ def interpolate_motion(displacement, velocity_inc, td):
             precip_warped = ip.map_coordinates(
                 precip,
                 coords_warped,
-                mode="constant",
+                mode=map_coordinates_mode,
                 cval=outval,
                 order=interp_order,
                 prefilter=prefilter,
@@ -233,7 +234,7 @@ def interpolate_motion(displacement, velocity_inc, td):
                 mask_warped = ip.map_coordinates(
                     mask_min,
                     coords_warped,
-                    mode="constant",
+                    mode=map_coordinates_mode,
                     cval=0,
                     order=1,
                     prefilter=False,
@@ -244,7 +245,7 @@ def interpolate_motion(displacement, velocity_inc, td):
                     mask_warped = ip.map_coordinates(
                         mask_finite,
                         coords_warped,
-                        mode="constant",
+                        mode=map_coordinates_mode,
                         cval=0,
                         order=1,
                         prefilter=False,
diff --git a/pysteps/io/__init__.py b/pysteps/io/__init__.py
index 6f5fd1677..b37a2f5d4 100644
--- a/pysteps/io/__init__.py
+++ b/pysteps/io/__init__.py
@@ -8,3 +8,4 @@
 from .importers import *
 from .nowcast_importers import *
 from .readers import *
+from .nwp_importers import *
diff --git a/pysteps/io/importers.py b/pysteps/io/importers.py
index b55319a14..b65f22696 100644
--- a/pysteps/io/importers.py
+++ b/pysteps/io/importers.py
@@ -90,12 +90,12 @@
 from functools import partial
 
 import numpy as np
+import xarray as xr
 from matplotlib.pyplot import imread
 
 from pysteps.decorators import postprocess_import
 from pysteps.exceptions import DataModelError
 from pysteps.exceptions import MissingOptionalDependency
-from pysteps.utils import aggregate_fields
 
 try:
     import gdalconst
@@ -233,7 +233,7 @@ def _get_threshold_value(precip):
 
 
 @postprocess_import(dtype="float32")
-def import_mrms_grib(filename, extent=None, window_size=4, **kwargs):
+def import_mrms_grib(filename, extent=None, **kwargs):
     """
     Importer for NSSL's Multi-Radar/Multi-Sensor System
     ([MRMS](https://www.nssl.noaa.gov/projects/mrms/)) rainrate product
@@ -250,12 +250,6 @@ def import_mrms_grib(filename, extent=None, window_size=4, **kwargs):
     precision, doubling the memory footprint.
     To change the precision of the data, use the *dtype* keyword.
 
-    Also, by default, the original data is downscaled by 4
-    (resulting in a ~4 km grid spacing).
-    In case that the original grid spacing is needed, use `window_size=1`.
-    But be aware that a single composite in double precipitation will
-    require 186 Mb of memory.
-
     Finally, if desired, the precipitation data can be extracted over a
     sub region of the full domain using the `extent` keyword.
     By default, the entire domain is returned.
@@ -281,11 +275,6 @@ def import_mrms_grib(filename, extent=None, window_size=4, **kwargs):
         By default (None), the entire domain is retrieved.
         The extent can be in any form that can be converted to a flat array
         of 4 elements array (e.g., lists or tuples).
-    window_size: array_like or int
-        Array containing down-sampling integer factor along each axis.
-        If an integer value is given, the same block shape is used for all the
-        image dimensions.
-        Default: window_size=4.
 
     {extra_kwargs_doc}
 
@@ -313,9 +302,6 @@ def import_mrms_grib(filename, extent=None, window_size=4, **kwargs):
     except OSError:
         raise OSError(f"Error opening NCEP's MRMS file. " f"File Not Found: {filename}")
 
-    if isinstance(window_size, int):
-        window_size = (window_size, window_size)
-
     if extent is not None:
         extent = np.asarray(extent)
         if (extent.ndim != 1) or (extent.size != 4):
@@ -346,27 +332,6 @@ def import_mrms_grib(filename, extent=None, window_size=4, **kwargs):
     precip = grib_msg.values
     no_data_mask = precip == -3  # Missing values
 
-    # Create a function with default arguments for aggregate_fields
-    block_reduce = partial(aggregate_fields, method="mean", trim=True)
-
-    if window_size != (1, 1):
-        # Downscale data
-        lats = block_reduce(lats, window_size[0])
-        lons = block_reduce(lons, window_size[1])
-
-        # Update the limits
-        ul_lat, lr_lat = lats[0], lats[-1]  # Lat from North to south!
-        ul_lon, lr_lon = lons[0], lons[-1]
-
-        precip[no_data_mask] = 0  # block_reduce does not handle nan values
-        precip = block_reduce(precip, window_size, axis=(0, 1))
-
-        # Consider that if a single invalid observation is located in the block,
-        # then mark that value as invalid.
-        no_data_mask = block_reduce(
-            no_data_mask.astype("int"), window_size, axis=(0, 1)
-        ).astype(bool)
-
     lons, lats = np.meshgrid(lons, lats)
     precip[no_data_mask] = np.nan
 
@@ -392,8 +357,8 @@ def import_mrms_grib(filename, extent=None, window_size=4, **kwargs):
     pr = pyproj.Proj(proj_params)
     proj_def = " ".join([f"+{key}={value} " for key, value in proj_params.items()])
 
-    xsize = grib_msg["iDirectionIncrementInDegrees"] * window_size[0]
-    ysize = grib_msg["jDirectionIncrementInDegrees"] * window_size[1]
+    xsize = grib_msg["iDirectionIncrementInDegrees"]
+    ysize = grib_msg["jDirectionIncrementInDegrees"]
 
     x1, y1 = pr(ul_lon, lr_lat)
     x2, y2 = pr(lr_lon, ul_lat)
@@ -456,6 +421,177 @@ def import_bom_rf3(filename, **kwargs):
     return precip, None, metadata
 
 
+@postprocess_import()
+def import_bom_rf3_xr(filename, **kwargs):
+    """Import a NetCDF radar rainfall product from the BoM Rainfields3
+    using xarray.
+
+    Parameters
+    ----------
+    filename: str
+        Name of the file to import.
+
+    {extra_kwargs_doc}
+
+    Returns
+    -------
+    out_da : xr.DataArray
+        A xarray DataArray containing the rainfall field in mm/h imported
+        from the Bureau RF3 netcdf, the quality field and the metadata. The
+        quality field is currently set to None.
+    """
+
+    if not NETCDF4_IMPORTED:
+        raise MissingOptionalDependency(
+            "netCDF4 package is required to import BoM Rainfields3 products "
+            "but it is not installed"
+        )
+
+    ds = _import_bom_rf3_data_xr(
+        filename,
+        **kwargs,
+    )
+    ds_meta = _import_bom_rf3_geodata_xr(
+        ds,
+        **kwargs,
+    )
+
+    # rename valid_time to t if exists
+    if "valid_time" in ds_meta:
+        ds_meta = ds_meta.rename({"valid_time": "t"})
+
+    return ds_meta.precipitation
+
+
+def _import_bom_rf3_data_xr(
+    filename,
+    **kwargs,
+):
+
+    varname_time = kwargs.get("varname_time", "valid_time")
+    # Tested in python3.6 and chunks did not work properly
+    # commenting next line until find the reason
+    # chunks = kwargs.get('chunks', {varname_time: 1})
+
+    ds_rainfall = xr.open_mfdataset(
+        filename,
+        combine="nested",
+        concat_dim=varname_time,
+        # chunks=chunks,
+        lock=False,
+        parallel=True,
+    )
+
+    return ds_rainfall
+
+
+def _import_bom_rf3_geodata_xr(
+    ds_in,
+    **kwargs,
+):
+
+    # select a default varname if none is passed
+    varname = kwargs.get("varname", "precipitation")
+
+    # extract useful information
+    # projection
+    projdef = None
+    if "proj" in ds_in:
+        projection = ds_in.proj
+        if projection.grid_mapping_name == "albers_conical_equal_area":
+            projdef = "+proj=aea "
+            lon_0 = projection.longitude_of_central_meridian
+            projdef += f" +lon_0={lon_0:.3f}"
+            lat_0 = projection.latitude_of_projection_origin
+            projdef += f" +lat_0={lat_0:.3f}"
+            standard_parallel = projection.standard_parallel
+            projdef += f" +lat_1={standard_parallel[0]:.3f}"
+            projdef += f" +lat_2={standard_parallel[1]:.3f}"
+
+    # get the accumulation period
+    valid_time = None
+    if "valid_time" in ds_in:
+        valid_time = ds_in.valid_time
+
+    start_time = None
+    if "start_time" in ds_in:
+        start_time = ds_in.start_time
+
+    time_step = None
+    if start_time is not None:
+        if valid_time is not None:
+            time_step = (valid_time - start_time).isel(valid_time=0)
+            time_step = time_step.values.astype("timedelta64[m]")
+
+    # get the units of precipitation
+    units = None
+    if "units" in ds_in[varname].attrs:
+        units = ds_in[varname].units
+        if units in ("kg m-2", "mm"):
+            units = "mm"
+            ds_in[varname].attrs.update({"units": units})
+    # get spatial boundaries and pixelsize
+    # move to meters if coordiantes in kilometers
+    if "units" in ds_in.x.attrs:
+        if ds_in.x.units == "km":
+            ds_in["x"] = ds_in.x * 1000.0
+            ds_in.x.attrs.update({"units": "m"})
+            ds_in["y"] = ds_in.y * 1000.0
+            ds_in.y.attrs.update({"units": "m"})
+
+    xmin = ds_in.x.min().values
+    xmax = ds_in.x.max().values
+    ymin = ds_in.y.min().values
+    ymax = ds_in.y.max().values
+    xpixelsize = abs(ds_in.x[1] - ds_in.x[0])
+    ypixelsize = abs(ds_in.y[1] - ds_in.y[0])
+
+    cartesian_unit = ds_in.x.units
+
+    # Add metadata needed by pySTEPS as attrs in X and Y variables
+
+    ds_in.x.attrs.update(
+        {
+            # TODO: Remove before final 2.0 version
+            "x1": xmin,
+            "x2": xmax,
+            "cartesian_unit": cartesian_unit,
+        }
+    )
+
+    ds_in.y.attrs.update(
+        {
+            # TODO: Remove before final 2.0 version
+            "y1": ymin,
+            "y2": ymax,
+            "cartesian_unit": cartesian_unit,
+        }
+    )
+
+    # Add metadata needed by pySTEPS as attrs in rainfall variable
+    da_rainfall = ds_in[varname].isel(valid_time=0)
+
+    ds_in[varname].attrs.update(
+        {
+            "transform": None,
+            "unit": units,  # copy 'units' in 'unit' for legacy reasons
+            "projection": projdef,
+            "accutime": time_step,
+            "zr_a": None,
+            "zr_b": None,
+            "zerovalue": np.nanmin(da_rainfall),
+            "institution": "Commonwealth of Australia, Bureau of Meteorology",
+            "threshold": _get_threshold_value(da_rainfall.values),
+            # TODO(_import_bom_rf3_geodata_xr): Remove before final 2.0 version
+            "yorigin": "upper",
+            "xpixelsize": xpixelsize.values,
+            "ypixelsize": ypixelsize.values,
+        }
+    )
+
+    return ds_in
+
+
 def _import_bom_rf3_data(filename):
     ds_rainfall = netCDF4.Dataset(filename)
     if "precipitation" in ds_rainfall.variables.keys():
@@ -745,7 +881,7 @@ def _import_fmi_pgm_metadata(filename, gzipped=False):
 
 
 @postprocess_import()
-def import_knmi_hdf5(filename, qty="ACRR", accutime=5.0, pixelsize=1.0, **kwargs):
+def import_knmi_hdf5(filename, qty="ACRR", accutime=5.0, pixelsize=1000.0, **kwargs):
     """Import a precipitation or reflectivity field (and optionally the quality
     field) from a HDF5 file conforming to the KNMI Data Centre specification.
 
@@ -762,8 +898,8 @@ def import_knmi_hdf5(filename, qty="ACRR", accutime=5.0, pixelsize=1.0, **kwargs
         is used as default, but hourly, daily and monthly accumulations
         are also available.
     pixelsize: float
-        The pixel size of a raster cell in kilometers. The default value for the
-        KNMI datasets is a 1 km grid cell size, but datasets with 2.4 km pixel
+        The pixel size of a raster cell in meters. The default value for the
+        KNMI datasets is a 1000 m grid cell size, but datasets with 2400 m pixel
         size are also available.
 
     {extra_kwargs_doc}
@@ -841,7 +977,7 @@ def import_knmi_hdf5(filename, qty="ACRR", accutime=5.0, pixelsize=1.0, **kwargs
     # The 'where' group of mch- and Opera-data, is called 'geographic' in the
     # KNMI data.
     geographic = f["geographic"]
-    proj4str = geographic["map_projection"].attrs["projection_proj4_params"].decode()
+    proj4str = "+proj=stere +lat_0=90 +lon_0=0.0 +lat_ts=60.0 +a=6378137 +b=6356752 +x_0=0 +y_0=0"
     pr = pyproj.Proj(proj4str)
     metadata["projection"] = proj4str
 
@@ -872,7 +1008,7 @@ def import_knmi_hdf5(filename, qty="ACRR", accutime=5.0, pixelsize=1.0, **kwargs
     metadata["y2"] = y2
     metadata["xpixelsize"] = pixelsize
     metadata["ypixelsize"] = pixelsize
-    metadata["cartesian_unit"] = "km"
+    metadata["cartesian_unit"] = "m"
     metadata["yorigin"] = "upper"
     metadata["institution"] = "KNMI - Royal Netherlands Meteorological Institute"
     metadata["accutime"] = accutime
@@ -1409,9 +1545,18 @@ def import_odim_hdf5(filename, qty="RATE", **kwargs):
         x2 = ur_x
         y2 = ur_y
 
+    dataset1 = f["dataset1"]
+
     if "xscale" in where.attrs.keys() and "yscale" in where.attrs.keys():
         xpixelsize = where.attrs["xscale"]
         ypixelsize = where.attrs["yscale"]
+    elif (
+        "xscale" in dataset1["where"].attrs.keys()
+        and "yscale" in dataset1["where"].attrs.keys()
+    ):
+        where = dataset1["where"]
+        xpixelsize = where.attrs["xscale"]
+        ypixelsize = where.attrs["yscale"]
     else:
         xpixelsize = None
         ypixelsize = None
@@ -1448,6 +1593,8 @@ def import_odim_hdf5(filename, qty="RATE", **kwargs):
         "threshold": _get_threshold_value(precip),
     }
 
+    metadata.update(kwargs)
+
     f.close()
 
     return precip, quality, metadata
diff --git a/pysteps/io/nwp_importers.py b/pysteps/io/nwp_importers.py
new file mode 100644
index 000000000..ccdbcd0ca
--- /dev/null
+++ b/pysteps/io/nwp_importers.py
@@ -0,0 +1,636 @@
+"""
+pysteps.io.nwp_importers
+====================
+
+Methods for importing files containing two-dimensional NWP mosaics.
+
+The methods in this module implement the following interface::
+
+    nwp_import_xxx(filename, optional arguments)
+
+where **xxx** is the name (or abbreviation) of the file format and filename
+is the name of the input file.
+
+The output of each method is a xarray DataArray containing
+forecast rainfall fields in mm/timestep and the metadata as attributes.
+
+The metadata should contain the following recommended key-value pairs:
+
+.. tabularcolumns:: |p{2cm}|L|
+
++------------------+----------------------------------------------------------+
+|       Key        |                Value                                     |
++==================+==========================================================+
+|   projection     | PROJ.4-compatible projection definition                  |
++------------------+----------------------------------------------------------+
+|   x1             | x-coordinate of the lower-left corner of the data raster |
++------------------+----------------------------------------------------------+
+|   y1             | y-coordinate of the lower-left corner of the data raster |
++------------------+----------------------------------------------------------+
+|   x2             | x-coordinate of the upper-right corner of the data raster|
++------------------+----------------------------------------------------------+
+|   y2             | y-coordinate of the upper-right corner of the data raster|
++------------------+----------------------------------------------------------+
+|   xpixelsize     | grid resolution in x-direction                           |
++------------------+----------------------------------------------------------+
+|   ypixelsize     | grid resolution in y-direction                           |
++------------------+----------------------------------------------------------+
+|   cartesian_unit | the physical unit of the cartesian x- and y-coordinates: |
+|                  | e.g. 'm' or 'km'                                         |
++------------------+----------------------------------------------------------+
+|   yorigin        | a string specifying the location of the first element in |
+|                  | the data raster w.r.t. y-axis:                           |
+|                  | 'upper' = upper border                                   |
+|                  | 'lower' = lower border                                   |
++------------------+----------------------------------------------------------+
+|   institution    | name of the institution who provides the data            |
++------------------+----------------------------------------------------------+
+|   unit           | the physical unit of the data: 'mm/h', 'mm' or 'dBZ'     |
++------------------+----------------------------------------------------------+
+|   transform      | the transformation of the data: None, 'dB', 'Box-Cox' or |
+|                  | others                                                   |
++------------------+----------------------------------------------------------+
+|   accutime       | the accumulation time in minutes of the data, float      |
++------------------+----------------------------------------------------------+
+|   threshold      | the rain/no rain threshold with the same unit,           |
+|                  | transformation and accutime of the data.                 |
++------------------+----------------------------------------------------------+
+|   zerovalue      | the value assigned to the no rain pixels with the same   |
+|                  | unit, transformation and accutime of the data.           |
++------------------+----------------------------------------------------------+
+|   zr_a           | the Z-R constant a in Z = a*R**b                         |
++------------------+----------------------------------------------------------+
+|   zr_b           | the Z-R exponent b in Z = a*R**b                         |
++------------------+----------------------------------------------------------+
+
+Available Importers
+-------------------
+
+.. autosummary::
+    :toctree: ../generated/
+
+    import_bom_nwp_xr
+    import_rmi_nwp_xr
+    import_knmi_nwp_xr
+"""
+
+import numpy as np
+import xarray as xr
+
+from pysteps.decorators import postprocess_import
+from pysteps.exceptions import MissingOptionalDependency
+
+try:
+    import netCDF4
+
+    NETCDF4_IMPORTED = True
+except ImportError:
+    NETCDF4_IMPORTED = False
+
+try:
+    import dask
+
+    DASK_IMPORTED = True
+except ImportError:
+    DASK_IMPORTED = False
+
+
+@postprocess_import()
+def import_bom_nwp_xr(filename, **kwargs):
+    """Import a NetCDF with NWP rainfall forecasts regridded to a BoM Rainfields3
+    using xarray.
+
+    Parameters
+    ----------
+    filename: str
+        Name of the file to import.
+
+    {extra_kwargs_doc}
+
+    Returns
+    -------
+    out_da : xr.DataArray
+        A xarray DataArray containing forecast rainfall fields in mm/timestep
+        imported from a netcdf, and the metadata.
+    """
+
+    if not NETCDF4_IMPORTED:
+        raise MissingOptionalDependency(
+            "netCDF4 package is required to import BoM NWP regridded rainfall "
+            "products but it is not installed"
+        )
+
+    if not DASK_IMPORTED:
+        raise MissingOptionalDependency(
+            "dask package is required to import BoM NWP regridded rainfall "
+            "products but it is not installed"
+        )
+
+    ds = _import_bom_nwp_data_xr(filename, **kwargs)
+    ds_meta = _import_bom_nwp_geodata_xr(ds, **kwargs)
+
+    # rename varname_time (def: time) to t
+    varname_time = kwargs.get("varname_time", "time")
+    ds_meta = ds_meta.rename({varname_time: "t"})
+    varname_time = "t"
+
+    # if data variable is named accum_prcp
+    # it is assumed that NWP rainfall data is accumulated
+    # so it needs to be disagregated by time step
+    varname = kwargs.get("varname", "accum_prcp")
+    if varname == "accum_prcp":
+        print("Rainfall values are accumulated. Disaggregating by time step")
+        accum_prcp = ds_meta[varname]
+        precipitation = accum_prcp - accum_prcp.shift({varname_time: 1})
+        precipitation = precipitation.dropna(varname_time, "all")
+        # update/copy attributes
+        precipitation.name = "precipitation"
+        # copy attributes
+        precipitation.attrs.update({**accum_prcp.attrs})
+        # update attributes
+        precipitation.attrs.update({"standard_name": "precipitation_amount"})
+    else:
+        precipitation = ds_meta[varname]
+
+    return precipitation
+
+
+def _import_bom_nwp_data_xr(filename, **kwargs):
+
+    varname_time = kwargs.get("varname_time", "time")
+    chunks = kwargs.get("chunks", {varname_time: 1})
+
+    ds_rainfall = xr.open_mfdataset(
+        filename,
+        combine="nested",
+        concat_dim=varname_time,
+        chunks=chunks,
+        lock=False,
+        parallel=True,
+    )
+
+    return ds_rainfall
+
+
+def _import_bom_nwp_geodata_xr(
+    ds_in,
+    **kwargs,
+):
+
+    varname = kwargs.get("varname", "accum_prcp")
+    varname_time = kwargs.get("varname_time", "time")
+
+    # extract useful information
+    # projection
+    projdef = None
+    if "proj" in ds_in:
+        projection = ds_in.proj
+        if projection.grid_mapping_name == "albers_conical_equal_area":
+            projdef = "+proj=aea "
+            lon_0 = projection.longitude_of_central_meridian
+            projdef += f" +lon_0={lon_0:.3f}"
+            lat_0 = projection.latitude_of_projection_origin
+            projdef += f" +lat_0={lat_0:.3f}"
+            standard_parallel = projection.standard_parallel
+            projdef += f" +lat_1={standard_parallel[0]:.3f}"
+            projdef += f" +lat_2={standard_parallel[1]:.3f}"
+
+    # get the accumulation period
+    time = ds_in[varname_time]
+    # shift the array to calculate the time step
+    delta_time = time - time.shift({varname_time: 1})
+    # assuming first valid delta_time is representative of all time steps
+    time_step = delta_time[1]
+    time_step = time_step.values.astype("timedelta64[m]").astype(int)
+
+    # get the units of precipitation
+    units = None
+    if "units" in ds_in[varname].attrs:
+        units = ds_in[varname].units
+        if units in ("kg m-2", "mm"):
+            units = "mm"
+            ds_in[varname].attrs.update({"units": units})
+    # get spatial boundaries and pixelsize
+    # move to meters if coordiantes in kilometers
+    if "units" in ds_in.x.attrs:
+        if ds_in.x.units == "km":
+            ds_in["x"] = ds_in.x * 1000.0
+            ds_in.x.attrs.update({"units": "m"})
+            ds_in["y"] = ds_in.y * 1000.0
+            ds_in.y.attrs.update({"units": "m"})
+
+    xmin = ds_in.x.min().values
+    xmax = ds_in.x.max().values
+    ymin = ds_in.y.min().values
+    ymax = ds_in.y.max().values
+    xpixelsize = abs(ds_in.x[1] - ds_in.x[0])
+    ypixelsize = abs(ds_in.y[1] - ds_in.y[0])
+
+    cartesian_unit = ds_in.x.units
+
+    # Add metadata needed by pySTEPS as attrs in X and Y variables
+    ds_in.x.attrs.update(
+        {
+            # TODO: Remove before final 2.0 version
+            "x1": xmin,
+            "x2": xmax,
+            "cartesian_unit": cartesian_unit,
+        }
+    )
+
+    ds_in.y.attrs.update(
+        {
+            # TODO: Remove before final 2.0 version
+            "y1": ymin,
+            "y2": ymax,
+            "cartesian_unit": cartesian_unit,
+        }
+    )
+
+    # Add metadata needed by pySTEPS as attrs in rainfall variable
+    da_rainfall = ds_in[varname].isel({varname_time: 0})
+
+    ds_in[varname].attrs.update(
+        {
+            "transform": None,
+            "unit": units,  # copy 'units' in 'unit' for legacy reasons
+            "projection": projdef,
+            "accutime": time_step,
+            "zr_a": None,
+            "zr_b": None,
+            "zerovalue": np.nanmin(da_rainfall),
+            "institution": "Commonwealth of Australia, Bureau of Meteorology",
+            "threshold": _get_threshold_value(da_rainfall.values),
+            # TODO(_import_bom_rf3_geodata_xr): Remove before final 2.0 version
+            "yorigin": "upper",
+            "xpixelsize": xpixelsize.values,
+            "ypixelsize": ypixelsize.values,
+        }
+    )
+
+    return ds_in
+
+
+@postprocess_import()
+def import_rmi_nwp_xr(filename, **kwargs):
+    """Import a NetCDF with NWP rainfall forecasts from RMI using xarray.
+
+    Parameters
+    ----------
+    filename: str
+        Name of the file to import.
+
+    {extra_kwargs_doc}
+
+    Returns
+    -------
+    out_da : xr.DataArray
+        A xarray DataArray containing forecast rainfall fields in mm/timestep
+        imported from a netcdf, and the metadata.
+    """
+
+    if not NETCDF4_IMPORTED:
+        raise MissingOptionalDependency(
+            "netCDF4 package is required to import RMI NWP rainfall "
+            "products but it is not installed"
+        )
+
+    if not DASK_IMPORTED:
+        raise MissingOptionalDependency(
+            "dask package is required to import BoM NWP regridded rainfall "
+            "products but it is not installed"
+        )
+
+    ds = _import_rmi_nwp_data_xr(filename, **kwargs)
+    ds_meta = _import_rmi_nwp_geodata_xr(ds, **kwargs)
+
+    # rename varname_time (def: time) to t
+    varname_time = kwargs.get("varname_time", "time")
+    ds_meta = ds_meta.rename({varname_time: "t"})
+    varname_time = "t"
+
+    # if data variable is named accum_prcp
+    # it is assumed that NWP rainfall data is accumulated
+    # so it needs to be disagregated by time step
+    varname = kwargs.get("varname", "precipitation")
+    if varname == "accum_prcp":
+        print("Rainfall values are accumulated. Disaggregating by time step")
+        accum_prcp = ds_meta[varname]
+        precipitation = accum_prcp - accum_prcp.shift({varname_time: 1})
+        precipitation = precipitation.dropna(varname_time, "all")
+        # update/copy attributes
+        precipitation.name = "precipitation"
+        # copy attributes
+        precipitation.attrs.update({**accum_prcp.attrs})
+        # update attributes
+        precipitation.attrs.update({"standard_name": "precipitation_amount"})
+    else:
+        precipitation = ds_meta[varname]
+
+    return precipitation
+
+
+def _import_rmi_nwp_data_xr(filename, **kwargs):
+
+    varname_time = kwargs.get("varname_time", "time")
+    chunks = kwargs.get("chunks", {varname_time: 1})
+
+    ds_rainfall = xr.open_mfdataset(
+        filename,
+        combine="nested",
+        concat_dim=varname_time,
+        chunks=chunks,
+        lock=False,
+        parallel=True,
+    )
+
+    return ds_rainfall
+
+
+def _import_rmi_nwp_geodata_xr(
+    ds_in,
+    **kwargs,
+):
+
+    varname = kwargs.get("varname", "precipitation")
+    varname_time = kwargs.get("varname_time", "time")
+    projdef = None
+    if "proj4string" in ds_in.attrs:
+        projdef = ds_in.proj4string
+
+    # get the accumulation period
+    time = ds_in[varname_time]
+    # shift the array to calculate the time step
+    delta_time = time - time.shift({varname_time: 1})
+    # assuming first valid delta_time is representative of all time steps
+    time_step = delta_time[1]
+    time_step = time_step.values.astype("timedelta64[m]").astype(int)
+
+    # get the units of precipitation
+    units = None
+    if "units" in ds_in[varname].attrs:
+        units = ds_in[varname].units
+        if units in ("kg m-2", "mm"):
+            units = "mm"
+            ds_in[varname].attrs.update({"units": units})
+    # get spatial boundaries and pixelsize
+    # move to meters if coordinates in kilometers
+    if "units" in ds_in.x.attrs:
+        if ds_in.x.units == "km":
+            ds_in["x"] = ds_in.x * 1000.0
+            ds_in.x.attrs.update({"units": "m"})
+            ds_in["y"] = ds_in.y * 1000.0
+            ds_in.y.attrs.update({"units": "m"})
+
+    xmin = ds_in.x.min().values
+    xmax = ds_in.x.max().values
+    ymin = ds_in.y.min().values
+    ymax = ds_in.y.max().values
+    xpixelsize = abs(ds_in.x[1] - ds_in.x[0])
+    ypixelsize = abs(ds_in.y[1] - ds_in.y[0])
+
+    x_coords = (
+        np.arange(xmin, xmin + float(xpixelsize) * ds_in.x.shape[0], float(xpixelsize))
+        + float(xpixelsize) / 2
+    )
+    y_coords = (
+        np.arange(ymin, ymin + float(ypixelsize) * ds_in.y.shape[0], float(ypixelsize))
+        + float(ypixelsize) / 2
+    )
+
+    cartesian_unit = ds_in.x.units
+
+    # Assign new coordinates that are in the middle of the cell
+    ds_in = ds_in.assign_coords(x=x_coords, y=y_coords)
+
+    # Add metadata needed by pySTEPS as attrs in X and Y variables
+    ds_in.x.attrs.update(
+        {
+            # TODO: Remove before final 2.0 version
+            "x1": xmin,
+            "x2": xmax,
+            "cartesian_unit": cartesian_unit,
+        }
+    )
+
+    ds_in.y.attrs.update(
+        {
+            # TODO: Remove before final 2.0 version
+            "y1": ymin,
+            "y2": ymax,
+            "cartesian_unit": cartesian_unit,
+        }
+    )
+
+    # Add metadata needed by pySTEPS as attrs in rainfall variable
+    da_rainfall = ds_in[varname].isel({varname_time: 0})
+
+    ds_in[varname].attrs.update(
+        {
+            "transform": None,
+            "unit": units,  # copy 'units' in 'unit' for legacy reasons
+            "projection": projdef,
+            "accutime": time_step,
+            "zr_a": None,
+            "zr_b": None,
+            "zerovalue": np.nanmin(da_rainfall),
+            "institution": "Royal Meteorological Institute of Belgium",
+            "threshold": _get_threshold_value(da_rainfall.values),
+            # TODO(_import_bom_rf3_geodata_xr): Remove before final 2.0 version
+            "yorigin": "upper",
+            "xpixelsize": xpixelsize.values,
+            "ypixelsize": ypixelsize.values,
+        }
+    )
+
+    return ds_in
+
+
+@postprocess_import()
+def import_knmi_nwp_xr(filename, **kwargs):
+    """Import a NetCDF with HARMONIE NWP rainfall forecasts from KNMI using
+    xarray.
+
+    Parameters
+    ----------
+    filename: str
+        Name of the file to import.
+
+    {extra_kwargs_doc}
+
+    Returns
+    -------
+    out_da : xr.DataArray
+        A xarray DataArray containing forecast rainfall fields in mm/timestep
+        imported from a netcdf, and the metadata.
+    """
+
+    if not NETCDF4_IMPORTED:
+        raise MissingOptionalDependency(
+            "netCDF4 package is required to import BoM NWP regrided rainfall "
+            "products but it is not installed"
+        )
+
+    if not DASK_IMPORTED:
+        raise MissingOptionalDependency(
+            "dask package is required to import BoM NWP regridded rainfall "
+            "products but it is not installed"
+        )
+
+    ds = _import_knmi_nwp_data_xr(filename, **kwargs)
+    ds_meta = _import_knmi_nwp_geodata_xr(ds, **kwargs)
+
+    # rename varname_time (def: time) to t
+    varname_time = kwargs.get("varname_time", "time")
+    ds_meta = ds_meta.rename({varname_time: "t"})
+    varname_time = "t"
+
+    # if data variable is named accum_prcp
+    # it is assumed that NWP rainfall data is accumulated
+    # so it needs to be disagregated by time step
+    varname = kwargs.get("varname", "P_fc")
+    if varname == "P_fc":
+        precipitation = ds_meta[varname]
+        # update/copy attributes
+        precipitation.name = "precipitation"
+        # update attributes
+        precipitation.attrs.update({"standard_name": "precipitation_amount"})
+    else:
+        precipitation = ds_meta[varname]
+
+    return precipitation
+
+
+def _import_knmi_nwp_data_xr(filename, **kwargs):
+
+    varname_time = kwargs.get("varname_time", "time")
+    chunks = kwargs.get("chunks", {varname_time: 1})
+
+    ds_rainfall = xr.open_mfdataset(
+        filename,
+        combine="nested",
+        concat_dim=varname_time,
+        chunks=chunks,
+        lock=False,
+        parallel=True,
+    )
+
+    return ds_rainfall
+
+
+def _import_knmi_nwp_geodata_xr(
+    ds_in,
+    **kwargs,
+):
+
+    varname = kwargs.get("varname", "P_fc")
+    varname_time = kwargs.get("varname_time", "time")
+    # Get the projection string
+    projdef = ds_in.crs.proj4_params
+
+    # Get the accumulation period
+    time = ds_in[varname_time]
+    # Shift the array to calculate the time step
+    delta_time = time - time.shift({varname_time: 1})
+    # Assuming first valid delta_time is representative of all time steps
+    time_step = delta_time[1]
+    time_step = time_step.values.astype("timedelta64[m]").astype(int)
+
+    # Get the units of precipitation
+    units = None
+    if "units" in ds_in[varname].attrs:
+        units = ds_in[varname].units
+        if units in ("kg m-2", "mm"):
+            units = "mm"
+            ds_in[varname].attrs.update({"units": units})
+
+    # Get spatial boundaries and pixelsize
+    xmin = ds_in.x.min().values
+    xmax = ds_in.x.max().values
+    ymin = ds_in.y.min().values
+    ymax = ds_in.y.max().values
+    xpixelsize = abs(ds_in.x[1] - ds_in.x[0])
+    ypixelsize = abs(ds_in.y[1] - ds_in.y[0])
+
+    x_coords = (
+        np.arange(xmin, xmin + float(xpixelsize) * ds_in.x.shape[0], float(xpixelsize))
+        + float(xpixelsize) / 2
+    )
+    y_coords = (
+        np.arange(ymin, ymin + float(ypixelsize) * ds_in.y.shape[0], float(ypixelsize))
+        + float(ypixelsize) / 2
+    )
+
+    cartesian_unit_x = ds_in.x.units
+    cartesian_unit_y = ds_in.y.units
+
+    # Assign new coordinates that are in the middle of the cell
+    ds_in = ds_in.assign_coords(x=x_coords, y=y_coords)
+
+    # Add metadata needed by pySTEPS as attrs in X and Y variables
+    ds_in.x.attrs.update(
+        {
+            # TODO: Remove before final 2.0 version
+            "x1": xmin,
+            "x2": xmax,
+            "cartesian_unit": cartesian_unit_x,
+        }
+    )
+
+    ds_in.y.attrs.update(
+        {
+            # TODO: Remove before final 2.0 version
+            "y1": ymin,
+            "y2": ymax,
+            "cartesian_unit": cartesian_unit_y,
+        }
+    )
+
+    # Add metadata needed by pySTEPS as attrs in rainfall variable
+    da_rainfall = ds_in[varname].isel({varname_time: 0})
+    # Set values below 0.0 to 0.0
+    da_rainfall = da_rainfall.where(da_rainfall >= 0.0, 0.0)
+
+    ds_in[varname].attrs.update(
+        {
+            "transform": None,
+            "unit": units,  # copy 'units' in 'unit' for legacy reasons
+            "projection": projdef,
+            "accutime": time_step,
+            "zr_a": None,
+            "zr_b": None,
+            "zerovalue": np.nanmin(da_rainfall),
+            "institution": ds_in.attrs["institution"],
+            "threshold": _get_threshold_value(da_rainfall.values),
+            # TODO(_import_bom_rf3_geodata_xr): Remove before final 2.0 version
+            "yorigin": "lower",
+            "xpixelsize": xpixelsize.values,
+            "ypixelsize": ypixelsize.values,
+        }
+    )
+
+    return ds_in
+
+
+def _get_threshold_value(precip):
+    """
+    Get the rain/no rain threshold with the same unit, transformation and
+    accutime of the data.
+    If all the values are NaNs, the returned value is `np.nan`.
+    Otherwise, np.min(precip[precip > precip.min()]) is returned.
+
+    Returns
+    -------
+    threshold: float
+    """
+    valid_mask = np.isfinite(precip)
+    if valid_mask.any():
+        _precip = precip[valid_mask]
+        min_precip = _precip.min()
+        above_min_mask = _precip > min_precip
+        if above_min_mask.any():
+            return np.min(_precip[above_min_mask])
+        else:
+            return min_precip
+    else:
+        return np.nan
diff --git a/pysteps/io/readers.py b/pysteps/io/readers.py
index a5ef9a661..92599be11 100644
--- a/pysteps/io/readers.py
+++ b/pysteps/io/readers.py
@@ -12,16 +12,19 @@
 """
 
 import numpy as np
+import xarray as xr
 
+from pysteps.decorators import _xarray2legacy
 
-def read_timeseries(inputfns, importer, **kwargs):
+
+def read_timeseries(input_fns, importer, **kwargs):
     """Read a time series of input files using the methods implemented in the
-    :py:mod:`pysteps.io.importers` module and stack them into a 3d array of
-    shape (num_timesteps, height, width).
+    :py:mod:`pysteps.io.importers` module and concatenate them into a 3d array
+    of shape (t, y, x).
 
     Parameters
     ----------
-    inputfns: tuple
+    input_fns: tuple
         Input files returned by a function implemented in the
         :py:mod:`pysteps.io.archive` module.
     importer: function
@@ -31,50 +34,45 @@ def read_timeseries(inputfns, importer, **kwargs):
 
     Returns
     -------
-    out: tuple
-        A three-element tuple containing the read data and quality rasters and
+    out: xr.DataArray
+        A xarray DataArray containing the data rasters and
         associated metadata. If an input file name is None, the corresponding
-        precipitation and quality fields are filled with nan values. If all
-        input file names are None or if the length of the file name list is
-        zero, a three-element tuple containing None values is returned.
-
+        fields are filled with nan values.
+        If all input file names are None or if the length of the file name list
+        is zero, None is returned.
     """
-
+    legacy = kwargs.get("legacy", False)
+    kwargs["legacy"] = False
     # check for missing data
     precip_ref = None
-    if all(ifn is None for ifn in inputfns):
-        return None, None, None
+    if all(ifn is None for ifn in input_fns):
+        return None
     else:
-        if len(inputfns[0]) == 0:
-            return None, None, None
-        for ifn in inputfns[0]:
+        if len(input_fns[0]) == 0:
+            return None
+        for ifn in input_fns[0]:
             if ifn is not None:
-                precip_ref, quality_ref, metadata = importer(ifn, **kwargs)
+                precip_ref = importer(ifn, **kwargs)
                 break
 
     if precip_ref is None:
-        return None, None, None
+        return None
 
     precip = []
-    quality = []
     timestamps = []
-    for i, ifn in enumerate(inputfns[0]):
+    for i, ifn in enumerate(input_fns[0]):
         if ifn is not None:
-            precip_, quality_, _ = importer(ifn, **kwargs)
+            precip_ = importer(ifn, **kwargs)
             precip.append(precip_)
-            quality.append(quality_)
-            timestamps.append(inputfns[1][i])
+            timestamps.append(input_fns[1][i])
         else:
-            precip.append(precip_ref * np.nan)
-            if quality_ref is not None:
-                quality.append(quality_ref * np.nan)
-            else:
-                quality.append(None)
-            timestamps.append(inputfns[1][i])
+            precip.append(xr.full_like(precip_ref, np.nan))
+            timestamps.append(input_fns[1][i])
+
+    precip = xr.concat(precip, "t")
+    precip = precip.assign_coords({"t": timestamps})
 
-    # Replace this with stack?
-    precip = np.concatenate([precip_[None, :, :] for precip_ in precip])
-    # TODO: Q should be organized as R, but this is not trivial as Q_ can be also None or a scalar
-    metadata["timestamps"] = np.array(timestamps)
+    if legacy:
+        return _xarray2legacy(precip)
 
-    return precip, quality, metadata
+    return precip
diff --git a/pysteps/nowcasts/interface.py b/pysteps/nowcasts/interface.py
index a86e6c080..0b7b94bce 100644
--- a/pysteps/nowcasts/interface.py
+++ b/pysteps/nowcasts/interface.py
@@ -32,7 +32,14 @@
 """
 
 from pysteps.extrapolation.interface import eulerian_persistence
-from pysteps.nowcasts import anvil, extrapolation, linda, sprog, steps, sseps
+from pysteps.nowcasts import (
+    anvil,
+    extrapolation,
+    linda,
+    sprog,
+    steps,
+    sseps,
+)
 from pysteps.nowcasts import lagrangian_probability
 
 _nowcast_methods = dict()
diff --git a/pysteps/nowcasts/lagrangian_probability.py b/pysteps/nowcasts/lagrangian_probability.py
index c9889da6b..2bc8a5a08 100644
--- a/pysteps/nowcasts/lagrangian_probability.py
+++ b/pysteps/nowcasts/lagrangian_probability.py
@@ -1,7 +1,7 @@
 # -*- coding: utf-8 -*-
 """
 pysteps.nowcasts.lagrangian_probability
-======================================
+=======================================
 
 Implementation of the local Lagrangian probability nowcasting technique
 described in :cite:`GZ2004`.
diff --git a/pysteps/pystepsrc b/pysteps/pystepsrc
index 7e2b2c9d7..c2b6137a8 100644
--- a/pysteps/pystepsrc
+++ b/pysteps/pystepsrc
@@ -76,8 +76,19 @@
             "importer_kwargs": {
                 "accutime": 5,
                 "qty": "ACRR",
-                "pixelsize": 1.0
+                "pixelsize": 1000.0
 			}
+        },
+		"rmi": {
+            "root_path": "./radar/rmi/radqpe",
+            "path_fmt": "%Y%m%d",
+            "fn_pattern": "%Y%m%d%H%M00.rad.bhbjbwdnfa.comp.rate.qpe2",
+            "fn_ext": "hdf",
+            "importer": "odim_hdf5",
+            "timestep": 5,
+            "importer_kwargs": {
+				"accutime": 5.0
+            }
         },
         "saf": {
             "root_path": "./saf",
@@ -89,6 +100,39 @@
             "importer_kwargs": {
                 "gzipped": true
             }
+        },
+        "bom_nwp": {
+            "root_path": "./nwp/bom",
+            "path_fmt": "%Y/%m/%d",
+            "fn_pattern": "%Y%m%d_%H00_regrid_short",
+            "fn_ext": "nc",
+            "importer": "bom_nwp",
+            "timestep": 10,
+            "importer_kwargs": {
+                "gzipped": true
+    	    }
+        },
+        "rmi_nwp": {
+            "root_path": "./nwp/rmi",
+            "path_fmt": "%Y/%m/%d",
+            "fn_pattern": "ao13_%Y%m%d%H_native_5min",
+            "fn_ext": "nc",
+            "importer": "rmi_nwp",
+            "timestep": 5,
+            "importer_kwargs": {
+                "gzipped": true
+            }
+        },
+        "knmi_nwp": {
+            "root_path": "./nwp/knmi",
+            "path_fmt": "%Y/%m/%d",
+            "fn_pattern": "%Y%m%d_%H00_Pforecast_Harmonie",
+            "fn_ext": "nc",
+            "importer": "knmi_nwp",
+            "timestep": 60,
+            "importer_kwargs": {
+                "gzipped": true
+            }
         }
     }
 }
diff --git a/pysteps/utils/__init__.py b/pysteps/utils/__init__.py
index 214d9a309..9594a75ae 100644
--- a/pysteps/utils/__init__.py
+++ b/pysteps/utils/__init__.py
@@ -11,3 +11,4 @@
 from .spectral import *
 from .tapering import *
 from .transformation import *
+from .reprojection import *
diff --git a/pysteps/utils/interface.py b/pysteps/utils/interface.py
index ee62dd48e..eebf84b1d 100644
--- a/pysteps/utils/interface.py
+++ b/pysteps/utils/interface.py
@@ -18,6 +18,7 @@
 from . import fft
 from . import images
 from . import interpolate
+from . import reprojection
 from . import spectral
 from . import tapering
 from . import transformation
@@ -187,6 +188,9 @@ def donothing(R, metadata=None, *args, **kwargs):
     methods_objects["rbfinterp2d"] = interpolate.rbfinterp2d
     methods_objects["idwinterp2d"] = interpolate.idwinterp2d
 
+    # reprojection methods
+    methods_objects["reprojection"] = reprojection.reprojection
+
     # spectral methods
     methods_objects["rapsd"] = spectral.rapsd
     methods_objects["rm_rdisc"] = spectral.remove_rain_norain_discontinuity
diff --git a/pysteps/utils/reprojection.py b/pysteps/utils/reprojection.py
new file mode 100644
index 000000000..a702aef29
--- /dev/null
+++ b/pysteps/utils/reprojection.py
@@ -0,0 +1,113 @@
+# -*- coding: utf-8 -*-
+"""
+pysteps.utils.reprojection
+==================
+
+Reprojection tool to reproject the grid and adjust the grid cell size of an 
+input field to a destination field.
+
+.. autosummary::
+    :toctree: ../generated/
+
+    reprojection
+"""
+from pysteps.exceptions import MissingOptionalDependency
+import xarray as xr
+import numpy as np
+
+try:
+    from rasterio import Affine as A
+    from rasterio.warp import reproject, Resampling
+
+    RASTERIO_IMPORTED = True
+except ImportError:
+    RASTERIO_IMPORTED = False
+
+
+def reprojection(src_array, dst_array):
+    """Reprojects precipitation fields to the domain of another precipitation
+    field.
+
+    Parameters
+    ----------
+    src_array: xr.DataArray
+        Three-dimensional xarray DataArray with dimensions (t, x, y) containing
+        a time series of precipitation fields. These precipitation fields
+        will be reprojected.
+    dst_array: xr.DataArray
+        Xarray DataArray containing a precipitation field or a time series of
+        precipitation fields. The xarray src_array will be reprojected to the
+        domain of dst_array.
+
+    Returns
+    -------
+    r_rprj: xr.DataArray
+        Three-dimensional xarray DataArray with dimensions (t, x, y) containing
+        the precipitation fields of src_array, but reprojected to the domain of
+        dst_array.
+    """
+
+    if not RASTERIO_IMPORTED:
+        raise MissingOptionalDependency(
+            "rasterio package is required for the reprojection tool, but it is "
+            "not installed"
+        )
+
+    # Extract the grid info from src_array
+    src_crs = src_array.attrs["projection"]
+    x1_src = src_array.x.attrs["x1"]
+    y2_src = src_array.y.attrs["y2"]
+    xpixelsize_src = src_array.attrs["xpixelsize"]
+    ypixelsize_src = src_array.attrs["ypixelsize"]
+    src_transform = A.translation(float(x1_src), float(y2_src)) * A.scale(
+        float(xpixelsize_src), float(-ypixelsize_src)
+    )
+
+    # Extract the grid info from dst_array
+    dst_crs = dst_array.attrs["projection"]
+    x1_dst = dst_array.x.attrs["x1"]
+    y2_dst = dst_array.y.attrs["y2"]
+    xpixelsize_dst = dst_array.attrs["xpixelsize"]
+    ypixelsize_dst = dst_array.attrs["ypixelsize"]
+    dst_transform = A.translation(float(x1_dst), float(y2_dst)) * A.scale(
+        float(xpixelsize_dst), float(-ypixelsize_dst)
+    )
+
+    # Initialise the reprojected (x)array
+    r_rprj = np.zeros((src_array.shape[0], dst_array.shape[-2], dst_array.shape[-1]))
+
+    # For every timestep, reproject the precipitation field of src_array to
+    # the domain of dst_array
+    if src_array.attrs["yorigin"] != dst_array.attrs["yorigin"]:
+        src_array = src_array[:, ::-1, :]
+
+    for i in range(src_array["t"].shape[0]):
+        reproject(
+            src_array.isel(t=i).values,
+            r_rprj[i, :, :],
+            src_transform=src_transform,
+            src_crs=src_crs,
+            dst_transform=dst_transform,
+            dst_crs=dst_crs,
+            resampling=Resampling.nearest,
+            dst_nodata=np.nan,
+        )
+
+    # Assign the necessary attributes from src_array and dst_array to R_rprj
+    r_rprj = xr.DataArray(
+        data=r_rprj,
+        dims=("t", "y", "x"),
+        coords=dict(
+            t=("t", src_array.coords["t"].data),
+            x=("x", dst_array.coords["x"].data),
+            y=("y", dst_array.coords["y"].data),
+        ),
+    )
+
+    r_rprj.attrs.update(src_array.attrs)
+    r_rprj.x.attrs.update(dst_array.x.attrs)
+    r_rprj.y.attrs.update(dst_array.y.attrs)
+    for key in ["projection", "yorigin", "xpixelsize", "ypixelsize"]:
+        r_rprj.attrs[key] = dst_array.attrs[key]
+
+    return r_rprj
diff --git a/requirements.txt b/requirements.txt
index 1804df1d9..b8aea1788 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -7,3 +7,5 @@ matplotlib
 jsmin
 jsonschema
 netCDF4
+xarray
+bottleneck
diff --git a/requirements_dev.txt b/requirements_dev.txt
index bf85f7d86..e1e4a7d44 100644
--- a/requirements_dev.txt
+++ b/requirements_dev.txt
@@ -9,6 +9,8 @@ matplotlib
 jsmin
 jsonschema
 netCDF4
+xarray
+bottleneck
 
 # Optional dependencies
 dask
@@ -17,6 +19,7 @@ cartopy>=0.18
 h5py
 scikit-image
 pandas
+rasterio
 
 # Testing
 pytest
diff --git a/setup.py b/setup.py
index 61d0d19ae..5542775c6 100644
--- a/setup.py
+++ b/setup.py
@@ -66,6 +66,8 @@
     "scipy",
     "matplotlib",
     "jsonschema",
+    "xarray",
+    "bottleneck",
 ]
 
 setup(
diff --git a/tox.ini b/tox.ini
index 207f92e46..880d41939 100644
--- a/tox.ini
+++ b/tox.ini
@@ -36,6 +36,7 @@ conda_deps =
     pyproj
     cartopy
     pygrib
+	rasterio
 conda_channels = conda-forge
 
 setenv =


Test patch:
diff --git a/.github/workflows/test_pysteps.yml b/.github/workflows/test_pysteps.yml
index 41d8c8426..08a5e5061 100644
--- a/.github/workflows/test_pysteps.yml
+++ b/.github/workflows/test_pysteps.yml
@@ -2,15 +2,19 @@ name: Test Pysteps
 
 env:
   MINIMAL_DEPENDENCIES: cython numpy jsmin jsonschema matplotlib netCDF4 opencv pillow pyproj scipy dask
-  OPTIONAL_DEPENDENCIES: pyfftw cartopy h5py PyWavelets pandas scikit-image
+  OPTIONAL_DEPENDENCIES: pyfftw cartopy h5py PyWavelets pandas scikit-image rasterio
   TEST_DEPENDENCIES: pytest pytest-cov
 
 on:
   # Triggers the workflow on push or pull request events but only for the master branch
   push:
-    branches: [ master ]
+    branches:
+      - master
+      - pysteps-v2
   pull_request:
-    branches: [ master ]
+      branches:
+        - master
+        - pysteps-v2
 
 jobs:
   unit_tests:
diff --git a/examples/test_decomposition_NWP.py b/examples/test_decomposition_NWP.py
new file mode 100644
index 000000000..525bdeb66
--- /dev/null
+++ b/examples/test_decomposition_NWP.py
@@ -0,0 +1,71 @@
+import matplotlib.pyplot as plt
+import numpy as np
+
+from datetime import datetime
+from pprint import pprint
+from pysteps import io, nowcasts, rcparams
+from pysteps.motion.lucaskanade import dense_lucaskanade
+from pysteps.postprocessing.ensemblestats import excprob
+from pysteps.utils import conversion, dimension, transformation
+from pysteps.visualization import plot_precip_field
+from pysteps.blending.utils import decompose_NWP
+from pysteps.cascade.bandpass_filters import filter_gaussian
+
+num_cascade_levels = 8
+
+###############################################################################
+# Read precipitation field
+# ------------------------
+#
+# First thing, the sequence of Swiss radar composites is imported, converted and
+# transformed into units of dBR.
+
+
+date = datetime.strptime("201701311200", "%Y%m%d%H%M")
+data_source = "mch"
+
+# Load data source config
+root_path = rcparams.data_sources[data_source]["root_path"]
+path_fmt = rcparams.data_sources[data_source]["path_fmt"]
+fn_pattern = rcparams.data_sources[data_source]["fn_pattern"]
+fn_ext = rcparams.data_sources[data_source]["fn_ext"]
+importer_name = rcparams.data_sources[data_source]["importer"]
+importer_kwargs = rcparams.data_sources[data_source]["importer_kwargs"]
+timestep = rcparams.data_sources[data_source]["timestep"]
+
+# Find the radar files in the archive
+fns = io.find_by_date(
+    date, root_path, path_fmt, fn_pattern, fn_ext, timestep, num_prev_files=12
+)
+
+# Read the data from the archive
+importer = io.get_method(importer_name, "importer")
+R_NWP, _, metadata = io.read_timeseries(fns, importer, legacy=True, **importer_kwargs)
+
+# Convert to rain rate
+R_NWP, metadata = conversion.to_rainrate(R_NWP, metadata)
+
+# Log-transform the data
+R_NWP, metadata = transformation.dB_transform(
+    R_NWP, metadata, threshold=0.1, zerovalue=-15.0
+)
+
+# Fill in the missing data with the threshold value
+R_NWP[~np.isfinite(R_NWP)] = metadata["zerovalue"]
+
+# Find the location to save the NWP files
+NWP_output = rcparams.outputs["path_workdir"]
+
+# Define the start time of the NWP forecast
+analysis_time = metadata["timestamps"][0]
+
+# Decompose the NWP and save to netCDF file
+decompose_NWP(
+    R_NWP,
+    "alaro13_01",
+    analysis_time,
+    5,
+    metadata["timestamps"],
+    num_cascade_levels,
+    NWP_output,
+)
diff --git a/pysteps/tests/helpers.py b/pysteps/tests/helpers.py
index 76a07bf38..86c59b663 100644
--- a/pysteps/tests/helpers.py
+++ b/pysteps/tests/helpers.py
@@ -11,6 +11,7 @@
 
 import pysteps as stp
 from pysteps import io, rcparams
+from pysteps.decorators import _to_xarray, _xarray2legacy
 from pysteps.utils import aggregate_fields_space
 
 _reference_dates = dict()
@@ -30,80 +31,82 @@ def get_precipitation_fields(
     metadata=False,
     upscale=None,
     source="mch",
+    legacy=True,
     log_transform=True,
     clip=None,
     **importer_kwargs,
 ):
     """
-    Get a precipitation field from the archive to be used as reference.
+        Get a precipitation field from the archive to be used as reference.
 
-    Source: bom
-    Reference time: 2018/06/16 10000 UTC
+        Source: bom
+        Reference time: 2018/06/16 10000 UTC
 
-    Source: fmi
-    Reference time: 2016/09/28 1600 UTC
+        Source: fmi
+        Reference time: 2016/09/28 1600 UTC
 
-    Source: knmi
-    Reference time: 2010/08/26 0000 UTC
+        Source: knmi
+        Reference time: 2010/08/26 0000 UTC
 
-    Source: mch
-    Reference time: 2015/05/15 1630 UTC
+        Source: mch
+        Reference time: 2015/05/15 1630 UTC
 
-    Source: opera
-    Reference time: 2018/08/24 1800 UTC
+        Source: opera
+        Reference time: 2018/08/24 1800 UTC
 
-    Source: saf
-    Reference time: 2018/06/01 0700 UTC
+        Source: saf
+        Reference time: 2018/06/01 0700 UTC
 
-    Source: mrms
-    Reference time: 2019/06/10 0000 UTC
+        Source: mrms
+        Reference time: 2019/06/10 0000 UTC
 
-    Parameters
-    ----------
+        Parameters
+        ----------
 
-    num_prev_files: int, optional
-        Number of previous times (files) to return with respect to the
-        reference time.
+        num_prev_files: int, optional
+            Number of previous times (files) to return with respect to the
+            reference time.
 
-    num_next_files: int, optional
-        Number of future times (files) to return with respect to the
-        reference time.
+        num_next_files: int, optional
+            Number of future times (files) to return with respect to the
+            reference time.
 
-    return_raw: bool, optional
-        Do not preprocess the precipitation fields. False by default.
-        The pre-processing steps are: 1) Convert to mm/h,
-        2) Mask invalid values, 3) Log-transform the data [dBR].
+        return_raw: bool, optional
+            Do not preprocess the precipitation fields. False by default.
+            The pre-processing steps are: 1) Convert to mm/h,
+            2) Mask invalid values, 3) Log-transform the data [dBR].
 
-    metadata: bool, optional
-        If True, also return file metadata.
+    <<<<<<< HEAD
+    =======
+        metadata: bool, optional
+            If True, also return file metadata.
 
-    clip: scalars (left, right, bottom, top), optional
-        The extent of the bounding box in data coordinates to be used to clip
-        the data.
+        clip: scalars (left, right, bottom, top), optional
+            The extent of the bounding box in data coordinates to be used to clip
+            the data.
 
-    upscale: float or None, optional
-        Upscale fields in space during the pre-processing steps.
-        If it is None, the precipitation field is not modified.
-        If it is a float, represents the length of the space window that is
-        used to upscale the fields.
+    >>>>>>> master
+        upscale: float or None, optional
+            Upscale fields in space during the pre-processing steps.
+            If it is None, the precipitation field is not modified.
+            If it is a float, represents the length of the space window that is
+            used to upscale the fields.
 
-    source: {"bom", "fmi" , "knmi", "mch", "opera", "saf", "mrms"}, optional
-        Name of the data source to be used.
+        source: {"bom", "fmi" , "knmi", "mch", "opera", "saf", "mrms"}, optional
+            Name of the data source to be used.
 
-    log_transform: bool
-        Whether to transform the output to dB.
+        log_transform: bool
+            Whether to transform the output to dB.
 
-    Other Parameters
-    ----------------
+        Other Parameters
+        ----------------
 
-    importer_kwargs : dict
-        Additional keyword arguments passed to the importer.
+        importer_kwargs : dict
+            Additional keyword arguments passed to the importer.
 
-    Returns
-    -------
-    reference_field : array
-
-    metadata : dict
+        Returns
+        -------
+        data_array : xr.DataArray
     """
 
     if source == "bom":
@@ -161,9 +164,9 @@ def get_precipitation_fields(
     # Read the radar composites
     importer = io.get_method(importer_name, "importer")
 
-    reference_field, __, ref_metadata = io.read_timeseries(
-        fns, importer, **_importer_kwargs
-    )
+    reference_field = io.read_timeseries(fns, importer, **_importer_kwargs)
+
+    reference_field, _, ref_metadata = _xarray2legacy(reference_field)
 
     if not return_raw:
 
@@ -199,10 +202,13 @@ def get_precipitation_fields(
             np.ma.set_fill_value(reference_field, -15.0)
             reference_field.data[reference_field.mask] = -15.0
 
-    if metadata:
-        return reference_field, ref_metadata
+    if legacy or metadata:
+        if metadata:
+            return reference_field, ref_metadata
+        else:
+            return reference_field
 
-    return reference_field
+    return _to_xarray(reference_field, ref_metadata)
 
 
 def smart_assert(actual_value, expected, tolerance=None):
diff --git a/pysteps/tests/test_blending_clim.py b/pysteps/tests/test_blending_clim.py
new file mode 100644
index 000000000..1e1fede1a
--- /dev/null
+++ b/pysteps/tests/test_blending_clim.py
@@ -0,0 +1,161 @@
+# -*- coding: utf-8 -*-
+
+
+from datetime import datetime, timedelta
+from os.path import join, exists
+import pickle
+import random
+
+import numpy as np
+from numpy.testing import assert_array_equal
+import pytest
+
+from pysteps.blending.clim import save_skill, calc_clim_skill
+
+
+random.seed(12356)
+n_cascade_levels = 7
+model_names = ["alaro13", "arome13"]
+default_start_skill = [0.8, 0.5]
+""" Helper functions """
+
+
+def generate_random_skill(n_cascade_levels, n_models=1):
+    """
+    Generate random skill which decay exponentially with scale.
+    """
+    start_skill = np.array([random.uniform(0.5, 0.99) for i in range(n_models)])
+    powers = np.arange(1, n_cascade_levels + 1)
+    return pow(start_skill[:, np.newaxis], powers)
+
+
+def generate_fixed_skill(n_cascade_levels, n_models=1):
+    """
+    Generate skill starting at default_start_skill which decay exponentially with scale.
+    """
+    start_skill = np.resize(default_start_skill, n_models)
+    powers = np.arange(1, n_cascade_levels + 1)
+    return pow(start_skill[:, np.newaxis], powers)
+
+
+""" Test arguments """
+
+clim_arg_names = ("startdatestr", "enddatestr", "n_models", "expected_skill_today")
+
+test_enddates = ["20210701235500", "20210702000000", "20200930235500"]
+
+clim_arg_values = [
+    (
+        "20210701230000",
+        "20210701235500",
+        1,
+        {
+            "mean_skill": generate_fixed_skill(n_cascade_levels),
+            "n": 12,
+            "last_validtime": datetime.strptime(test_enddates[0], "%Y%m%d%H%M%S"),
+        },
+    ),
+    (
+        "20210701235500",
+        "20210702000000",
+        1,
+        {
+            "mean_skill": generate_fixed_skill(n_cascade_levels),
+            "n": 1,
+            "last_validtime": datetime.strptime(test_enddates[1], "%Y%m%d%H%M%S"),
+        },
+    ),
+    (
+        "20200801000000",
+        "20200930235500",
+        1,
+        {
+            "mean_skill": generate_fixed_skill(n_cascade_levels),
+            "n": 288,
+            "last_validtime": datetime.strptime(test_enddates[2], "%Y%m%d%H%M%S"),
+        },
+    ),
+    (
+        "20210701230000",
+        "20210701235500",
+        2,
+        {
+            "mean_skill": generate_fixed_skill(n_cascade_levels, 2),
+            "n": 12,
+            "last_validtime": datetime.strptime(test_enddates[0], "%Y%m%d%H%M%S"),
+        },
+    ),
+    (
+        "20210701230000",
+        "20210702000000",
+        2,
+        {
+            "mean_skill": generate_fixed_skill(n_cascade_levels, 2),
+            "n": 1,
+            "last_validtime": datetime.strptime(test_enddates[1], "%Y%m%d%H%M%S"),
+        },
+    ),
+    (
+        "20200801000000",
+        "20200930235500",
+        2,
+        {
+            "mean_skill": generate_fixed_skill(n_cascade_levels, 2),
+            "n": 288,
+            "last_validtime": datetime.strptime(test_enddates[2], "%Y%m%d%H%M%S"),
+        },
+    ),
+]
+
+
+@pytest.mark.parametrize(clim_arg_names, clim_arg_values)
+def test_save_skill(startdatestr, enddatestr, n_models, expected_skill_today, tmpdir):
+    """Test if the skill are saved correctly and the daily average is computed"""
+
+    # get validtime
+    currentdate = datetime.strptime(startdatestr, "%Y%m%d%H%M%S")
+    enddate = datetime.strptime(enddatestr, "%Y%m%d%H%M%S")
+    timestep = timedelta(minutes=5)
+
+    outdir_path = tmpdir
+
+    while currentdate <= enddate:
+        current_skill = generate_fixed_skill(n_cascade_levels, n_models)
+        print("Saving skill: ", current_skill, currentdate, outdir_path)
+        save_skill(
+            current_skill, currentdate, outdir_path, n_models=n_models, window_length=2
+        )
+        currentdate += timestep
+
+    skill_today_file = join(outdir_path, "NWP_skill_today.pkl")
+    assert exists(skill_today_file)
+    with open(skill_today_file, "rb") as f:
+        skill_today = pickle.load(f)
+
+    # Check type
+    assert type(skill_today) == type({})
+    assert "mean_skill" in skill_today
+    assert "n" in skill_today
+    assert "last_validtime" in skill_today
+    assert_array_equal(skill_today["mean_skill"], expected_skill_today["mean_skill"])
+    assert skill_today["n"] == expected_skill_today["n"]
+    assert skill_today["last_validtime"] == expected_skill_today["last_validtime"]
+
+    # Finally, check if the clim skill calculation returns an array of values
+    clim_skill = calc_clim_skill(
+        outdir_path=tmpdir,
+        n_cascade_levels=n_cascade_levels,
+        n_models=n_models,
+        window_length=2,
+    )
+
+    assert clim_skill.shape[0] == n_models
+    assert clim_skill.shape[1] == n_cascade_levels
+
+
+if __name__ == "__main__":
+    save_skill(
+        generate_fixed_skill(n_cascade_levels, 1),
+        datetime.strptime("20200801000000", "%Y%m%d%H%M%S"),
+        "/tmp/",
+    )
diff --git a/pysteps/tests/test_blending_linear_blending.py b/pysteps/tests/test_blending_linear_blending.py
new file mode 100644
index 000000000..712963eba
--- /dev/null
+++ b/pysteps/tests/test_blending_linear_blending.py
@@ -0,0 +1,109 @@
+# -*- coding: utf-8 -*-
+
+import numpy as np
+import pytest
+from pysteps.blending.linear_blending import forecast
+from numpy.testing import assert_array_almost_equal
+from pysteps.utils import transformation
+
+# Test function arguments
+linear_arg_values = [
+    (5, 30, 60, 20, 45, "eulerian", None),
+    (4, 23, 33, 9, 28, "eulerian", None),
+    (3, 18, 36, 13, 27, "eulerian", None),
+    (7, 30, 68, 11, 49, "eulerian", None),
+    (10, 100, 160, 25, 130, "eulerian", None),
+    (6, 60, 180, 22, 120, "eulerian", None),
+    (5, 100, 200, 40, 150, "eulerian", None),
+    (5, 30, 60, 20, 45, "extrapolation", np.zeros((2, 200, 200))),
+    (4, 23, 33, 9, 28, "extrapolation", np.zeros((2, 200, 200))),
+    (3, 18, 36, 13, 27, "extrapolation", np.zeros((2, 200, 200))),
+    (7, 30, 68, 11, 49, "extrapolation", np.zeros((2, 200, 200))),
+    (10, 100, 160, 25, 130, "extrapolation", np.zeros((2, 200, 200))),
+    (6, 60, 180, 22, 120, "extrapolation", np.zeros((2, 200, 200))),
+    (5, 100, 200, 40, 150, "extrapolation", np.zeros((2, 200, 200))),
+]
+
+
+@pytest.mark.parametrize(
+    "timestep, start_blending, end_blending, n_timesteps, controltime, nowcast_method, V",
+    linear_arg_values,
+)
+def test_linear_blending(
+    timestep, start_blending, end_blending, n_timesteps, controltime, nowcast_method, V
+):
+    """Tests if the linear blending function is correct. For the nowcast data a precipitation field
+    which is constant over time is taken. One half of the field has no rain and the other half
+    has a set value. For the NWP data a similar field is taken, the only difference
+    being that now the other half of the field is zero. The blended field should have a
+    constant value over the entire field at the timestep right in the middle between the start
+    of the blending and the end of the blending. This assertion is checked to see if the
+    linear blending function works well."""
+
+    # The argument controltime gives the timestep at which the field is assumed to be
+    # entirely constant
+
+    # Assert that the control time step is in the range of the forecasted time steps
+    assert controltime <= (
+        n_timesteps * timestep
+    ), "Control time needs to be within reach of forecasts, controltime = {} and n_timesteps = {}".format(
+        controltime, n_timesteps
+    )
+
+    # Assert that the start time of the blending comes before the end time of the blending
+    assert (
+        start_blending < end_blending
+    ), "Start time of blending needs to be smaller than end time of blending"
+
+    # Assert that the control time is a multiple of the time step
+    assert (
+        not controltime % timestep
+    ), "Control time needs to be a multiple of the time step"
+
+    # Initialise dummy NWP data
+    r_nwp = np.zeros((n_timesteps, 200, 200))
+
+    for i in range(100):
+        r_nwp[:, i, :] = 11.0
+
+    # Define nowcast input data
+    r_input = np.zeros((200, 200))
+
+    for i in range(100, 200):
+        r_input[i, :] = 11.0
+
+    # Transform from mm/h to dB
+    r_input, _ = transformation.dB_transform(
+        r_input, None, threshold=0.1, zerovalue=-15.0
+    )
+
+    # Calculate the blended field
+    r_blended = forecast(
+        r_input,
+        dict({"unit": "mm/h", "transform": "dB"}),
+        V,
+        n_timesteps,
+        timestep,
+        nowcast_method,
+        r_nwp,
+        dict({"unit": "mm/h", "transform": None}),
+        start_blending=start_blending,
+        end_blending=end_blending,
+    )
+
+    # Assert that the blended field has the expected dimension
+    assert r_blended.shape == (
+        n_timesteps,
+        200,
+        200,
+    ), "The shape of the blended array does not have the expected value. The shape is {}".format(
+        r_blended.shape
+    )
+
+    # Assert that the blended field at the control time step is equal to
+    # a constant field with the expected value.
+    assert_array_almost_equal(
+        r_blended[controltime // timestep - 1],
+        np.ones((200, 200)) * 5.5,
+        err_msg="The blended array does not have the expected value",
+    )
diff --git a/pysteps/tests/test_blending_skill_scores.py b/pysteps/tests/test_blending_skill_scores.py
new file mode 100644
index 000000000..63c948fee
--- /dev/null
+++ b/pysteps/tests/test_blending_skill_scores.py
@@ -0,0 +1,295 @@
+# -*- coding: utf-8 -*-
+
+import numpy as np
+import pytest
+from numpy.testing import assert_array_almost_equal
+
+from pysteps.blending.skill_scores import (
+    spatial_correlation,
+    lt_dependent_cor_nwp,
+    lt_dependent_cor_extrapolation,
+    clim_regr_values,
+)
+
+# TODO: Fix tests for xarray fields
+
+# Set the climatological correlations values
+clim_cor_values_8lev = np.array(
+    [0.848, 0.537, 0.237, 0.065, 0.020, 0.0044, 0.0052, 0.0040]
+)
+clim_cor_values_6lev = np.array([0.848, 0.537, 0.237, 0.065, 0.020, 0.0044])
+clim_cor_values_9lev = np.array(
+    [0.848, 0.537, 0.237, 0.065, 0.020, 0.0044, 0.0052, 0.0040, 1e-4]
+)
+
+# Set the regression values
+regr_pars_8lev = np.array(
+    [
+        [130.0, 165.0, 120.0, 55.0, 50.0, 15.0, 15.0, 10.0],
+        [155.0, 220.0, 200.0, 75.0, 10e4, 10e4, 10e4, 10e4],
+    ]
+)
+regr_pars_6lev = np.array(
+    [
+        [130.0, 165.0, 120.0, 55.0, 50.0, 15.0],
+        [155.0, 220.0, 200.0, 75.0, 10e4, 10e4],
+    ]
+)
+regr_pars_9lev = np.array(
+    [
+        [130.0, 165.0, 120.0, 55.0, 50.0, 15.0, 15.0, 10.0, 10.0],
+        [155.0, 220.0, 200.0, 75.0, 10e4, 10e4, 10e4, 10e4, 10e4],
+    ]
+)
+
+# Set the dummy observation and model values
+dummy_2d_array = np.array([[1.0, 2.0], [3.0, 4.0]])
+obs_8lev = np.repeat(dummy_2d_array[None, :, :], 8, axis=0)
+obs_6lev = np.repeat(dummy_2d_array[None, :, :], 6, axis=0)
+obs_9lev = np.repeat(dummy_2d_array[None, :, :], 9, axis=0)
+mod_8lev = np.repeat(dummy_2d_array[None, :, :], 8, axis=0)
+mod_6lev = np.repeat(dummy_2d_array[None, :, :], 6, axis=0)
+mod_9lev = np.repeat(dummy_2d_array[None, :, :], 9, axis=0)
+
+# Gives some dummy values to PHI
+dummy_phi = np.array([0.472650, 0.523825, 0.103454])
+PHI_8lev = np.repeat(dummy_phi[None, :], 8, axis=0)
+PHI_6lev = np.repeat(dummy_phi[None, :], 6, axis=0)
+PHI_9lev = np.repeat(dummy_phi[None, :], 9, axis=0)
+
+# Test function arguments
+skill_scores_arg_names = (
+    "obs",
+    "mod",
+    "lt",
+    "PHI",
+    "cor_prev",
+    "clim_cor_values",
+    "regr_pars",
+    "n_cascade_levels",
+    "expected_cor_t0",
+    "expected_cor_nwp_lt",
+    "expected_cor_nowcast_lt",
+)
+
+# Test function values
+skill_scores_arg_values = [
+    (
+        obs_8lev,
+        mod_8lev,
+        60,
+        PHI_8lev,
+        None,
+        clim_cor_values_8lev,
+        regr_pars_8lev,
+        8,
+        np.repeat(1.0, 8),
+        np.array(
+            [
+                0.97455941,
+                0.9356775,
+                0.81972779,
+                0.55202975,
+                0.31534738,
+                0.02264599,
+                0.02343133,
+                0.00647032,
+            ]
+        ),
+        np.array(
+            [
+                0.996475,
+                0.996475,
+                0.996475,
+                0.996475,
+                0.996475,
+                0.996475,
+                0.996475,
+                0.996475,
+            ]
+        ),
+    ),
+    (
+        obs_6lev,
+        mod_6lev,
+        60,
+        PHI_6lev,
+        None,
+        clim_cor_values_6lev,
+        regr_pars_6lev,
+        6,
+        np.repeat(1.0, 6),
+        np.array(
+            [0.97455941, 0.9356775, 0.81972779, 0.55202975, 0.31534738, 0.02264599]
+        ),
+        np.array([0.996475, 0.996475, 0.996475, 0.996475, 0.996475, 0.996475]),
+    ),
+    (
+        obs_9lev,
+        mod_9lev,
+        60,
+        PHI_9lev,
+        None,
+        clim_cor_values_9lev,
+        regr_pars_9lev,
+        9,
+        np.repeat(1.0, 9),
+        np.array(
+            [
+                0.97455941,
+                0.9356775,
+                0.81972779,
+                0.55202975,
+                0.31534738,
+                0.02264599,
+                0.02343133,
+                0.00647032,
+                0.00347776,
+            ]
+        ),
+        np.array(
+            [
+                0.996475,
+                0.996475,
+                0.996475,
+                0.996475,
+                0.996475,
+                0.996475,
+                0.996475,
+                0.996475,
+                0.996475,
+            ]
+        ),
+    ),
+    (
+        obs_8lev,
+        mod_8lev,
+        0,
+        PHI_8lev,
+        None,
+        clim_cor_values_8lev,
+        regr_pars_8lev,
+        8,
+        np.repeat(1.0, 8),
+        np.repeat(1.0, 8),
+        np.array(
+            [
+                0.996475,
+                0.996475,
+                0.996475,
+                0.996475,
+                0.996475,
+                0.996475,
+                0.996475,
+                0.996475,
+            ]
+        ),
+    ),
+]
+
+
+# The test
+@pytest.mark.parametrize(skill_scores_arg_names, skill_scores_arg_values)
+
+# The test function to be used
+def test_blending_skill_scores(
+    obs,
+    mod,
+    lt,
+    PHI,
+    cor_prev,
+    clim_cor_values,
+    regr_pars,
+    n_cascade_levels,
+    expected_cor_t0,
+    expected_cor_nwp_lt,
+    expected_cor_nowcast_lt,
+):
+    """Tests if the skill_score functions behave correctly. A dummy gridded
+    model and observation field should be given for n_cascade_levels, which
+    leads to a given spatial correlation per cascade level. Then, the function
+    tests if the correlation regresses towards the climatological values given
+    lead time lt for the NWP fields or given the PHI-values for the
+    extrapolation field.
+
+    """
+    domain_mask = np.full(obs[0, :, :].shape, False, dtype=bool)
+
+    # Calculate the spatial correlation of the given model field
+    correlations_t0 = np.array(spatial_correlation(obs, mod, domain_mask))
+
+    # Check if the field has the same number of cascade levels as the model
+    # field and as the given n_cascade_levels
+    assert (
+        correlations_t0.shape[0] == mod.shape[0]
+    ), "Number of cascade levels should be the same as in the model field"
+    assert (
+        correlations_t0.shape[0] == n_cascade_levels
+    ), "Number of cascade levels should be the same as n_cascade_levels"
+
+    # Check if the returned values are as expected
+    assert_array_almost_equal(
+        correlations_t0,
+        expected_cor_t0,
+        decimal=3,
+        err_msg="Returned spatial correlation is not the same as the expected value",
+    )
+
+    # Test if the NWP correlation regresses towards the correct value given
+    # a lead time in minutes
+    # First, check if the climatological values are returned correctly
+    correlations_clim, regr_clim = clim_regr_values(
+        n_cascade_levels=n_cascade_levels, outdir_path="./tmp/"
+    )
+    assert (
+        correlations_clim.shape[0] == n_cascade_levels
+    ), "Number of cascade levels should be the same as n_cascade_levels"
+    assert_array_almost_equal(
+        correlations_clim,
+        clim_cor_values,
+        decimal=3,
+        err_msg="Not the correct climatological correlations were returned",
+    )
+    assert_array_almost_equal(
+        regr_clim,
+        regr_pars,
+        decimal=3,
+        err_msg="Not the correct regression parameters were returned",
+    )
+
+    # Then, check the regression of the correlation values
+    correlations_nwp_lt = lt_dependent_cor_nwp(
+        lt=lt, correlations=correlations_t0, outdir_path="./tmp/"
+    )
+    assert (
+        correlations_nwp_lt.shape[0] == mod.shape[0]
+    ), "Number of cascade levels should be the same as in the model field"
+    assert (
+        correlations_nwp_lt.shape[0] == n_cascade_levels
+    ), "Number of cascade levels should be the same as n_cascade_levels"
+    assert_array_almost_equal(
+        correlations_nwp_lt,
+        expected_cor_nwp_lt,
+        decimal=3,
+        err_msg="Correlations of NWP not equal to the expected correlations",
+    )
+
+    # Finally, make sure nowcast correlation regresses towards the correct
+    # value given some PHI-values.
+    correlations_nowcast_lt, __ = lt_dependent_cor_extrapolation(
+        PHI, correlations_t0, cor_prev
+    )
+
+    print(correlations_nowcast_lt)
+    assert (
+        correlations_nowcast_lt.shape[0] == mod.shape[0]
+    ), "Number of cascade levels should be the same as in the model field"
+    assert (
+        correlations_nowcast_lt.shape[0] == n_cascade_levels
+    ), "Number of cascade levels should be the same as n_cascade_levels"
+    assert_array_almost_equal(
+        correlations_nowcast_lt,
+        expected_cor_nowcast_lt,
+        decimal=3,
+        err_msg="Correlations of nowcast not equal to the expected correlations",
+    )
diff --git a/pysteps/tests/test_blending_steps.py b/pysteps/tests/test_blending_steps.py
new file mode 100644
index 000000000..5c90ed9ef
--- /dev/null
+++ b/pysteps/tests/test_blending_steps.py
@@ -0,0 +1,274 @@
+# -*- coding: utf-8 -*-
+
+import os
+import numpy as np
+import datetime
+import pytest
+import pysteps
+from pysteps import cascade, blending
+
+
+# TODO: Fix tests for xarray fields?
+
+steps_arg_names = (
+    "n_models",
+    "n_timesteps",
+    "n_ens_members",
+    "n_cascade_levels",
+    "mask_method",
+    "probmatching_method",
+    "blend_nwp_members",
+    "weights_method",
+    "decomposed_nwp",
+    "expected_n_ens_members",
+)
+
+steps_arg_values = [
+    (1, 3, 4, 8, None, None, False, "spn", True, 4),
+    (1, 3, 4, 8, "obs", None, False, "spn", True, 4),
+    (1, 3, 4, 8, "incremental", None, False, "spn", True, 4),
+    (1, 3, 4, 8, None, "mean", False, "spn", True, 4),
+    (1, 3, 4, 8, None, "cdf", False, "spn", True, 4),
+    (1, 3, 4, 8, "incremental", "cdf", False, "spn", True, 4),
+    (1, 3, 4, 6, "incremental", "cdf", False, "bps", True, 4),
+    (1, 3, 4, 6, "incremental", "cdf", False, "bps", False, 4),
+    (1, 3, 4, 9, "incremental", "cdf", False, "spn", True, 4),
+    (2, 3, 10, 8, "incremental", "cdf", False, "spn", True, 10),
+    (5, 3, 4, 8, "incremental", "cdf", False, "spn", True, 5),
+    (1, 10, 1, 8, "incremental", "cdf", False, "spn", True, 1),
+    (5, 3, 2, 8, "incremental", "cdf", True, "spn", True, 2),
+]
+
+
+@pytest.mark.parametrize(steps_arg_names, steps_arg_values)
+def test_steps_blending(
+    n_models,
+    n_timesteps,
+    n_ens_members,
+    n_cascade_levels,
+    mask_method,
+    probmatching_method,
+    blend_nwp_members,
+    weights_method,
+    decomposed_nwp,
+    expected_n_ens_members,
+):
+    ###
+    # The input data
+    ###
+    # Initialise dummy NWP data
+    R_NWP = np.zeros((n_models, n_timesteps + 1, 200, 200))
+
+    for n_model in range(n_models):
+        for i in range(R_NWP.shape[1]):
+            R_NWP[n_model, i, 30:185, 30 + 1 * (i + 1) * n_model] = 0.1
+            R_NWP[n_model, i, 30:185, 31 + 1 * (i + 1) * n_model] = 0.1
+            R_NWP[n_model, i, 30:185, 32 + 1 * (i + 1) * n_model] = 1.0
+            R_NWP[n_model, i, 30:185, 33 + 1 * (i + 1) * n_model] = 5.0
+            R_NWP[n_model, i, 30:185, 34 + 1 * (i + 1) * n_model] = 5.0
+            R_NWP[n_model, i, 30:185, 35 + 1 * (i + 1) * n_model] = 4.5
+            R_NWP[n_model, i, 30:185, 36 + 1 * (i + 1) * n_model] = 4.5
+            R_NWP[n_model, i, 30:185, 37 + 1 * (i + 1) * n_model] = 4.0
+            R_NWP[n_model, i, 30:185, 38 + 1 * (i + 1) * n_model] = 2.0
+            R_NWP[n_model, i, 30:185, 39 + 1 * (i + 1) * n_model] = 1.0
+            R_NWP[n_model, i, 30:185, 40 + 1 * (i + 1) * n_model] = 0.5
+            R_NWP[n_model, i, 30:185, 41 + 1 * (i + 1) * n_model] = 0.1
+
+    # Define dummy nowcast input data
+    R_input = np.zeros((3, 200, 200))
+
+    for i in range(2):
+        R_input[i, 5:150, 30 + 1 * i] = 0.1
+        R_input[i, 5:150, 31 + 1 * i] = 0.5
+        R_input[i, 5:150, 32 + 1 * i] = 0.5
+        R_input[i, 5:150, 33 + 1 * i] = 5.0
+        R_input[i, 5:150, 34 + 1 * i] = 5.0
+        R_input[i, 5:150, 35 + 1 * i] = 4.5
+        R_input[i, 5:150, 36 + 1 * i] = 4.5
+        R_input[i, 5:150, 37 + 1 * i] = 4.0
+        R_input[i, 5:150, 38 + 1 * i] = 1.0
+        R_input[i, 5:150, 39 + 1 * i] = 0.5
+        R_input[i, 5:150, 40 + 1 * i] = 0.5
+        R_input[i, 5:150, 41 + 1 * i] = 0.1
+    R_input[2, 30:155, 30 + 1 * 2] = 0.1
+    R_input[2, 30:155, 31 + 1 * 2] = 0.1
+    R_input[2, 30:155, 32 + 1 * 2] = 1.0
+    R_input[2, 30:155, 33 + 1 * 2] = 5.0
+    R_input[2, 30:155, 34 + 1 * 2] = 5.0
+    R_input[2, 30:155, 35 + 1 * 2] = 4.5
+    R_input[2, 30:155, 36 + 1 * 2] = 4.5
+    R_input[2, 30:155, 37 + 1 * 2] = 4.0
+    R_input[2, 30:155, 38 + 1 * 2] = 2.0
+    R_input[2, 30:155, 39 + 1 * 2] = 1.0
+    R_input[2, 30:155, 40 + 1 * 3] = 0.5
+    R_input[2, 30:155, 41 + 1 * 3] = 0.1
+
+    metadata = dict()
+    metadata["unit"] = "mm"
+    metadata["transformation"] = "dB"
+    metadata["accutime"] = 5.0
+    metadata["transform"] = "dB"
+    metadata["zerovalue"] = 0.0
+    metadata["threshold"] = 0.01
+    metadata["zr_a"] = 200.0
+    metadata["zr_b"] = 1.6
+
+    # Also set the outdir_path and clim_kwargs
+    outdir_path_skill = "./tmp/"
+    clim_kwargs = dict({"n_models": n_models, "window_length": 30})
+
+    ###
+    # First threshold the data and convert it to dBR
+    ###
+    # threshold the data
+    R_input[R_input < metadata["threshold"]] = 0.0
+    R_NWP[R_NWP < metadata["threshold"]] = 0.0
+
+    # convert the data
+    converter = pysteps.utils.get_method("mm/h")
+    R_input, _ = converter(R_input, metadata)
+    R_NWP, metadata = converter(R_NWP, metadata)
+
+    # transform the data
+    transformer = pysteps.utils.get_method(metadata["transformation"])
+    R_input, _ = transformer(R_input, metadata)
+    R_NWP, metadata = transformer(R_NWP, metadata)
+
+    # set NaN equal to zero
+    R_input[~np.isfinite(R_input)] = metadata["zerovalue"]
+    R_NWP[~np.isfinite(R_NWP)] = metadata["zerovalue"]
+
+    assert (
+        np.any(~np.isfinite(R_input)) == False
+    ), "There are still infinite values in the input radar data"
+    assert (
+        np.any(~np.isfinite(R_NWP)) == False
+    ), "There are still infinite values in the NWP data"
+
+    ###
+    # Decompose the R_NWP data
+    ###
+
+    # Initial decomposition settings
+    decomp_method, recomp_method = cascade.get_method("fft")
+    bandpass_filter_method = "gaussian"
+    M, N = R_input.shape[1:]
+    filter_method = cascade.get_method(bandpass_filter_method)
+    filter = filter_method((M, N), n_cascade_levels)
+
+    # If we only use one model:
+    if R_NWP.ndim == 3:
+        R_NWP = R_NWP[None, :]
+
+    if decomposed_nwp:
+        R_d_models = []
+        # Loop through the n_models
+        for i in range(R_NWP.shape[0]):
+            R_d_models_ = []
+            # Loop through the time steps
+            for j in range(R_NWP.shape[1]):
+                R_ = decomp_method(
+                    field=R_NWP[i, j, :, :],
+                    bp_filter=filter,
+                    normalize=True,
+                    compute_stats=True,
+                    compact_output=True,
+                )
+                R_d_models_.append(R_)
+            R_d_models.append(R_d_models_)
+
+        R_d_models = np.array(R_d_models)
+
+        assert R_d_models.ndim == 2, "Wrong number of dimensions in R_d_models"
+
+    else:
+        R_d_models = R_NWP.copy()
+
+        assert R_d_models.ndim == 4, "Wrong number of dimensions in R_d_models"
+
+    ###
+    # Determine the velocity fields
+    ###
+    oflow_method = pysteps.motion.get_method("lucaskanade")
+    V_radar = oflow_method(R_input)
+    V_NWP = []
+    # Loop through the models
+    for n_model in range(R_NWP.shape[0]):
+        # Loop through the timesteps. We need two images to construct a motion
+        # field, so we can start from timestep 1. Timestep 0 will be the same
+        # as timestep 0.
+        _V_NWP_ = []
+        for t in range(1, R_NWP.shape[1]):
+            V_NWP_ = oflow_method(R_NWP[n_model, t - 1 : t + 1, :])
+            _V_NWP_.append(V_NWP_)
+            V_NWP_ = None
+        _V_NWP_ = np.insert(_V_NWP_, 0, _V_NWP_[0], axis=0)
+        V_NWP.append(_V_NWP_)
+
+    V_NWP = np.stack(V_NWP)
+
+    assert V_NWP.ndim == 5, "V_NWP must be a five-dimensional array"
+
+    ###
+    # The nowcasting
+    ###
+    precip_forecast = blending.steps.forecast(
+        R=R_input,
+        R_d_models=R_d_models,
+        V=V_radar,
+        V_models=V_NWP,
+        timesteps=n_timesteps,
+        timestep=5.0,
+        issuetime=datetime.datetime.strptime("202112012355", "%Y%m%d%H%M"),
+        n_ens_members=n_ens_members,
+        n_cascade_levels=n_cascade_levels,
+        blend_nwp_members=blend_nwp_members,
+        R_thr=metadata["threshold"],
+        kmperpixel=1.0,
+        extrap_method="semilagrangian",
+        decomp_method="fft",
+        bandpass_filter_method="gaussian",
+        noise_method="nonparametric",
+        noise_stddev_adj="auto",
+        ar_order=2,
+        vel_pert_method=None,
+        weights_method=weights_method,
+        conditional=False,
+        probmatching_method=probmatching_method,
+        mask_method=mask_method,
+        callback=None,
+        return_output=True,
+        seed=None,
+        num_workers=1,
+        fft_method="numpy",
+        domain="spatial",
+        outdir_path_skill=outdir_path_skill,
+        extrap_kwargs=None,
+        filter_kwargs=None,
+        noise_kwargs=None,
+        vel_pert_kwargs=None,
+        clim_kwargs=clim_kwargs,
+        mask_kwargs=None,
+        measure_time=False,
+    )
+
+    assert precip_forecast.ndim == 4, "Wrong amount of dimensions in forecast output"
+    assert (
+        precip_forecast.shape[0] == expected_n_ens_members
+    ), "Wrong amount of output ensemble members in forecast output"
+    assert (
+        precip_forecast.shape[1] == n_timesteps
+    ), "Wrong amount of output time steps in forecast output"
+
+    # Transform the data back into mm/h
+    precip_forecast, _ = converter(precip_forecast, metadata)
+
+    assert (
+        precip_forecast.ndim == 4
+    ), "Wrong amount of dimensions in converted forecast output"
+    assert (
+        precip_forecast.shape[0] == expected_n_ens_members
+    ), "Wrong amount of output ensemble members in converted forecast output"
+    assert (
+        precip_forecast.shape[1] == n_timesteps
+    ), "Wrong amount of output time steps in converted forecast output"
diff --git a/pysteps/tests/test_blending_utils.py b/pysteps/tests/test_blending_utils.py
new file mode 100644
index 000000000..640a8c08d
--- /dev/null
+++ b/pysteps/tests/test_blending_utils.py
@@ -0,0 +1,323 @@
+# -*- coding: utf-8 -*-
+
+import os
+
+import numpy as np
+from numpy.testing import assert_array_almost_equal
+import pytest
+
+import pysteps
+from pysteps.blending.utils import (
+    stack_cascades,
+    blend_cascades,
+    recompose_cascade,
+    blend_optical_flows,
+    decompose_NWP,
+    compute_store_nwp_motion,
+    load_NWP,
+)
+
+# TODO: Fix tests for xarray fields
+
+pytest.importorskip("netCDF4")
+
+# Read an NWP forecast
+root_path = pysteps.rcparams.data_sources["rmi_nwp"]["root_path"]
+rel_path = os.path.join("2021", "07", "04")
+filename = os.path.join(root_path, rel_path, "ao13_2021070412_native_5min.nc")
+precip_nwp = pysteps.io.import_rmi_nwp_xr(filename)[0:7]
+
+nwp_metadata = precip_nwp.x.attrs.copy()
+nwp_metadata.update(**precip_nwp.y.attrs)
+nwp_metadata.update(**precip_nwp.attrs)
+
+# Get the analysis time and valid time
+times_nwp = precip_nwp["t"].values
+
+# Prepare input NWP files
+# Convert to rain rates [mm/h]
+converter = pysteps.utils.get_method("mm/h")
+precip_nwp, nwp_metadata = converter(precip_nwp, nwp_metadata)
+
+# Threshold the data
+precip_nwp.data[precip_nwp.data < 0.1] = 0.0
+nwp_metadata["threshold"] = 0.1
+
+# Transform the data
+transformer = pysteps.utils.get_method("dB")
+precip_nwp, nwp_metadata = transformer(
+    precip_nwp.values, nwp_metadata, threshold=nwp_metadata["threshold"]
+)
+
+# Set two issue times for testing
+issue_time_first = times_nwp[0]
+issue_time_second = times_nwp[3]
+
+# Set the blending weights (we'll blend with a 50-50 weight)
+weights = np.full((2, 8), fill_value=0.5)
+
+# Set the testing arguments
+# Test function arguments
+utils_arg_names = (
+    "precip_nwp",
+    "nwp_model",
+    "issue_times",
+    "timestep",
+    "n_timesteps",
+    "valid_times",
+    "shape",
+    "weights",
+)
+
+# Test function values
+utils_arg_values = [
+    (
+        precip_nwp,
+        "test",
+        [issue_time_first, issue_time_second],
+        5.0,
+        3,
+        times_nwp,
+        precip_nwp.shape[1:],
+        weights,
+    )
+]
+
+
+###
+# The test
+###
+@pytest.mark.parametrize(utils_arg_names, utils_arg_values)
+
+# The test function to be used
+def test_blending_utils(
+    precip_nwp,
+    nwp_model,
+    issue_times,
+    timestep,
+    n_timesteps,
+    valid_times,
+    shape,
+    weights,
+):
+    """Tests if all blending utils functions behave correctly."""
+
+    # First, make the output path if it does not exist yet
+    tmpdir = "./tmp/"
+    os.makedirs(tmpdir, exist_ok=True)
+
+    # Get the optical flow method
+    oflow_method = pysteps.motion.get_method("lucaskanade")
+
+    ###
+    # Compute and store the motion
+    ###
+    compute_store_nwp_motion(
+        precip_nwp=precip_nwp,
+        oflow_method=oflow_method,
+        analysis_time=valid_times[0],
+        nwp_model=nwp_model,
+        output_path=tmpdir,
+    )
+
+    # Check if file exists
+    date_string = np.datetime_as_string(valid_times[0], "s")
+    motion_file = os.path.join(
+        tmpdir,
+        nwp_model
+        + "_"
+        + date_string[:4]
+        + date_string[5:7]
+        + date_string[8:10]
+        + date_string[11:13]
+        + date_string[14:16]
+        + date_string[17:19]
+        + ".npy",
+    )
+    assert os.path.exists(motion_file)
+
+    ###
+    # Decompose and store NWP forecast
+    ###
+    decompose_NWP(
+        R_NWP=precip_nwp,
+        NWP_model=nwp_model,
+        analysis_time=valid_times[0],
+        timestep=timestep,
+        valid_times=valid_times,
+        num_cascade_levels=8,
+        num_workers=1,
+        output_path=tmpdir,
+        decomp_method="fft",
+        fft_method="numpy",
+        domain="spatial",
+        normalize=True,
+        compute_stats=True,
+        compact_output=False,
+    )
+
+    # Check if file exists
+    decomp_file = os.path.join(
+        tmpdir,
+        "cascade_"
+        + nwp_model
+        + "_"
+        + date_string[:4]
+        + date_string[5:7]
+        + date_string[8:10]
+        + date_string[11:13]
+        + date_string[14:16]
+        + date_string[17:19]
+        + ".nc",
+    )
+    assert os.path.exists(decomp_file)
+
+    ###
+    # Now check if files load correctly for two different issue times
+    ###
+    precip_decomposed_nwp_first, v_nwp_first = load_NWP(
+        input_nc_path_decomp=os.path.join(decomp_file),
+        input_path_velocities=os.path.join(motion_file),
+        start_time=issue_times[0],
+        n_timesteps=n_timesteps,
+    )
+
+    precip_decomposed_nwp_second, v_nwp_second = load_NWP(
+        input_nc_path_decomp=os.path.join(decomp_file),
+        input_path_velocities=os.path.join(motion_file),
+        start_time=issue_times[1],
+        n_timesteps=n_timesteps,
+    )
+
+    # Check if the output type and shapes are correct
+    assert isinstance(precip_decomposed_nwp_first, list)
+    assert isinstance(precip_decomposed_nwp_second, list)
+    assert isinstance(precip_decomposed_nwp_first[0], dict)
+    assert isinstance(precip_decomposed_nwp_second[0], dict)
+
+    assert "domain" in precip_decomposed_nwp_first[0]
+    assert "normalized" in precip_decomposed_nwp_first[0]
+    assert "compact_output" in precip_decomposed_nwp_first[0]
+    assert "valid_times" in precip_decomposed_nwp_first[0]
+    assert "cascade_levels" in precip_decomposed_nwp_first[0]
+    assert "means" in precip_decomposed_nwp_first[0]
+    assert "stds" in precip_decomposed_nwp_first[0]
+
+    assert precip_decomposed_nwp_first[0]["cascade_levels"].shape == (
+        8,
+        shape[0],
+        shape[1],
+    )
+    assert precip_decomposed_nwp_first[0]["domain"] == "spatial"
+    assert precip_decomposed_nwp_first[0]["normalized"] == True
+    assert precip_decomposed_nwp_first[0]["compact_output"] == False
+    assert len(precip_decomposed_nwp_first) == n_timesteps + 1
+    assert len(precip_decomposed_nwp_second) == n_timesteps + 1
+    assert precip_decomposed_nwp_first[0]["means"].shape[0] == 8
+    assert precip_decomposed_nwp_first[0]["stds"].shape[0] == 8
+
+    assert np.array(v_nwp_first).shape == (n_timesteps + 1, 2, shape[0], shape[1])
+    assert np.array(v_nwp_second).shape == (n_timesteps + 1, 2, shape[0], shape[1])
+
+    # Check if the right times are loaded
+    assert (
+        precip_decomposed_nwp_first[0]["valid_times"][0] == valid_times[0]
+    ), "Not the right valid times were loaded for the first forecast"
+    assert (
+        precip_decomposed_nwp_second[0]["valid_times"][0] == valid_times[3]
+    ), "Not the right valid times were loaded for the second forecast"
+
+    # Check, for a sample, if the stored motion fields are as expected
+    assert_array_almost_equal(
+        v_nwp_first[1],
+        oflow_method(precip_nwp[0:2, :, :]),
+        decimal=3,
+        err_msg="Stored motion field of first forecast not equal to expected motion field",
+    )
+    assert_array_almost_equal(
+        v_nwp_second[1],
+        oflow_method(precip_nwp[3:5, :, :]),
+        decimal=3,
+        err_msg="Stored motion field of second forecast not equal to expected motion field",
+    )
+
+    ###
+    # Stack the cascades
+    ###
+    precip_decomposed_first_stack, mu_first_stack, sigma_first_stack = stack_cascades(
+        R_d=precip_decomposed_nwp_first, donorm=False
+    )
+
+    print(precip_decomposed_nwp_first)
+    print(precip_decomposed_first_stack)
+    print(mu_first_stack)
+
+    (
+        precip_decomposed_second_stack,
+        mu_second_stack,
+        sigma_second_stack,
+    ) = stack_cascades(R_d=precip_decomposed_nwp_second, donorm=False)
+
+    # Check if the array shapes are still correct
+    assert precip_decomposed_first_stack.shape == (
+        n_timesteps + 1,
+        8,
+        shape[0],
+        shape[1],
+    )
+    assert mu_first_stack.shape == (n_timesteps + 1, 8)
+    assert sigma_first_stack.shape == (n_timesteps + 1, 8)
+
+    ###
+    # Blend the cascades
+    ###
+    precip_decomposed_blended = blend_cascades(
+        cascades_norm=np.stack(
+            (precip_decomposed_first_stack[0], precip_decomposed_second_stack[0])
+        ),
+        weights=weights,
+    )
+
+    assert precip_decomposed_blended.shape == precip_decomposed_first_stack[0].shape
+
+    ###
+    # Blend the optical flow fields
+    ###
+    v_nwp_blended = blend_optical_flows(
+        flows=np.stack((v_nwp_first[1], v_nwp_second[1])), weights=weights[:, 1]
+    )
+
+    assert v_nwp_blended.shape == v_nwp_first[1].shape
+    assert_array_almost_equal(
+        v_nwp_blended,
+        (oflow_method(precip_nwp[0:2, :, :]) + oflow_method(precip_nwp[3:5, :, :])) / 2,
+        decimal=3,
+        err_msg="Blended motion field does not equal average of the two motion fields",
+    )
+
+    ###
+    # Recompose the fields (the non-blended fields are used for this here)
+    ###
+    precip_recomposed_first = recompose_cascade(
+        combined_cascade=precip_decomposed_first_stack[0],
+        combined_mean=mu_first_stack[0],
+        combined_sigma=sigma_first_stack[0],
+    )
+    precip_recomposed_second = recompose_cascade(
+        combined_cascade=precip_decomposed_second_stack[0],
+        combined_mean=mu_second_stack[0],
+        combined_sigma=sigma_second_stack[0],
+    )
+
+    assert_array_almost_equal(
+        precip_recomposed_first,
+        precip_nwp[0, :, :],
+        decimal=3,
+        err_msg="Recomposed field of first forecast does not equal original field",
+    )
+    assert_array_almost_equal(
+        precip_recomposed_second,
+        precip_nwp[3, :, :],
+        decimal=3,
+        err_msg="Recomposed field of second forecast does not equal original field",
+    )
diff --git a/pysteps/tests/test_cascade.py b/pysteps/tests/test_cascade.py
index 4a428be99..77951922c 100644
--- a/pysteps/tests/test_cascade.py
+++ b/pysteps/tests/test_cascade.py
@@ -13,6 +13,9 @@
 from pysteps.cascade.decomposition import decomposition_fft, recompose_fft
 from pysteps.tests.helpers import smart_assert
 
+# TODO: Fix tests for xarray fields
+pytestmark = pytest.mark.skip("Needs migration to xarray")
+
 
 def test_decompose_recompose():
     """Tests cascade decomposition."""
diff --git a/pysteps/tests/test_datasets.py b/pysteps/tests/test_datasets.py
index 3c6f9581b..e524099e7 100644
--- a/pysteps/tests/test_datasets.py
+++ b/pysteps/tests/test_datasets.py
@@ -13,6 +13,9 @@
 )
 from pysteps.exceptions import DirectoryNotEmpty
 
+# TODO: Fix tests for xarray fields
+pytestmark = pytest.mark.skip("Needs migration to xarray")
+
 _datasets_opt_deps = dict(
     fmi=["pyproj"],
     mch=["PIL"],
diff --git a/pysteps/tests/test_detcatscores.py b/pysteps/tests/test_detcatscores.py
index 2126df6bb..e1dc3de4b 100644
--- a/pysteps/tests/test_detcatscores.py
+++ b/pysteps/tests/test_detcatscores.py
@@ -6,6 +6,9 @@
 
 from pysteps.verification import det_cat_fct
 
+# TODO: Fix tests for xarray fields
+pytestmark = pytest.mark.skip("Needs migration to xarray")
+
 # CREATE A LARGE DATASET TO MATCH
 # EXAMPLES IN
 # http://www.cawcr.gov.au/projects/verification/
diff --git a/pysteps/tests/test_exporters.py b/pysteps/tests/test_exporters.py
index 95776df23..9515828b7 100644
--- a/pysteps/tests/test_exporters.py
+++ b/pysteps/tests/test_exporters.py
@@ -15,6 +15,11 @@
 from pysteps.io.exporters import initialize_forecast_exporter_netcdf
 from pysteps.tests.helpers import get_precipitation_fields, get_invalid_mask
 
+# TODO: Fix tests for xarray fields
+pytestmark = pytest.mark.skip(
+    "Needs migration to xarray. Problem with cartesian_unit attribute."
+)
+
 
 def test_get_geotiff_filename():
     """Test the geotif name generator."""
diff --git a/pysteps/tests/test_io_archive.py b/pysteps/tests/test_io_archive.py
new file mode 100644
index 000000000..74a0567ec
--- /dev/null
+++ b/pysteps/tests/test_io_archive.py
@@ -0,0 +1,35 @@
+from datetime import datetime
+
+import pytest
+
+import pysteps
+
+
+def test_find_by_date_mch():
+
+    pytest.importorskip("PIL")
+
+    date = datetime.strptime("201505151630", "%Y%m%d%H%M")
+    data_source = pysteps.rcparams.data_sources["mch"]
+    root_path = data_source["root_path"]
+    path_fmt = data_source["path_fmt"]
+    fn_pattern = data_source["fn_pattern"]
+    fn_ext = data_source["fn_ext"]
+    timestep = data_source["timestep"]
+
+    fns = pysteps.io.archive.find_by_date(
+        date,
+        root_path,
+        path_fmt,
+        fn_pattern,
+        fn_ext,
+        timestep=timestep,
+        num_prev_files=1,
+        num_next_files=1,
+    )
+
+    assert len(fns) == 2
+    assert len(fns[0]) == 3
+    assert len(fns[1]) == 3
+    assert isinstance(fns[0][0], str)
+    assert isinstance(fns[1][0], datetime)
diff --git a/pysteps/tests/test_io_bom_nwp.py b/pysteps/tests/test_io_bom_nwp.py
new file mode 100644
index 000000000..a4762726f
--- /dev/null
+++ b/pysteps/tests/test_io_bom_nwp.py
@@ -0,0 +1,78 @@
+# -*- coding: utf-8 -*-
+
+import os
+import numpy as np
+
+import xarray as xr
+import pytest
+
+import pysteps
+from pysteps.tests.helpers import smart_assert
+
+pytest.importorskip("netCDF4")
+
+root_path = pysteps.rcparams.data_sources["bom_nwp"]["root_path"]
+rel_path = os.path.join("2020", "10", "31")
+filename = os.path.join(root_path, rel_path, "20201031_0000_regrid_short.nc")
+data_array_xr = pysteps.io.import_bom_nwp_xr(filename)
+
+expected_proj = "+proj=aea  +lon_0=153.240 +lat_0=-27.718 +lat_1=-26.200 +lat_2=-29.300"
+
+
+def test_io_import_bom_nwp_xarray():
+    """Test the importer Bom NWP."""
+    assert isinstance(data_array_xr, xr.DataArray)
+
+
+def test_io_import_bom_nwp_xarray_shape():
+    """Test the importer Bom NWP shape."""
+    assert isinstance(data_array_xr, xr.DataArray)
+    assert data_array_xr.shape == (144, 512, 512)
+
+
+test_attrs_xr = [
+    ("projection", expected_proj, None),
+    ("institution", "Commonwealth of Australia, Bureau of Meteorology", None),
+    ("transform", None, None),
+    ("zerovalue", 0.0, 0.1),
+    ("unit", "mm", None),
+    ("accutime", 10, None),
+    ("zr_a", None, None),
+    ("zr_b", None, None),
+    ("xpixelsize", 500.0, 0.1),
+    ("ypixelsize", 500.0, 0.1),
+    ("units", "mm", None),
+    ("yorigin", "upper", None),
+]
+
+
+@pytest.mark.parametrize("variable, expected, tolerance", test_attrs_xr)
+def test_io_import_bom_nwp_xarray_attrs(variable, expected, tolerance):
+    """Test the importer Bom RF3."""
+    smart_assert(data_array_xr.attrs[variable], expected, tolerance)
+
+
+test_attrs_xr_coord_x = [
+    ("cartesian_unit", "m", None),
+    ("x1", -127750.0, 0.1),
+    ("x2", 127750.0, 0.1),
+]
+
+
+@pytest.mark.parametrize("variable, expected, tolerance", test_attrs_xr_coord_x)
+def test_io_import_bom_nwp_xarray_attrs_coordx(variable, expected, tolerance):
+    """Test the importer Bom NWP."""
+    smart_assert(data_array_xr.x.attrs[variable], expected, tolerance)
+
+
+test_attrs_xr_coord_y = [
+    ("cartesian_unit", "m", None),
+    ("y1", -127750.0, 0.1),
+    ("y2", 127750.0, 0.1),
+]
+
+
+@pytest.mark.parametrize("variable, expected, tolerance", test_attrs_xr_coord_y)
+def test_io_import_bom_nwp_xarray_attrs_coordy(variable, expected, tolerance):
+    """Test the importer Bom NWP."""
+    smart_assert(data_array_xr.y.attrs[variable], expected, tolerance)
diff --git a/pysteps/tests/test_io_bom_rf3.py b/pysteps/tests/test_io_bom_rf3.py
index 6c2f17062..0699c6ea2 100644
--- a/pysteps/tests/test_io_bom_rf3.py
+++ b/pysteps/tests/test_io_bom_rf3.py
@@ -1,7 +1,9 @@
 # -*- coding: utf-8 -*-
 
 import os
+import numpy as np
 
+import xarray as xr
 import pytest
 
 import pysteps
@@ -9,55 +11,41 @@
 
 pytest.importorskip("netCDF4")
 
+root_path = pysteps.rcparams.data_sources["bom"]["root_path"]
+rel_path = os.path.join("prcp-cscn", "2", "2018", "06", "16")
+filename = os.path.join(root_path, rel_path, "2_20180616_100000.prcp-cscn.nc")
+data_array = pysteps.io.import_bom_rf3(filename)
+
 
 def test_io_import_bom_rf3_shape():
     """Test the importer Bom RF3."""
-    root_path = pysteps.rcparams.data_sources["bom"]["root_path"]
-    rel_path = os.path.join("prcp-cscn", "2", "2018", "06", "16")
-    filename = os.path.join(root_path, rel_path, "2_20180616_100000.prcp-cscn.nc")
-    precip, _, _ = pysteps.io.import_bom_rf3(filename)
-    assert precip.shape == (512, 512)
+    assert isinstance(data_array, xr.DataArray)
+    assert data_array.shape == (512, 512)
 
 
-expected_proj1 = (
+expected_proj = (
     "+proj=aea  +lon_0=144.752 +lat_0=-37.852 " "+lat_1=-18.000 +lat_2=-36.000"
 )
 
-# test_metadata_bom: list of (variable,expected,tolerance) tuples
-test_metadata_bom = [
+test_attrs = [
+    ("projection", expected_proj, None),
+    ("institution", "Commonwealth of Australia, Bureau of Meteorology", None),
     ("transform", None, None),
     ("zerovalue", 0.0, 0.1),
-    ("projection", expected_proj1, None),
     ("unit", "mm", None),
     ("accutime", 6, 0.1),
-    ("x1", -128000.0, 0.1),
-    ("x2", 127500.0, 0.1),
-    ("y1", -127500.0, 0.1),
-    ("y2", 128000.0, 0.1),
-    ("xpixelsize", 500.0, 0.1),
-    ("ypixelsize", 500.0, 0.1),
-    ("cartesian_unit", "m", None),
-    ("yorigin", "upper", None),
-    ("institution", "Commonwealth of Australia, Bureau of Meteorology", None),
 ]
 
 
-@pytest.mark.parametrize("variable, expected, tolerance", test_metadata_bom)
-def test_io_import_bom_rf3_metadata(variable, expected, tolerance):
+@pytest.mark.parametrize("variable, expected, tolerance", test_attrs)
+def test_io_import_bom_rf3_dataset_attrs(variable, expected, tolerance):
     """Test the importer Bom RF3."""
-    root_path = pysteps.rcparams.data_sources["bom"]["root_path"]
-    rel_path = os.path.join("prcp-cscn", "2", "2018", "06", "16")
-    filename = os.path.join(root_path, rel_path, "2_20180616_100000.prcp-cscn.nc")
-    _, _, metadata = pysteps.io.import_bom_rf3(filename)
-    smart_assert(metadata[variable], expected, tolerance)
+    smart_assert(data_array.attrs[variable], expected, tolerance)
 
 
-expected_proj2 = (
-    "+proj=aea  +lon_0=144.752 +lat_0=-37.852 " "+lat_1=-18.000 +lat_2=-36.000"
-)
 # test_geodata: list of (variable,expected,tolerance) tuples
 test_geodata_bom = [
-    ("projection", expected_proj2, None),
+    ("projection", expected_proj, None),
     ("unit", "mm", None),
     ("accutime", 6, 0.1),
     ("x1", -128000.0, 0.1),
@@ -80,3 +68,66 @@ def test_io_import_bom_rf3_geodata(variable, expected, tolerance):
     filename = os.path.join(root_path, rel_path, "2_20180616_100000.prcp-cscn.nc")
     geodata = pysteps.io.importers._import_bom_rf3_geodata(filename)
     smart_assert(geodata[variable], expected, tolerance)
+
+
+# TEST XARRAY IMPLEMENTATION
+data_array_xr = pysteps.io.import_bom_rf3_xr(filename)
+
+
+def test_io_import_bom_rf3_xarray():
+    """Test the importer Bom RF3."""
+    assert isinstance(data_array_xr, xr.DataArray)
+
+
+def test_io_import_bom_rf3_xarray_shape():
+    """Test the importer Bom RF3."""
+    assert isinstance(data_array_xr, xr.DataArray)
+    assert data_array_xr.shape == (1, 512, 512)
+
+
+test_attrs_xr = [
+    ("projection", expected_proj, None),
+    ("institution", "Commonwealth of Australia, Bureau of Meteorology", None),
+    ("transform", None, None),
+    ("zerovalue", 0.0, 0.1),
+    ("unit", "mm", None),
+    ("accutime", np.timedelta64(6, "m"), None),
+    ("zr_a", None, None),
+    ("zr_b", None, None),
+    ("xpixelsize", 500.0, 0.1),
+    ("ypixelsize", 500.0, 0.1),
+    ("units", "mm", None),
+    ("yorigin", "upper", None),
+]
+
+
+@pytest.mark.parametrize("variable, expected, tolerance", test_attrs_xr)
+def test_io_import_bom_rf3_xarray_attrs(variable, expected, tolerance):
+    """Test the importer Bom RF3."""
+    smart_assert(data_array_xr.attrs[variable], expected, tolerance)
+
+
+test_attrs_xr_coord_x = [
+    ("units", "m", None),
+    ("x1", -128000.0, 0.1),
+    ("x2", 127500.0, 0.1),
+]
+
+
+@pytest.mark.parametrize("variable, expected, tolerance", test_attrs_xr_coord_x)
+def test_io_import_bom_rf3_xarray_attrs_coordx(variable, expected, tolerance):
+    """Test the importer Bom RF3."""
+    smart_assert(data_array_xr.x.attrs[variable], expected, tolerance)
+
+
+test_attrs_xr_coord_y = [
+    ("units", "m", None),
+    ("y1", -127500.0, 0.1),
+    ("y2", 128000.0, 0.1),
+]
+
+
+@pytest.mark.parametrize("variable, expected, tolerance", test_attrs_xr_coord_y)
+def test_io_import_bom_rf3_xarray_attrs_coordy(variable, expected, tolerance):
+    """Test the importer Bom RF3."""
+    smart_assert(data_array_xr.y.attrs[variable], expected, tolerance)
diff --git a/pysteps/tests/test_io_fmi_pgm.py b/pysteps/tests/test_io_fmi_pgm.py
index 58dfd5137..a07ebb9ee 100644
--- a/pysteps/tests/test_io_fmi_pgm.py
+++ b/pysteps/tests/test_io_fmi_pgm.py
@@ -2,6 +2,7 @@
 
 import os
 
+import xarray as xr
 import pytest
 
 import pysteps
@@ -10,62 +11,66 @@
 pytest.importorskip("pyproj")
 
 
-def test_io_import_fmi_pgm_shape():
-    """Test the importer FMI PGM."""
-    root_path = pysteps.rcparams.data_sources["fmi"]["root_path"]
-    filename = os.path.join(
-        root_path,
-        "20160928",
-        "201609281600_fmi.radar.composite.lowest_FIN_SUOMI1.pgm.gz",
-    )
-    R, _, _ = pysteps.io.import_fmi_pgm(filename, gzipped=True)
-    assert R.shape == (1226, 760)
-
-
-# test_metadata: list of (variable,expected) tuples
-test_metadata = [
-    ("composite_area", ["FIN"]),
-    ("projection_name", ["SUOMI1"]),
-    ("radar", ["LUO", "1", "26.9008", "67.1386"]),
-    ("obstime", ["201609281600"]),
-    ("producttype", ["CAPPI"]),
-    ("productname", ["LOWEST"]),
-    ("param", ["CorrectedReflectivity"]),
-    ("metersperpixel_x", ["999.674053"]),
-    ("metersperpixel_y", ["999.62859"]),
-    ("projection", ["radar", "{"]),
-    ("type", ["stereographic"]),
-    ("centrallongitude", ["25"]),
-    ("centrallatitude", ["90"]),
-    ("truelatitude", ["60"]),
-    ("bottomleft", ["18.600000", "57.930000"]),
-    ("topright", ["34.903000", "69.005000"]),
-    ("missingval", 255),
-]
+root_path = pysteps.rcparams.data_sources["fmi"]["root_path"]
+filename = os.path.join(
+    root_path,
+    "20160928",
+    "201609281600_fmi.radar.composite.lowest_FIN_SUOMI1.pgm.gz",
+)
+data_array = pysteps.io.import_fmi_pgm(filename, gzipped=True)
 
 
-@pytest.mark.parametrize("variable,expected", test_metadata)
-def test_io_import_fmi_pmg_metadata(variable, expected):
-    """Test the importer FMI PMG."""
-    root_path = pysteps.rcparams.data_sources["fmi"]["root_path"]
-    filename = os.path.join(
-        root_path,
-        "20160928",
-        "201609281600_fmi.radar.composite.lowest_FIN_SUOMI1.pgm.gz",
-    )
-    metadata = pysteps.io.importers._import_fmi_pgm_metadata(filename, gzipped=True)
-    assert metadata[variable] == expected
+def test_io_import_fmi_pgm_shape():
+    """Test the importer FMI PGM."""
+    assert isinstance(data_array, xr.DataArray)
+    assert data_array.shape == (1226, 760)
 
 
-expected_proj1 = (
+expected_proj = (
     "+proj=stere  +lon_0=25E +lat_0=90N "
     "+lat_ts=60 +a=6371288 +x_0=380886.310 "
     "+y_0=3395677.920 +no_defs"
 )
 
+test_attrs = [
+    ("projection", expected_proj, None),
+    ("institution", "Finnish Meteorological Institute", None),
+    # ("composite_area", ["FIN"]),
+    # ("projection_name", ["SUOMI1"]),
+    # ("radar", ["LUO", "1", "26.9008", "67.1386"]),
+    # ("obstime", ["201609281600"]),
+    # ("producttype", ["CAPPI"]),
+    # ("productname", ["LOWEST"]),
+    # ("param", ["CorrectedReflectivity"]),
+    # ("metersperpixel_x", ["999.674053"]),
+    # ("metersperpixel_y", ["999.62859"]),
+    # ("projection", ["radar", "{"]),
+    # ("type", ["stereographic"]),
+    # ("centrallongitude", ["25"]),
+    # ("centrallatitude", ["90"]),
+    # ("truelatitude", ["60"]),
+    # ("bottomleft", ["18.600000", "57.930000"]),
+    # ("topright", ["34.903000", "69.005000"]),
+    # ("missingval", 255),
+    ("accutime", 5.0, 0.1),
+    ("unit", "dBZ", None),
+    ("transform", "dB", None),
+    ("zerovalue", -32.0, 0.1),
+    ("threshold", -31.5, 0.1),
+    ("zr_a", 223.0, 0.1),
+    ("zr_b", 1.53, 0.1),
+]
+
+
+@pytest.mark.parametrize("variable, expected, tolerance", test_attrs)
+def test_io_import_mch_gif_dataset_attrs(variable, expected, tolerance):
+    """Test the importer FMI PMG."""
+    smart_assert(data_array.attrs[variable], expected, tolerance)
+
+
 # test_geodata: list of (variable,expected,tolerance) tuples
 test_geodata = [
-    ("projection", expected_proj1, None),
+    ("projection", expected_proj, None),
     ("x1", 0.0049823258887045085, 1e-20),
     ("x2", 759752.2852757066, 1e-10),
     ("y1", 0.009731985162943602, 1e-18),
diff --git a/pysteps/tests/test_io_knmi_hdf5.py b/pysteps/tests/test_io_knmi_hdf5.py
index 44e777a11..aee3076d0 100644
--- a/pysteps/tests/test_io_knmi_hdf5.py
+++ b/pysteps/tests/test_io_knmi_hdf5.py
@@ -2,6 +2,7 @@
 
 import os
 
+import xarray as xr
 import pytest
 
 import pysteps
@@ -10,54 +11,38 @@
 pytest.importorskip("h5py")
 
 
-def test_io_import_knmi_hdf5_shape():
-    """Test the importer KNMI HDF5."""
-
-    root_path = pysteps.rcparams.data_sources["knmi"]["root_path"]
+root_path = pysteps.rcparams.data_sources["knmi"]["root_path"]
+filename = os.path.join(root_path, "2010/08", "RAD_NL25_RAP_5min_201008260000.h5")
+data_array = pysteps.io.import_knmi_hdf5(filename)
 
-    filename = os.path.join(root_path, "2010/08", "RAD_NL25_RAP_5min_201008260000.h5")
 
-    precip, __, __ = pysteps.io.import_knmi_hdf5(filename)
-
-    assert precip.shape == (765, 700)
+def test_io_import_knmi_hdf5_shape():
+    """Test the importer KNMI HDF5."""
+    assert isinstance(data_array, xr.DataArray)
+    assert data_array.shape == (765, 700)
 
 
 # test_metadata: list of (variable,expected, tolerance) tuples
 
-expected_proj1 = (
-    "+proj=stere +lat_0=90 +lon_0=0.0 "
-    "+lat_ts=60.0 +a=6378.137 +b=6356.752 +x_0=0 "
-    "+y_0=0"
+expected_proj = (
+    "+proj=stere +lat_0=90 +lon_0=0.0 +lat_ts=60.0 +a=6378137 +b=6356752 +x_0=0 +y_0=0"
 )
 
-test_metadata = [
-    ("projection", expected_proj1, None),
-    ("x1", 0.0, 1e-10),
-    ("y1", -4415.038179210632, 1e-10),
-    ("x2", 699.9842646331593, 1e-10),
-    ("y2", -3649.950360247753, 1e-10),
-    ("xpixelsize", 1.0, 1e-10),
-    ("xpixelsize", 1.0, 1e-10),
-    ("cartesian_unit", "km", None),
+# list of (variable,expected,tolerance) tuples
+test_attrs = [
+    ("projection", expected_proj, None),
+    ("institution", "KNMI - Royal Netherlands Meteorological Institute", None),
     ("accutime", 5.0, 1e-10),
-    ("yorigin", "upper", None),
     ("unit", "mm", None),
-    ("institution", "KNMI - Royal Netherlands Meteorological Institute", None),
     ("transform", None, None),
     ("zerovalue", 0.0, 1e-10),
     ("threshold", 0.01, 1e-10),
-    ("zr_a", 200.0, None),
-    ("zr_b", 1.6, None),
+    ("zr_a", 200.0, 0.1),
+    ("zr_b", 1.6, 0.1),
 ]
 
 
-@pytest.mark.parametrize("variable,expected,tolerance", test_metadata)
+@pytest.mark.parametrize("variable,expected,tolerance", test_attrs)
 def test_io_import_knmi_hdf5_metadata(variable, expected, tolerance):
     """Test the importer KNMI HDF5."""
-    root_path = pysteps.rcparams.data_sources["knmi"]["root_path"]
-
-    filename = os.path.join(root_path, "2010/08", "RAD_NL25_RAP_5min_201008260000.h5")
-
-    __, __, metadata = pysteps.io.import_knmi_hdf5(filename)
-
-    smart_assert(metadata[variable], expected, tolerance)
+    smart_assert(data_array.attrs[variable], expected, tolerance)
diff --git a/pysteps/tests/test_io_knmi_nwp.py b/pysteps/tests/test_io_knmi_nwp.py
new file mode 100644
index 000000000..96c1ebddf
--- /dev/null
+++ b/pysteps/tests/test_io_knmi_nwp.py
@@ -0,0 +1,78 @@
+# -*- coding: utf-8 -*-
+
+import os
+import numpy as np
+
+import xarray as xr
+import pytest
+
+import pysteps
+from pysteps.tests.helpers import smart_assert
+
+pytest.importorskip("netCDF4")
+
+root_path = pysteps.rcparams.data_sources["knmi_nwp"]["root_path"]
+rel_path = os.path.join("2018", "09", "05")
+filename = os.path.join(root_path, rel_path, "20180905_0600_Pforecast_Harmonie.nc")
+data_array_xr = pysteps.io.import_knmi_nwp_xr(filename)
+
+expected_proj = "+proj=longlat +ellps=WGS84 +datum=WGS84 +no_defs"
+
+
+def test_io_import_knmi_nwp_xarray():
+    """Test the KNMI NWP Harmonie importer."""
+    assert isinstance(data_array_xr, xr.DataArray)
+
+
+def test_io_import_knmi_nwp_xarray_shape():
+    """Test the importer KNMI NWP shape."""
+    assert isinstance(data_array_xr, xr.DataArray)
+    assert data_array_xr.shape == (49, 300, 300)
+
+
+test_attrs_xr = [
+    ("projection", expected_proj, None),
+    ("institution", "  Royal Netherlands Meteorological Institute (KNMI)  ", None),
+    ("transform", None, None),
+    ("zerovalue", 0.0, 0.1),
+    ("unit", "mm", None),
+    ("accutime", 60, None),
+    ("zr_a", None, None),
+    ("zr_b", None, None),
+    ("xpixelsize", 0.037, 0.0001),
+    ("ypixelsize", 0.023, 0.0001),
+    ("units", "mm", None),
+    ("yorigin", "lower", None),
+]
+
+
+@pytest.mark.parametrize("variable, expected, tolerance", test_attrs_xr)
+def test_io_import_knmi_nwp_xarray_attrs(variable, expected, tolerance):
+    """Test the KNMI Harmonie NWP importer."""
+    smart_assert(data_array_xr.attrs[variable], expected, tolerance)
+
+
+test_attrs_xr_coord_x = [
+    ("cartesian_unit", "degrees_east", None),
+    ("x1", 0.0, 0.0001),
+    ("x2", 11.063, 0.0001),
+]
+
+
+@pytest.mark.parametrize("variable, expected, tolerance", test_attrs_xr_coord_x)
+def test_io_import_knmi_nwp_xarray_attrs_coordx(variable, expected, tolerance):
+    """Test the grid of the KNMI Harmonie NWP importer in the x-direction."""
+    smart_assert(data_array_xr.x.attrs[variable], expected, tolerance)
+
+
+test_attrs_xr_coord_y = [
+    ("cartesian_unit", "degrees_north", None),
+    ("y1", 49.0, 0.0001),
+    ("y2", 55.877, 0.0001),
+]
+
+
+@pytest.mark.parametrize("variable, expected, tolerance", test_attrs_xr_coord_y)
+def test_io_import_knmi_nwp_xarray_attrs_coordy(variable, expected, tolerance):
+    """Test the grid of the KNMI Harmonie NWP importer in the y-direction."""
+    smart_assert(data_array_xr.y.attrs[variable], expected, tolerance)
diff --git a/pysteps/tests/test_io_mch_gif.py b/pysteps/tests/test_io_mch_gif.py
index f569dd4a2..d0eeae2b1 100644
--- a/pysteps/tests/test_io_mch_gif.py
+++ b/pysteps/tests/test_io_mch_gif.py
@@ -3,22 +3,25 @@
 import os
 
 import pytest
+import xarray as xr
 
 import pysteps
 from pysteps.tests.helpers import smart_assert
 
 pytest.importorskip("PIL")
 
+root_path = pysteps.rcparams.data_sources["mch"]["root_path"]
+filename = os.path.join(root_path, "20170131", "AQC170310945F_00005.801.gif")
+data_array = pysteps.io.import_mch_gif(filename, "AQC", "mm", 5.0)
+
 
 def test_io_import_mch_gif_shape():
     """Test the importer MCH GIF."""
-    root_path = pysteps.rcparams.data_sources["mch"]["root_path"]
-    filename = os.path.join(root_path, "20170131", "AQC170310945F_00005.801.gif")
-    precip, _, metadata = pysteps.io.import_mch_gif(filename, "AQC", "mm", 5.0)
-    assert precip.shape == (640, 710)
+    assert isinstance(data_array, xr.DataArray)
+    assert data_array.shape == (640, 710)
 
 
-expected_proj1 = (
+expected_proj = (
     "+proj=somerc  +lon_0=7.43958333333333 "
     "+lat_0=46.9524055555556 +k_0=1 "
     "+x_0=600000 +y_0=200000 +ellps=bessel "
@@ -26,48 +29,29 @@ def test_io_import_mch_gif_shape():
     "+units=m +no_defs"
 )
 
-# test_metadata: list of (variable,expected,tolerance) tuples
-test_metadata = [
-    ("projection", expected_proj1, None),
-    ("x1", 255000.0, 0.1),
-    ("y1", -160000.0, 0.1),
-    ("x2", 965000.0, 0.1),
-    ("y2", 480000.0, 0.1),
-    ("xpixelsize", 1000.0, 0.1),
-    ("ypixelsize", 1000.0, 0.1),
-    ("cartesian_unit", "m", None),
-    ("yorigin", "upper", None),
+# list of (variable,expected,tolerance) tuples
+test_attrs = [
+    ("projection", expected_proj, None),
+    ("institution", "MeteoSwiss", None),
     ("accutime", 5.0, 0.1),
     ("unit", "mm", None),
     ("transform", None, None),
-    ("zerovalue", 0.0, None),
+    ("zerovalue", 0.0, 0.1),
     ("threshold", 0.0009628129986471908, 1e-19),
-    ("institution", "MeteoSwiss", None),
-    ("product", "AQC", None),
-    ("zr_a", 316.0, None),
-    ("zr_b", 1.5, None),
+    ("zr_a", 316.0, 0.1),
+    ("zr_b", 1.5, 0.1),
 ]
 
 
-@pytest.mark.parametrize("variable, expected, tolerance", test_metadata)
-def test_io_import_mch_gif_metadata(variable, expected, tolerance):
+@pytest.mark.parametrize("variable, expected, tolerance", test_attrs)
+def test_io_import_mch_gif_dataset_attrs(variable, expected, tolerance):
     """Test the importer MCH GIF."""
-    root_path = pysteps.rcparams.data_sources["mch"]["root_path"]
-    filename = os.path.join(root_path, "20170131", "AQC170310945F_00005.801.gif")
-    _, _, metadata = pysteps.io.import_mch_gif(filename, "AQC", "mm", 5.0)
-    smart_assert(metadata[variable], expected, tolerance)
-
+    smart_assert(data_array.attrs[variable], expected, tolerance)
 
-expected_proj2 = (
-    "+proj=somerc  +lon_0=7.43958333333333 +lat_0=46.9524055555556 "
-    "+k_0=1 +x_0=600000 +y_0=200000 +ellps=bessel "
-    "+towgs84=674.374,15.056,405.346,0,0,0,0 "
-    "+units=m +no_defs"
-)
 
 # test_geodata: list of (variable,expected,tolerance) tuples
 test_geodata = [
-    ("projection", expected_proj2, None),
+    ("projection", expected_proj, None),
     ("x1", 255000.0, 0.1),
     ("y1", -160000.0, 0.1),
     ("x2", 965000.0, 0.1),
diff --git a/pysteps/tests/test_io_mrms_grib.py b/pysteps/tests/test_io_mrms_grib.py
index 65dd0caeb..afebb7b37 100644
--- a/pysteps/tests/test_io_mrms_grib.py
+++ b/pysteps/tests/test_io_mrms_grib.py
@@ -4,6 +4,7 @@
 
 import numpy as np
 import pytest
+import xarray as xr
 from numpy.testing import assert_array_almost_equal
 
 import pysteps
@@ -15,33 +16,32 @@ def test_io_import_mrms_grib():
     """Test the importer for NSSL data."""
 
     root_path = pysteps.rcparams.data_sources["mrms"]["root_path"]
-
     filename = os.path.join(
         root_path, "2019/06/10/", "PrecipRate_00.00_20190610-000000.grib2"
     )
+    data_Array = pysteps.io.import_mrms_grib(filename, fillna=0, window_size=1)
 
-    precip_full, _, metadata = pysteps.io.import_mrms_grib(
-        filename, fillna=0, window_size=1
-    )
-    assert precip_full.shape == (3500, 7000)
-    assert precip_full.dtype == "single"
+    assert isinstance(data_Array, xr.DataArray)
+    assert data_Array.shape == (3500, 7000)
+    assert data_Array.dtype == "single"
 
     expected_metadata = {
-        "xpixelsize": 0.01,
-        "ypixelsize": 0.01,
+        "institution": "NOAA National Severe Storms Laboratory",
+        "projection": "+proj=longlat  +ellps=IAU76",
         "unit": "mm/h",
+        "accutime": 2.0,
         "transform": None,
         "zerovalue": 0,
-        "projection": "+proj=longlat  +ellps=IAU76",
-        "yorigin": "upper",
         "threshold": 0.1,
         "x1": -129.99999999999997,
         "x2": -60.00000199999991,
         "y1": 20.000001,
         "y2": 55.00000000000001,
-        "cartesian_unit": "degrees",
+        "units": "degrees",
     }
-
+    metadata = data_Array.attrs
+    metadata.update(**data_Array.x.attrs)
+    metadata.update(**data_Array.y.attrs)
     for key, value in expected_metadata.items():
         if isinstance(value, float):
             assert_array_almost_equal(metadata[key], expected_metadata[key])
@@ -50,6 +50,7 @@ def test_io_import_mrms_grib():
 
     x = np.arange(metadata["x1"], metadata["x2"], metadata["xpixelsize"])
     y = np.arange(metadata["y1"], metadata["y2"], metadata["ypixelsize"])
+    precip_full = data_Array.values
     assert y.size == precip_full.shape[0]
     assert x.size == precip_full.shape[1]
 
@@ -59,36 +60,38 @@ def test_io_import_mrms_grib():
     # Test that if the bounding box is larger than the domain, all the points are returned.
     precip_full2 = pysteps.io.import_mrms_grib(
         filename, fillna=0, extent=(220, 300, 20, 55), window_size=1
-    )[0]
+    )
     assert precip_full2.shape == (3500, 7000)
 
-    assert_array_almost_equal(precip_full, precip_full2)
+    assert_array_almost_equal(data_Array, precip_full2)
 
     del precip_full2
 
     # Test that a portion of the domain is returned correctly
     precip_clipped = pysteps.io.import_mrms_grib(
         filename, fillna=0, extent=(250, 260, 30, 35), window_size=1
-    )[0]
+    )
 
     assert precip_clipped.shape == (500, 1000)
-    assert_array_almost_equal(precip_clipped, precip_full[2000:2500, 2000:3000])
+    assert_array_almost_equal(
+        precip_clipped, data_Array.isel(y=slice(2000, 2500), x=slice(2000, 3000))
+    )
     del precip_clipped
 
-    precip_single = pysteps.io.import_mrms_grib(filename, dtype="double", fillna=0)[0]
+    precip_single = pysteps.io.import_mrms_grib(filename, dtype="double", fillna=0)
     assert precip_single.dtype == "double"
     del precip_single
 
-    precip_single = pysteps.io.import_mrms_grib(filename, dtype="single", fillna=0)[0]
+    precip_single = pysteps.io.import_mrms_grib(filename, dtype="single", fillna=0)
     assert precip_single.dtype == "single"
     del precip_single
 
     precip_donwscaled = pysteps.io.import_mrms_grib(
         filename, dtype="single", fillna=0, window_size=2
-    )[0]
+    )
     assert precip_donwscaled.shape == (3500 / 2, 7000 / 2)
 
-    precip_donwscaled, _, metadata = pysteps.io.import_mrms_grib(
+    precip_donwscaled = pysteps.io.import_mrms_grib(
         filename, dtype="single", fillna=0, window_size=3
     )
     print(metadata)
@@ -108,6 +111,22 @@ def test_io_import_mrms_grib():
     # precipitation pattern (i.e. the file being read).
     expected_metadata.pop("threshold", None)
 
-    for key in expected_metadata.keys():
-        assert metadata[key] == expected_metadata[key]
-    assert precip_donwscaled.shape == (3500 // 3, 7000 // 3)
+    # expected_metadata.update(
+    #     xpixelsize=0.03,
+    #     ypixelsize=0.03,
+    #     x1=-129.98500000028577,
+    #     x2=-60.02500199942843,
+    #     y1=20.03500099914261,
+    #     y2=54.985000000285794,
+    # )
+
+    # # Remove the threshold keyword from the test when the window_size>1 is used.
+    # # The threshold is computed automatically from the minimum valid precipitation values.
+    # # The minimum value is affected by the the block_averaging (window_size=3 keyword)
+    # # of the precipitation fields. Hence, the "threshold" value will depend on the
+    # # precipitation pattern (i.e. the file being read).
+    # expected_metadata.pop("threshold", None)
+
+    # for key in expected_metadata.keys():
+    #     assert metadata[key] == expected_metadata[key]
+    # assert precip_donwscaled.shape == (3500 // 3, 7000 // 3)
diff --git a/pysteps/tests/test_io_opera_hdf5.py b/pysteps/tests/test_io_opera_hdf5.py
index 6411bcd33..71032bbd3 100644
--- a/pysteps/tests/test_io_opera_hdf5.py
+++ b/pysteps/tests/test_io_opera_hdf5.py
@@ -2,6 +2,7 @@
 
 import os
 
+import xarray as xr
 import pytest
 
 import pysteps
@@ -10,53 +11,39 @@
 pytest.importorskip("h5py")
 
 
+root_path = pysteps.rcparams.data_sources["opera"]["root_path"]
+filename = os.path.join(root_path, "20180824", "T_PAAH21_C_EUOC_20180824180000.hdf")
+data_array = pysteps.io.import_opera_hdf5(filename)
+
+
 def test_io_import_opera_hdf5_shape():
     """Test the importer OPERA HDF5."""
-
-    root_path = pysteps.rcparams.data_sources["opera"]["root_path"]
-    filename = os.path.join(root_path, "20180824", "T_PAAH21_C_EUOC_20180824180000.hdf")
-    R, _, _ = pysteps.io.import_opera_hdf5(filename)
-    assert R.shape == (2200, 1900)
+    assert isinstance(data_array, xr.DataArray)
+    assert data_array.shape == (2200, 1900)
 
 
 # test_metadata: list of (variable,expected, tolerance) tuples
 
-expected_proj1 = (
+expected_proj = (
     "+proj=laea +lat_0=55.0 +lon_0=10.0 "
     "+x_0=1950000.0 "
     "+y_0=-2100000.0 "
     "+units=m +ellps=WGS84"
 )
 
-test_metadata = [
-    ("projection", expected_proj1, None),
-    ("ll_lon", -10.434576838640398, 1e-10),
-    ("ll_lat", 31.746215319325056, 1e-10),
-    ("ur_lon", 57.81196475014995, 1e-10),
-    ("ur_lat", 67.62103710275053, 1e-10),
-    ("x1", -0.0004161088727414608, 1e-6),
-    ("y1", -4400000.001057557, 1e-10),
-    ("x2", 3800000.0004256153, 1e-10),
-    ("y2", -0.0004262728616595268, 1e-6),
-    ("xpixelsize", 2000.0, 1e-10),
-    ("xpixelsize", 2000.0, 1e-10),
-    ("cartesian_unit", "m", None),
+# list of (variable,expected,tolerance) tuples
+test_attrs = [
+    ("projection", expected_proj, None),
+    ("institution", "Odyssey datacentre", None),
     ("accutime", 15.0, 1e-10),
-    ("yorigin", "upper", None),
     ("unit", "mm/h", None),
-    ("institution", "Odyssey datacentre", None),
     ("transform", None, None),
     ("zerovalue", 0.0, 1e-10),
     ("threshold", 0.01, 1e-10),
 ]
 
 
-@pytest.mark.parametrize("variable,expected,tolerance", test_metadata)
-def test_io_import_opera_hdf5_metadata(variable, expected, tolerance):
+@pytest.mark.parametrize("variable, expected, tolerance", test_attrs)
+def test_io_import_mch_gif_dataset_attrs(variable, expected, tolerance):
     """Test the importer OPERA HDF5."""
-    root_path = pysteps.rcparams.data_sources["opera"]["root_path"]
-    filename = os.path.join(root_path, "20180824", "T_PAAH21_C_EUOC_20180824180000.hdf")
-    _, _, metadata = pysteps.io.import_opera_hdf5(filename)
-    print(metadata)
-    # assert metadata[variable] == expected
-    smart_assert(metadata[variable], expected, tolerance)
+    smart_assert(data_array.attrs[variable], expected, tolerance)
diff --git a/pysteps/tests/test_io_readers.py b/pysteps/tests/test_io_readers.py
new file mode 100644
index 000000000..4feaf87c9
--- /dev/null
+++ b/pysteps/tests/test_io_readers.py
@@ -0,0 +1,38 @@
+from datetime import datetime
+
+import pytest
+import xarray as xr
+
+import pysteps
+
+
+def test_read_timeseries_mch():
+
+    pytest.importorskip("PIL")
+
+    date = datetime.strptime("201505151630", "%Y%m%d%H%M")
+    data_source = pysteps.rcparams.data_sources["mch"]
+    root_path = data_source["root_path"]
+    path_fmt = data_source["path_fmt"]
+    fn_pattern = data_source["fn_pattern"]
+    fn_ext = data_source["fn_ext"]
+    importer_name = data_source["importer"]
+    importer_kwargs = data_source["importer_kwargs"]
+    timestep = data_source["timestep"]
+
+    fns = pysteps.io.archive.find_by_date(
+        date,
+        root_path,
+        path_fmt,
+        fn_pattern,
+        fn_ext,
+        timestep=timestep,
+        num_prev_files=1,
+        num_next_files=1,
+    )
+
+    importer = pysteps.io.get_method(importer_name, "importer")
+    data_array = pysteps.io.read_timeseries(fns, importer, **importer_kwargs)
+
+    assert isinstance(data_array, xr.DataArray)
+    assert data_array.t.size == 3
diff --git a/pysteps/tests/test_io_rmi_nwp.py b/pysteps/tests/test_io_rmi_nwp.py
new file mode 100644
index 000000000..7556eb201
--- /dev/null
+++ b/pysteps/tests/test_io_rmi_nwp.py
@@ -0,0 +1,81 @@
+# -*- coding: utf-8 -*-
+
+# Test the importer for NWP data from the Royal Meteorological Institute of
+# Belgium (ALARO/AROME data in netCDF output).
+
+import os
+import numpy as np
+
+import xarray as xr
+import pytest
+
+import pysteps
+from pysteps.tests.helpers import smart_assert
+
+pytest.importorskip("netCDF4")
+
+root_path = pysteps.rcparams.data_sources["rmi_nwp"]["root_path"]
+rel_path = os.path.join("2021", "07", "04")
+filename = os.path.join(root_path, rel_path, "ao13_2021070412_native_5min.nc")
+data_array_xr = pysteps.io.import_rmi_nwp_xr(filename)
+
+expected_proj = "+proj=lcc +lon_0=4.55 +lat_1=50.8 +lat_2=50.8 +a=6371229 +es=0 +lat_0=50.8 +x_0=365950 +y_0=-365950.000000001"
+
+
+def test_io_import_rmi_nwp_xarray():
+    """Test the importer RMI NWP."""
+    assert isinstance(data_array_xr, xr.DataArray)
+
+
+def test_io_import_rmi_nwp_xarray_shape():
+    """Test the importer RMI NWP shape."""
+    assert isinstance(data_array_xr, xr.DataArray)
+    assert data_array_xr.shape == (24, 564, 564)
+
+
+test_attrs_xr = [
+    ("projection", expected_proj, None),
+    ("institution", "Royal Meteorological Institute of Belgium", None),
+    ("transform", None, None),
+    ("zerovalue", 0.0, 0.1),
+    ("unit", "mm", None),
+    ("accutime", 5, None),
+    ("zr_a", None, None),
+    ("zr_b", None, None),
+    ("xpixelsize", 1300.0, 0.1),
+    ("ypixelsize", 1300.0, 0.1),
+    ("units", "mm", None),
+    ("yorigin", "upper", None),
+]
+
+
+@pytest.mark.parametrize("variable, expected, tolerance", test_attrs_xr)
+def test_io_import_rmi_nwp_xarray_attrs(variable, expected, tolerance):
+    """Test the importer RMI NWP."""
+    smart_assert(data_array_xr.attrs[variable], expected, tolerance)
+
+
+test_attrs_xr_coord_x = [
+    ("cartesian_unit", "m", None),
+    ("x1", 0, 0.1),
+    ("x2", 731900.0, 0.1),
+]
+
+
+@pytest.mark.parametrize("variable, expected, tolerance", test_attrs_xr_coord_x)
+def test_io_import_rmi_nwp_xarray_attrs_coordx(variable, expected, tolerance):
+    """Test the importer RMI NWP."""
+    smart_assert(data_array_xr.x.attrs[variable], expected, tolerance)
+
+
+test_attrs_xr_coord_y = [
+    ("cartesian_unit", "m", None),
+    ("y1", -731900.0, 0.1),
+    ("y2", 0.0, 0.1),
+]
+
+
+@pytest.mark.parametrize("variable, expected, tolerance", test_attrs_xr_coord_y)
+def test_io_import_rmi_nwp_xarray_attrs_coordy(variable, expected, tolerance):
+    """Test the importer RMI NWP."""
+    smart_assert(data_array_xr.y.attrs[variable], expected, tolerance)
diff --git a/pysteps/tests/test_io_saf_crri.py b/pysteps/tests/test_io_saf_crri.py
index 66faa29d2..6e4e5dce9 100644
--- a/pysteps/tests/test_io_saf_crri.py
+++ b/pysteps/tests/test_io_saf_crri.py
@@ -2,6 +2,8 @@
 
 import os
 
+import numpy as np
+import xarray as xr
 import pytest
 
 import pysteps
@@ -36,30 +38,31 @@ def test_io_import_saf_crri_geodata(variable, expected, tolerance):
         root_path, rel_path, "S_NWC_CRR_MSG4_Europe-VISIR_20180601T070000Z.nc"
     )
     geodata = pysteps.io.importers._import_saf_crri_geodata(filename)
-    print(variable, geodata[variable], expected, tolerance)
     smart_assert(geodata[variable], expected, tolerance)
 
 
-test_metadata_crri = [
+root_path = pysteps.rcparams.data_sources["saf"]["root_path"]
+rel_path = "20180601/CRR"
+filename = os.path.join(
+    root_path, rel_path, "S_NWC_CRR_MSG4_Europe-VISIR_20180601T070000Z.nc"
+)
+data_array = pysteps.io.import_saf_crri(filename)
+
+# list of (variable,expected,tolerance) tuples
+test_attrs = [
+    ("projection", expected_proj, None),
+    ("institution", "Agencia Estatal de Meteorología (AEMET)", None),
     ("transform", None, None),
     ("zerovalue", 0.0, 0.1),
     ("unit", "mm/h", None),
     ("accutime", None, None),
-    ("institution", "Agencia Estatal de Meteorología (AEMET)", None),
 ]
 
 
-@pytest.mark.parametrize("variable, expected, tolerance", test_metadata_crri)
-def test_io_import_saf_crri_metadata(variable, expected, tolerance):
+@pytest.mark.parametrize("variable, expected, tolerance", test_attrs)
+def test_io_import_mch_gif_dataset_attrs(variable, expected, tolerance):
     """Test the importer SAF CRRI."""
-    root_path = pysteps.rcparams.data_sources["saf"]["root_path"]
-    rel_path = "20180601/CRR"
-    filename = os.path.join(
-        root_path, rel_path, "S_NWC_CRR_MSG4_Europe-VISIR_20180601T070000Z.nc"
-    )
-    _, _, metadata = pysteps.io.import_saf_crri(filename)
-    print(variable, metadata[variable], expected, tolerance)
-    smart_assert(metadata[variable], expected, tolerance)
+    smart_assert(data_array.attrs[variable], expected, tolerance)
 
 
 test_extent_crri = [
@@ -83,22 +86,17 @@ def test_io_import_saf_crri_extent(extent, expected_extent, expected_shape, tole
     filename = os.path.join(
         root_path, rel_path, "S_NWC_CRR_MSG4_Europe-VISIR_20180601T070000Z.nc"
     )
-    precip, _, metadata = pysteps.io.import_saf_crri(filename, extent=extent)
-    extent_out = (metadata["x1"], metadata["x2"], metadata["y1"], metadata["y2"])
-
-    print(extent, extent_out, expected_extent)
+    data_array = pysteps.io.import_saf_crri(filename, extent=extent)
+
+    xgridsize = np.diff(data_array.x)[0]
+    ygridsize = np.diff(data_array.y)[0]
+    extent_out = (
+        data_array.x.min() - xgridsize / 2,
+        data_array.x.max() + xgridsize / 2,
+        data_array.y.min() - ygridsize / 2,
+        data_array.y.max() + ygridsize / 2,
+    )
+    print([int(out) for out in extent_out])
     smart_assert(extent_out, expected_extent, tolerance)
-    print(precip.shape, expected_shape)
-    smart_assert(precip.shape, expected_shape, tolerance)
-
-
-if __name__ == "__main__":
-
-    for i, args in enumerate(test_geodata_crri):
-        test_io_import_saf_crri_geodata(args[0], args[1], args[2])
-
-    for i, args in enumerate(test_metadata_crri):
-        test_io_import_saf_crri_metadata(args[0], args[1], args[2])
 
-    for i, args in enumerate(test_extent_crri):
-        test_io_import_saf_crri_extent(args[0], args[1], args[2], args[3])
+    smart_assert(data_array.shape, expected_shape, tolerance)
diff --git a/pysteps/tests/test_nowcasts_steps.py b/pysteps/tests/test_nowcasts_steps.py
index ec01998db..f432f1d49 100644
--- a/pysteps/tests/test_nowcasts_steps.py
+++ b/pysteps/tests/test_nowcasts_steps.py
@@ -9,6 +9,11 @@
 from pysteps import io, motion, nowcasts, verification
 from pysteps.tests.helpers import get_precipitation_fields
 
+# TODO: Fix tests for xarray fields
+pytestmark = pytest.mark.skip(
+    "Needs migration to xarray. Problem with cartesian_unit attribute."
+)
+
 steps_arg_names = (
     "n_ens_members",
     "n_cascade_levels",
diff --git a/pysteps/tests/test_plt_animate.py b/pysteps/tests/test_plt_animate.py
index 80cc45f8f..1021080c5 100644
--- a/pysteps/tests/test_plt_animate.py
+++ b/pysteps/tests/test_plt_animate.py
@@ -8,6 +8,9 @@
 from pysteps.tests.helpers import get_precipitation_fields
 from pysteps.visualization.animations import animate
 
+# TODO: Fix tests for xarray fields
+pytestmark = pytest.mark.skip("Needs migration to xarray")
+
 
 def test_animate(tmp_path):
 
diff --git a/pysteps/tests/test_plugins_support.py b/pysteps/tests/test_plugins_support.py
index 91f9f8de5..d64446178 100644
--- a/pysteps/tests/test_plugins_support.py
+++ b/pysteps/tests/test_plugins_support.py
@@ -10,6 +10,9 @@
 import sys
 import tempfile
 
+# TODO: Fix tests for xarray fields
+pytestmark = pytest.mark.skip("Needs migration to xarray")
+
 __ = pytest.importorskip("cookiecutter")
 from cookiecutter.main import cookiecutter
 
diff --git a/pysteps/tests/test_probscores.py b/pysteps/tests/test_probscores.py
index c7f9990b8..b1af325a7 100644
--- a/pysteps/tests/test_probscores.py
+++ b/pysteps/tests/test_probscores.py
@@ -8,7 +8,8 @@
 from pysteps.tests.helpers import get_precipitation_fields
 from pysteps.verification import probscores
 
-precip = get_precipitation_fields(num_next_files=10, return_raw=True)
+# TODO: Fix tests for xarray fields
+precip = get_precipitation_fields(num_next_files=10, return_raw=True, legacy=True)
 
 # CRPS
 test_data = [(precip[:10], precip[-1], 0.01470871)]
diff --git a/pysteps/tests/test_spatialscores.py b/pysteps/tests/test_spatialscores.py
index ac6a2b63c..a2d6714c4 100644
--- a/pysteps/tests/test_spatialscores.py
+++ b/pysteps/tests/test_spatialscores.py
@@ -6,7 +6,8 @@
 from pysteps.tests.helpers import get_precipitation_fields
 from pysteps.verification import spatialscores
 
-R = get_precipitation_fields(num_prev_files=1, return_raw=True)
+# TODO: Fix tests for xarray fields
+R = get_precipitation_fields(num_prev_files=1, return_raw=True, legacy=True)
 test_data = [
     (R[0], R[1], "FSS", [1], [10], None, 0.85161531),
     (R[0], R[1], "BMSE", [1], None, "Haar", 0.99989651),
diff --git a/pysteps/tests/test_timeseries_autoregression.py b/pysteps/tests/test_timeseries_autoregression.py
index f1cc76816..ff3326db4 100644
--- a/pysteps/tests/test_timeseries_autoregression.py
+++ b/pysteps/tests/test_timeseries_autoregression.py
@@ -211,7 +211,7 @@ def _create_data_multivariate():
     R = []
     for fn in filenames:
         filename = os.path.join(root_path, "20160928", fn)
-        R_, _, _ = pysteps.io.import_fmi_pgm(filename, gzipped=True)
+        R_, _, _ = pysteps.io.import_fmi_pgm(filename, gzipped=True, legacy=True)
         R_[~np.isfinite(R_)] = 0.0
         R.append(np.stack([R_, np.roll(R_, 5, axis=0)]))
 
@@ -235,7 +235,7 @@ def _create_data_univariate():
     R = []
     for fn in filenames:
         filename = os.path.join(root_path, "20160928", fn)
-        R_, _, _ = pysteps.io.import_fmi_pgm(filename, gzipped=True)
+        R_, _, _ = pysteps.io.import_fmi_pgm(filename, gzipped=True, legacy=True)
         R_[~np.isfinite(R_)] = 0.0
         R.append(R_)
 
diff --git a/pysteps/tests/test_utils_reprojection.py b/pysteps/tests/test_utils_reprojection.py
new file mode 100644
index 000000000..7bc5c9b10
--- /dev/null
+++ b/pysteps/tests/test_utils_reprojection.py
@@ -0,0 +1,92 @@
+# -*- coding: utf-8 -*-
+
+import os
+import xarray as xr
+import pytest
+import pysteps
+from pysteps.utils import reprojection
+
+pytest.importorskip("rasterio")
+
+root_path_radar = pysteps.rcparams.data_sources["knmi"]["root_path"]
+root_path_nwp = pysteps.rcparams.data_sources["knmi_nwp"]["root_path"]
+
+rel_path_nwp = os.path.join("2018", "09", "05")
+rel_path_radar = os.path.join(
+    "2010", "08"
+)  # Different day, but that does not matter for the tester
+
+filename_nwp = os.path.join(
+    root_path_nwp, rel_path_nwp, "20180905_0600_Pforecast_Harmonie.nc"
+)
+filename_radar = os.path.join(
+    root_path_radar, rel_path_radar, "RAD_NL25_RAP_5min_201008260230.h5"
+)
+
+# Open the radar and NWP data
+radar_array_xr = pysteps.io.importers.import_knmi_hdf5(filename_radar)
+nwp_array_xr = pysteps.io.import_knmi_nwp_xr(filename_nwp)
+
+steps_arg_names = (
+    "radar_array_xr",
+    "nwp_array_xr",
+    "t",
+)
+
+steps_arg_values = [
+    (radar_array_xr, nwp_array_xr, 1),
+    (radar_array_xr, nwp_array_xr, 4),
+    (radar_array_xr, nwp_array_xr, 8),
+]
+
+
+@pytest.mark.parametrize(steps_arg_names, steps_arg_values)
+def test_utils_reprojection(
+    radar_array_xr,
+    nwp_array_xr,
+    t,
+):
+
+    # Reproject
+    nwp_array_xr_reproj = reprojection(nwp_array_xr, radar_array_xr)
+
+    # The tests
+    assert (
+        nwp_array_xr_reproj["t"].shape[0] == nwp_array_xr["t"].shape[0]
+    ), "Time dimension has not the same length as source"
+    assert (
+        nwp_array_xr_reproj["y"].shape[0] == radar_array_xr["y"].shape[0]
+    ), "y dimension has not the same length as radar composite"
+    assert (
+        nwp_array_xr_reproj["x"].shape[0] == radar_array_xr["x"].shape[0]
+    ), "x dimension has not the same length as radar composite"
+
+    assert (
+        nwp_array_xr_reproj.x.attrs["x1"] == radar_array_xr.x.attrs["x1"]
+    ), "x-value lower left corner is not equal to radar composite"
+    assert (
+        nwp_array_xr_reproj.x.attrs["x2"] == radar_array_xr.x.attrs["x2"]
+    ), "x-value upper right corner is not equal to radar composite"
+    assert (
+        nwp_array_xr_reproj.y.attrs["y1"] == radar_array_xr.y.attrs["y1"]
+    ), "y-value lower left corner is not equal to radar composite"
+    assert (
+        nwp_array_xr_reproj.y.attrs["y2"] == radar_array_xr.y.attrs["y2"]
+    ), "y-value upper right corner is not equal to radar composite"
+
+    assert (
+        nwp_array_xr_reproj.x.min().values == radar_array_xr.x.min().values
+    ), "First x-coordinate does not equal first x-coordinate radar composite"
+    assert (
+        nwp_array_xr_reproj.y.min().values == radar_array_xr.y.min().values
+    ), "First y-coordinate does not equal first y-coordinate radar composite"
+    assert (
+        nwp_array_xr_reproj.x.max().values == radar_array_xr.x.max().values
+    ), "Last x-coordinate does not equal last x-coordinate radar composite"
+    assert (
+        nwp_array_xr_reproj.y.max().values == radar_array_xr.y.max().values
+    ), "Last y-coordinate does not equal last y-coordinate radar composite"
+
+    assert (
+        nwp_array_xr_reproj.attrs["projection"] == radar_array_xr.attrs["projection"]
+    ), "projection is different than destionation projection"