Repository: carbonplan/cmip6-downscaling (language: python)

Issue:
Maca fastforward



Discussion / hints:

Fix patch:
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 885b3f3c..d482411c 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -4,7 +4,7 @@ ci:
 
 repos:
   - repo: https://github.com/pre-commit/pre-commit-hooks
-    rev: v4.2.0
+    rev: v4.3.0
     hooks:
       - id: trailing-whitespace
       - id: end-of-file-fixer
@@ -15,14 +15,14 @@ repos:
       - id: mixed-line-ending
 
   - repo: https://github.com/asottile/pyupgrade
-    rev: v2.32.1
+    rev: v2.34.0
     hooks:
       - id: pyupgrade
         args:
           - '--py38-plus'
 
   - repo: https://github.com/psf/black
-    rev: 22.3.0
+    rev: 22.6.0
     hooks:
       - id: black
       - id: black-jupyter
@@ -47,9 +47,10 @@ repos:
       - id: isort
 
   - repo: https://github.com/pre-commit/mirrors-prettier
-    rev: v2.6.2
+    rev: v2.7.1
     hooks:
       - id: prettier
+        language_version: system
         additional_dependencies:
           - prettier
           - '@carbonplan/prettier'
diff --git a/cmip6_downscaling/config.py b/cmip6_downscaling/config.py
index 427e5757..06c0bd50 100644
--- a/cmip6_downscaling/config.py
+++ b/cmip6_downscaling/config.py
@@ -53,7 +53,13 @@
         },
         'gcm_obs_weights': {'uri': 'az://static/xesmf_weights/gcm_obs/weights.csv'},
     },
-    'run_options': {'runtime': "pangeo", 'use_cache': False, 'generate_pyramids': False},
+    'run_options': {
+        'runtime': "pangeo",
+        'use_cache': True,
+        'generate_pyramids': False,
+        'construct_analogs': True,
+        'combine_regions': False,
+    },
     "runtime": {
         "cloud": {
             "storage_prefix": "az://",
diff --git a/cmip6_downscaling/data/cmip.py b/cmip6_downscaling/data/cmip.py
index 324bacf4..74474bcb 100644
--- a/cmip6_downscaling/data/cmip.py
+++ b/cmip6_downscaling/data/cmip.py
@@ -186,14 +186,13 @@ def get_gcm(
         variable_ids=variable,
         time_slice=time_slice,
     )
-    if float(time_slice.stop) < 2015:
-        # you're working with historical data
-        ds_gcm = load_cmip(activity_ids='CMIP', experiment_ids='historical', **kws)
-    elif float(time_slice.start) > 2014:
-        # you're working with future data
-        ds_gcm = load_cmip(activity_ids='ScenarioMIP', experiment_ids=scenario, **kws)
+
+    if scenario == 'historical':
+        activity_id = 'CMIP'
     else:
-        raise ValueError(f'time slice {time_slice} not supported')
+        activity_id = 'ScenarioMIP'
+
+    ds_gcm = load_cmip(activity_ids=activity_id, experiment_ids=scenario, **kws)
     ds_gcm = ds_gcm.reindex(time=sorted(ds_gcm.time.values))
 
     return ds_gcm
diff --git a/cmip6_downscaling/methods/common/containers.py b/cmip6_downscaling/methods/common/containers.py
index 41970afd..a4877094 100644
--- a/cmip6_downscaling/methods/common/containers.py
+++ b/cmip6_downscaling/methods/common/containers.py
@@ -61,17 +61,19 @@ class RunParameters:
     table_id: str
     scenario: str
     variable: str
-    features: list
     latmin: float
     latmax: float
     lonmin: float
     lonmax: float
     train_dates: list
     predict_dates: list
-    bias_correction_method: str
-    bias_correction_kwargs: dict
-    model_type: str
-    model_params: dict
+    features: list = None  # gard only
+    bias_correction_method: str = None  # gard only
+    bias_correction_kwargs: dict = None  # gard only
+    model_type: str = None  # gard only
+    model_params: dict = None  # gard only
+    year_rolling_window: int = None  # maca only
+    day_rolling_window: int = None  # maca only
 
     @property
     def bbox(self) -> BBox:
diff --git a/cmip6_downscaling/methods/common/tasks.py b/cmip6_downscaling/methods/common/tasks.py
index 152bfb48..23fb32f8 100644
--- a/cmip6_downscaling/methods/common/tasks.py
+++ b/cmip6_downscaling/methods/common/tasks.py
@@ -2,7 +2,6 @@
 
 import contextlib
 import datetime
-import functools
 import json
 import os
 import warnings
@@ -28,15 +27,15 @@
 from ...data.cmip import get_gcm
 from ...data.observations import open_era5
 from ...utils import str_to_hash
-from .containers import RunParameters
+from .containers import RunParameters, TimePeriod
 from .utils import (
     blocking_to_zarr,
     calc_auspicious_chunks_dict,
+    is_cached,
     resample_wrapper,
     set_zarr_encoding,
     subset_dataset,
     validate_zarr_store,
-    zmetadata_exists,
 )
 
 xr.set_options(keep_attrs=True)
@@ -52,8 +51,6 @@
 results_dir = UPath(config.get("storage.results.uri")) / version
 use_cache = config.get('run_options.use_cache')
 
-is_cached = functools.partial(validate_zarr_store, raise_on_error=False)
-
 
 @task(log_stdout=True)
 def make_run_parameters(**kwargs) -> RunParameters:
@@ -83,7 +80,7 @@ def get_obs(run_parameters: RunParameters) -> UPath:
     ds_hash = str_to_hash(frmt_str)
     target = intermediate_dir / 'get_obs' / ds_hash
 
-    if use_cache and zmetadata_exists(target):
+    if use_cache and is_cached(target):
         print(f'found existing target: {target}')
         return target
     print(run_parameters)
@@ -100,7 +97,6 @@ def get_obs(run_parameters: RunParameters) -> UPath:
         subset[key].encoding = {}
 
     subset.attrs.update({'title': title}, **get_cf_global_attrs(version=version))
-    print(f'writing {target}', subset)
     subset = set_zarr_encoding(subset)
     blocking_to_zarr(ds=subset, target=target, validate=True, write_empty_chunks=True)
 
@@ -116,49 +112,75 @@ def get_experiment(run_parameters: RunParameters, time_subset: str) -> UPath:
     run_parameters : RunParameters
         RunParameter dataclass defined in common/conatiners.py. Constructed from prefect parameters.
     time_subset : str
-        String describing time subset request. Either 'train_period' or 'predict_period'
+        String describing time subset request. Either 'train_period', 'predict_period', or 'both'.
 
     Returns
     -------
     UPath
         UPath to experiment dataset.
     """
-    time_period = getattr(run_parameters, time_subset)
-    feature_string = '_'.join(run_parameters.features)
 
-    frmt_str = "{model}_{member}_{scenario}_{feature_string}_{latmin}_{latmax}_{lonmin}_{lonmax}_{time_period.start}_{time_period.stop}".format(
-        time_period=time_period, **asdict(run_parameters), feature_string=feature_string
-    )
+    if time_subset == 'both':
+        time_period = TimePeriod(
+            start=str(
+                min(
+                    int(run_parameters.train_period.start), int(run_parameters.predict_period.start)
+                )
+            ),
+            stop=str(
+                max(int(run_parameters.train_period.stop), int(run_parameters.predict_period.stop))
+            ),
+        )
+    else:
+        time_period = getattr(run_parameters, time_subset)
+
+    features = getattr(run_parameters, 'features')
+    if features:
+        feature_string = '_'.join(features)
+        frmt_str = "{model}_{member}_{scenario}_{feature_string}_{latmin}_{latmax}_{lonmin}_{lonmax}_{time_period.start}_{time_period.stop}".format(
+            time_period=time_period, **asdict(run_parameters), feature_string=feature_string
+        )
+
+    else:
+        frmt_str = "{model}_{member}_{scenario}_{variable}_{latmin}_{latmax}_{lonmin}_{lonmax}_{time_period.start}_{time_period.stop}".format(
+            time_period=time_period, **asdict(run_parameters)
+        )
+
+    if int(time_period.start) < 2015 and run_parameters.scenario != 'historical':
+        scenarios = ['historical', run_parameters.scenario]
+    else:
+        scenarios = [run_parameters.scenario]
 
     title = f"experiment ds: {frmt_str}"
     ds_hash = str_to_hash(frmt_str)
     target = intermediate_dir / 'get_experiment' / ds_hash
 
-    print(target)
     if use_cache and is_cached(target):
         print(f'found existing target: {target}')
         return target
 
-    # The for loop is a workaround for github issue: https://github.com/pydata/xarray/issues/6709
     mode = 'w'
     for feature in run_parameters.features:
-        ds = get_gcm(
-            scenario=run_parameters.scenario,
-            member_id=run_parameters.member,
-            table_id=run_parameters.table_id,
-            grid_label=run_parameters.grid_label,
-            source_id=run_parameters.model,
-            variable=feature,
-            time_slice=time_period.time_slice,
-        )
+        ds_list = []
+        for s in scenarios:
+            ds_list.append(
+                get_gcm(
+                    scenario=s,
+                    member_id=run_parameters.member,
+                    table_id=run_parameters.table_id,
+                    grid_label=run_parameters.grid_label,
+                    source_id=run_parameters.model,
+                    variable=feature,
+                    time_slice=time_period.time_slice,
+                )
+            )
+        ds = xr.concat(ds_list, dim='time')
         subset = subset_dataset(ds, feature, time_period.time_slice, run_parameters.bbox)
         # Note: dataset is chunked into time:365 chunks to standardize leap-year chunking.
         subset = subset.chunk({'time': 365})
         for key in subset.variables:
             subset[key].encoding = {}
-
         subset.attrs.update({'title': title}, **get_cf_global_attrs(version=version))
-
         subset = set_zarr_encoding(subset)
         subset[[feature]].to_zarr(target, mode=mode)
         mode = 'a'
@@ -206,8 +228,7 @@ def rechunk(
     task_hash = str_to_hash(str(path) + pattern_string + str(template) + max_mem)
     target = intermediate_dir / 'rechunk' / task_hash
     path_tmp = scratch_dir / 'rechunk' / task_hash
-    print(f'writing rechunked dataset to {target}')
-    print(target)
+
     target_store = fsspec.get_mapper(str(target))
     temp_store = fsspec.get_mapper(str(path_tmp))
 
@@ -243,7 +264,6 @@ def rechunk(
             chunk_dims = config.get(f"chunk_dims.{pattern}")
             for dim in chunk_def:
                 if dim not in chunk_dims:
-                    print('correcting dim')
                     # override the chunksize of those unchunked dimensions to be the complete length (like passing chunksize=-1
                     chunk_def[dim] = len(ds[dim])
     elif pattern is not None:
@@ -316,7 +336,7 @@ def time_summary(ds_path: UPath, freq: str) -> UPath:
 
     ds_hash = str_to_hash(str(ds_path) + freq)
     target = results_dir / 'time_summary' / ds_hash
-    print(target)
+
     if use_cache and is_cached(target):
         print(f'found existing target: {target}')
         return target
@@ -362,7 +382,6 @@ def get_weights(*, run_parameters, direction, regrid_method="bilinear"):
         .iloc[0]
         .path
     )
-    print(path)
     return path
 
 
@@ -385,16 +404,19 @@ def get_pyramid_weights(*, run_parameters, levels: int, regrid_method: str = "bi
         Path to pyramid weights file.
     """
     weights = pd.read_csv(config.get('weights.downscaled_pyramid_weights.uri'))
-    print(weights)
     path = (
         weights[(weights.regrid_method == regrid_method) & (weights.levels == levels)].iloc[0].path
     )
-    print(path)
     return path
 
 
 @task(log_stdout=True)
-def regrid(source_path: UPath, target_grid_path: UPath, weights_path: UPath = None) -> UPath:
+def regrid(
+    source_path: UPath,
+    target_grid_path: UPath,
+    weights_path: UPath = None,
+    pre_chunk_def: dict = None,
+) -> UPath:
     """Task to regrid a dataset to target grid.
 
     Parameters
@@ -416,18 +438,21 @@ def regrid(source_path: UPath, target_grid_path: UPath, weights_path: UPath = No
 
     ds_hash = str_to_hash(str(source_path) + str(target_grid_path))
     target = intermediate_dir / 'regrid' / ds_hash
-    print(target)
 
-    if use_cache and zmetadata_exists(target):
+    if use_cache and is_cached(target):
         print(f'found existing target: {target}')
         return target
+
     source_ds = xr.open_zarr(source_path)
     target_grid_ds = xr.open_zarr(target_grid_path)
+
+    if pre_chunk_def is not None:
+        source_ds = source_ds.chunk(**pre_chunk_def)
+
     if weights_path:
         from ndpyramid.regrid import _reconstruct_xesmf_weights
 
         weights = _reconstruct_xesmf_weights(xr.open_zarr(weights_path))
-        print(weights_path)
         regridder = xe.Regridder(
             source_ds,
             target_grid_ds,
@@ -450,9 +475,9 @@ def regrid(source_path: UPath, target_grid_path: UPath, weights_path: UPath = No
     regridded_ds.attrs.update(
         {'title': source_ds.attrs['title']}, **get_cf_global_attrs(version=version)
     )
+
     regridded_ds = set_zarr_encoding(regridded_ds)
     blocking_to_zarr(ds=regridded_ds, target=target, validate=True, write_empty_chunks=True)
-
     return target
 
 
diff --git a/cmip6_downscaling/methods/common/utils.py b/cmip6_downscaling/methods/common/utils.py
index eabf7eae..f95fc9a5 100644
--- a/cmip6_downscaling/methods/common/utils.py
+++ b/cmip6_downscaling/methods/common/utils.py
@@ -1,5 +1,6 @@
 from __future__ import annotations
 
+import functools
 import pathlib
 import re
 
@@ -66,6 +67,9 @@ def validate_zarr_store(target: str, raise_on_error=True) -> bool:
     return True
 
 
+is_cached = functools.partial(validate_zarr_store, raise_on_error=False)
+
+
 def zmetadata_exists(path: UPath):
     '''temporary workaround until path.exists() works'''
 
diff --git a/cmip6_downscaling/methods/deepsd/utils.py b/cmip6_downscaling/methods/deepsd/deepsd.py
similarity index 100%
rename from cmip6_downscaling/methods/deepsd/utils.py
rename to cmip6_downscaling/methods/deepsd/deepsd.py
diff --git a/cmip6_downscaling/methods/deepsd_config.ini b/cmip6_downscaling/methods/deepsd/deepsd_config.ini
similarity index 100%
rename from cmip6_downscaling/methods/deepsd_config.ini
rename to cmip6_downscaling/methods/deepsd/deepsd_config.ini
diff --git a/cmip6_downscaling/methods/detrend.py b/cmip6_downscaling/methods/detrend.py
deleted file mode 100644
index 1680f8ca..00000000
--- a/cmip6_downscaling/methods/detrend.py
+++ /dev/null
@@ -1,167 +0,0 @@
-from __future__ import annotations
-
-import pandas as pd
-
-
-def add_circular_temporal_pad(data, offset, timeunit='D'):
-    """
-    Pad the beginning of data with the last values of data, and pad the end of data with the first values of data.
-
-    Parameters
-    ----------
-    data: xr.Dataset
-        data to be padded, must have time as a dimension
-    offset: int
-        number of time points to be padded
-    timeunit: str
-        unit of offset. Default is 'D' = days
-
-    Returns
-    -------
-    padded: xr.Dataset
-        The padded dataset
-    """
-
-    padded = data.pad(time=offset, mode='wrap')
-
-    time_coord = padded.time.values
-    time_coord[:offset] = (data.time[:offset] - pd.Timedelta(offset, timeunit)).values
-    time_coord[-offset:] = (data.time[-offset:] + pd.Timedelta(offset, timeunit)).values
-
-    padded = padded.assign_coords({'time': time_coord})
-
-    return padded
-
-
-def days_in_year(year, use_leap_year=False):
-    """
-    Returns the number of days in the year input.
-
-    Parameters
-    ----------
-    year: int
-        year in question
-    use_leap_year: bool
-        whether to return 366 for leap years
-
-    Returns
-    -------
-    n_days_in_year: int
-        Number of days in the year of question
-    """
-    if (year % 4 == 0) and use_leap_year:
-        return 366
-    return 365
-    # there might be calendars with 360 days
-
-
-def pad_with_edge_year(data):
-    """
-    Pad data with the first and last year available. similar to the behavior of np.pad(mode='edge')
-    but uses the 365 edge values instead of repeating only 1 edge value (366 if leap year)
-
-    Parameters
-    ----------
-    data: xr.Dataset
-        data to be padded, must have time as a dimension
-
-    Returns
-    -------
-    padded: xr.Dataset
-        The padded dataset
-    """
-
-    def pad_with(vector, pad_width, iaxis, kwargs):
-        pstart = pad_width[0]
-        pend = pad_width[1]
-        if pstart > 0:
-            start = vector[pstart : pstart * 2]
-            vector[:pstart] = start
-        if pend > 0:
-            end = vector[-pend * 2 : -pend]
-            vector[-pend:] = end
-
-    # TODO: change use_leap_year to True once we unify the GCMs into Gregorian calendars
-    prev_year = days_in_year(data.time[0].dt.year.values - 1, use_leap_year=False)
-    next_year = days_in_year(data.time[-1].dt.year.values + 1, use_leap_year=False)
-    padded = data.pad({'time': (prev_year, next_year)}, mode=pad_with)
-    time_coord = padded.time.values
-    time_coord[:prev_year] = (data.time[:prev_year] - pd.Timedelta(prev_year, 'D')).values
-    time_coord[-next_year:] = (data.time[-next_year:] + pd.Timedelta(next_year, 'D')).values
-
-    padded = padded.assign_coords({'time': time_coord})
-
-    return padded
-
-
-def calc_epoch_trend(data, historical_period, day_rolling_window=21, year_rolling_window=31):
-    """
-    Calculate the epoch trend as a multi-day, multi-year rolling average. The trend is calculated as the anomaly
-    against the historical mean (also a multi-day rolling average).
-
-    Parameters
-    ----------
-    data : xr.Dataset
-        Data to calculate epoch trend on
-    historical_period: slice
-        Slice indicating the historical period to be used when calculating historical mean
-    day_rolling_window: int
-        Number of days to include when calculating the rolling average
-    year_rolling_window: int
-        Number of years to include when calculating the rolling average
-
-    Returns
-    -------
-    trend: xr.Dataset
-        The long term average trend
-    """
-    # the rolling windows need to be odd numbers since the rolling average is centered
-    assert day_rolling_window % 2 == 1
-    d_offset = int((day_rolling_window - 1) / 2)
-
-    assert year_rolling_window % 2 == 1
-    y_offset = int((year_rolling_window - 1) / 2)
-
-    # get historical mean as a rolling average -- the result has one number for each day of year
-    # which is the average of the neighboring day of years over multiple years
-    padded = add_circular_temporal_pad(data=data.sel(time=historical_period), offset=d_offset)
-    hist_mean = (
-        padded.rolling(time=day_rolling_window, center=True)
-        .mean()
-        .dropna('time')
-        .groupby("time.dayofyear")
-        .mean()
-    )
-
-    # get rolling average for the entire data -- the result has one number for each day in the entire time series
-    # which is the average of the neighboring day of years in neighboring years
-    padded = add_circular_temporal_pad(data=data, offset=d_offset)
-    func = lambda x: x.rolling(time=year_rolling_window, center=True).mean()
-    rolling_doy_mean = (
-        padded.rolling(time=day_rolling_window, center=True)
-        .mean()
-        .dropna('time')
-        .groupby('time.dayofyear')
-        .apply(func)
-        .dropna('time')
-    ).compute()  # TODO: this .compute is needed otherwise the pad_with_edge_year function below returns all nulls for unknown reasons, root cause?
-
-    # repeat the first/last year to cover the time periods without enough data for the rolling average
-    for i in range(y_offset):
-        rolling_doy_mean = pad_with_edge_year(rolling_doy_mean)
-
-    # remove historical mean from rolling average, leaving trend as the anamoly
-    trend = rolling_doy_mean.groupby('time.dayofyear') - hist_mean
-
-    assert trend.isnull().sum().values == 0
-
-    return trend
-
-
-def remove_epoch_trend(data, trend, **kwargs):
-    """
-    Subtract the trend from data
-    """
-    ea_data = data - trend
-
-    return ea_data
diff --git a/cmip6_downscaling/methods/maca/__init__.py b/cmip6_downscaling/methods/maca/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/cmip6_downscaling/methods/maca.py b/cmip6_downscaling/methods/maca/core.py
similarity index 52%
rename from cmip6_downscaling/methods/maca.py
rename to cmip6_downscaling/methods/maca/core.py
index 7e895a7e..f8c69f03 100644
--- a/cmip6_downscaling/methods/maca.py
+++ b/cmip6_downscaling/methods/maca/core.py
@@ -6,97 +6,78 @@
 from skdownscale.pointwise_models import EquidistantCdfMatcher, PointWiseDownscaler
 from sklearn.linear_model import LinearRegression
 
-from cmip6_downscaling.workflows.utils import generate_batches
+from .utils import add_circular_temporal_pad, generate_batches, pad_with_edge_year
 
 
-def maca_bias_correction(
-    ds_gcm: xr.Dataset,
-    ds_obs: xr.Dataset,
+def epoch_trend(
+    data: xr.Dataset,
     historical_period: slice,
-    variables: str | list[str],
-    batch_size: int | None = 15,
-    buffer_size: int | None = 15,
+    day_rolling_window: int = 21,
+    year_rolling_window: int = 31,
 ) -> xr.Dataset:
     """
-    Run bias correction as it is done in the MACA method. See https://climate.northwestknowledge.net/MACA/MACAmethod.php
-    for more details. Briefly, the bias correction is performed using the Equidistant CDF matching method in batches.
-    Neighboring day of years are bias corrected together with a buffer. That is, with a batch size of 15 and a buffer size
-    of 15, the 45 neighboring days of year are bias corrected together, but only the result of the center 15 days are used.
-    The historical GCM is mapped to historical coarsened observation in the bias correction.
+    Calculate the epoch trend as a multi-day, multi-year rolling average. The trend is calculated as the anomaly
+    against the historical mean (also a multi-day rolling average).
 
     Parameters
     ----------
-    ds_gcm: xr.Dataset
-        GCM dataset, must have a dimension called time on which we can call .dt.dayofyear on
-    ds_obs: xr.Dataset
-        Observation dataset, must have a dimension called time on which we can call .dt.dayofyear on
-    historical_period: slice
-        The historical period
-    variables: List[str]
-        Names of the variables used in obs and gcm dataset (including features and label)
-    batch_size: Optional[int]
-        The batch size in terms of day of year to bias correct together
-    buffer_size: Optional[int]
-        The buffer size in terms of day of year to include in the bias correction
+    data : xr.Dataset
+        Data to calculate epoch trend on
+    historical_period : slice
+        Slice indicating the historical period to be used when calculating historical mean
+    day_rolling_window : int
+        Number of days to include when calculating the rolling average
+    year_rolling_window : int
+        Number of years to include when calculating the rolling average
 
     Returns
     -------
-    ds_out: xr.Dataset
-        The bias corrected dataset
+    trend : xr.Dataset
+        The long term average trend
     """
-    if isinstance(variables, str):
-        variables = [variables]
-
-    doy_gcm = ds_gcm.time.dt.dayofyear
-    doy_obs = ds_obs.time.dt.dayofyear
-
-    ds_out = xr.Dataset()
-    for var in variables:
-        if var in ['pr', 'huss', 'vas', 'uas']:
-            kind = 'ratio'
-        else:
-            kind = 'difference'
-
-        bias_correction_model = PointWiseDownscaler(
-            EquidistantCdfMatcher(
-                kind=kind, extrapolate=None  # cdf in maca implementation spans [0, 1]
-            )
-        )
-
-        batches, cores = generate_batches(
-            n=doy_gcm.max().values, batch_size=batch_size, buffer_size=buffer_size, one_indexed=True
-        )
-
-        bc_result = []
-        # TODO: currently running in sequence but can be mapped out into separate workers/runners
-        for i, (b, c) in enumerate(zip(batches, cores)):
-            gcm_batch = ds_gcm.sel(time=doy_gcm.isin(b))
-            obs_batch = ds_obs.sel(time=doy_obs.isin(b))
-
-            train_x = gcm_batch.sel(time=historical_period)[[var]]
-            train_y = obs_batch.sel(time=historical_period)[var]
-
-            # TODO: this is a total hack to get around the different calendars of observation dataset and GCM
-            # should be able to remove once we unify all calendars
-            if len(train_x.time) > len(train_y.time):
-                train_x = train_x.isel(time=slice(0, len(train_y.time)))
-            elif len(train_x.time) < len(train_y.time):
-                train_y = train_y.isel(time=slice(0, len(train_x.time)))
-            train_x = train_x.assign_coords({'time': train_y.time})
+    # the rolling windows need to be odd numbers since the rolling average is centered
+    assert day_rolling_window % 2 == 1
+    d_offset = int((day_rolling_window - 1) / 2)
+
+    assert year_rolling_window % 2 == 1
+    y_offset = int((year_rolling_window - 1) / 2)
+
+    # get historical mean as a rolling average -- the result has one number for each day of year
+    # which is the average of the neighboring day of years over multiple years
+    padded = add_circular_temporal_pad(data=data.sel(time=historical_period), offset=d_offset)
+    hist_mean = (
+        padded.rolling(time=day_rolling_window, center=True)
+        .mean()
+        .dropna('time')
+        .groupby("time.dayofyear")
+        .mean()
+    )
 
-            bias_correction_model.fit(
-                train_x.unify_chunks(),  # dataset
-                train_y.unify_chunks(),  # dataarray
-            )
+    # get rolling average for the entire data -- the result has one number for each day in the entire time series
+    # which is the average of the neighboring day of years in neighboring years
+    padded = add_circular_temporal_pad(data=data, offset=d_offset)
+
+    def func(x):
+        window = min(len(x.time), year_rolling_window)
+        return x.rolling(time=window, center=True).mean()
+
+    rolling_doy_mean = (
+        padded.rolling(time=day_rolling_window, center=True)
+        .mean()
+        .dropna('time')
+        .groupby('time.dayofyear')
+        .apply(func)
+        .dropna('time')
+    )
 
-            bc_data = bias_correction_model.predict(X=gcm_batch.unify_chunks())
-            # needs the .compute here otherwise the code fails with errors
-            # TODO: not sure if this is scalable
-            bc_result.append(bc_data.sel(time=bc_data.time.dt.dayofyear.isin(c)).compute())
+    # repeat the first/last year to cover the time periods without enough data for the rolling average
+    for i in range(y_offset):
+        rolling_doy_mean = pad_with_edge_year(rolling_doy_mean)
 
-        ds_out[var] = xr.concat(bc_result, dim='time').sortby('time')
+    # remove historical mean from rolling average, leaving trend as the anamoly
+    trend = rolling_doy_mean.groupby('time.dayofyear') - hist_mean
 
-    return ds_out
+    return trend
 
 
 def get_doy_mask(
@@ -111,14 +92,14 @@ def get_doy_mask(
 
     Parameters
     ----------
-    source_doy: xr.DataArray
+    source_doy : xr.DataArray
         1D xr data array with day of year information
-    target_doy: xr.DataArray
+    target_doy : xr.DataArray
         1D xr data array with day of year information
 
     Returns
     -------
-    mask: xr.DataArray
+    mask : xr.DataArray
         2D xr data array of boolean type in the shape of len(target_doy) x len(source_doy)
     """
     # get the min and max doy within doy_range days to target doy
@@ -142,15 +123,98 @@ def get_doy_mask(
     return mask
 
 
-def maca_construct_analogs(
+def bias_correction(
+    ds_gcm: xr.Dataset,
+    ds_obs: xr.Dataset,
+    variables: list[str],
+    batch_size: int = 15,
+    buffer_size: int = 15,
+) -> xr.Dataset:
+    """
+    Run bias correction as it is done in the MACA method.
+
+    The bias correction is performed using the Equidistant CDF matching method in batches.
+    Neighboring day of years are bias corrected together with a buffer. That is, with a batch
+    size of 15 and a buffer size of 15, the 45 neighboring days of year are bias corrected
+    together, but only the result of the center 15 days are used. The historical GCM is mapped
+    to historical coarsened observation in the bias correction.
+
+    Parameters
+    ----------
+    ds_gcm : xr.Dataset
+        GCM dataset, must have a dimension called time on which we can call .dt.dayofyear on
+    ds_obs : xr.Dataset
+        Observation dataset, must have a dimension called time on which we can call .dt.dayofyear on
+    variables : List[str]
+        Names of the variables used in obs and gcm dataset (including features and label)
+    batch_size : Optional[int]
+        The batch size in terms of day of year to bias correct together
+    buffer_size : Optional[int]
+        The buffer size in terms of day of year to include in the bias correction
+
+    Returns
+    -------
+    ds_out : xr.Dataset
+        The bias corrected dataset
+
+    See Also
+    --------
+    https://climate.northwestknowledge.net/MACA/MACAmethod.php
+    """
+
+    # map_blocks work around
+    if 't2' in ds_obs.coords:
+        ds_obs = ds_obs.rename({'t2': 'time'})
+
+    if isinstance(variables, str):
+        variables = [variables]
+
+    doy_gcm = ds_gcm.time.dt.dayofyear
+    doy_obs = ds_obs.time.dt.dayofyear
+
+    ds_out = xr.Dataset()
+    for var in variables:
+        if var in ['pr', 'huss', 'vas', 'uas']:
+            kind = 'ratio'
+        else:
+            kind = 'difference'
+
+        bias_correction_model = PointWiseDownscaler(
+            EquidistantCdfMatcher(
+                kind=kind, extrapolate=None  # cdf in maca implementation spans [0, 1]
+            )
+        )
+
+        batches, cores = generate_batches(
+            n=doy_gcm.max().values, batch_size=batch_size, buffer_size=buffer_size, one_indexed=True
+        )
+
+        bc_result = []
+        # TODO: currently running in sequence but can be mapped out into separate workers/runners
+        for i, (b, c) in enumerate(zip(batches, cores)):
+            gcm_batch = ds_gcm.sel(time=doy_gcm.isin(b))
+            obs_batch = ds_obs.sel(time=doy_obs.isin(b))
+
+            train_x, train_y = xr.align(gcm_batch[[var]], obs_batch[var], join='inner', copy=True)
+
+            bias_correction_model.fit(train_x, train_y)
+
+            bc_data = bias_correction_model.predict(X=gcm_batch)
+            bc_result.append(bc_data.sel(time=bc_data.time.dt.dayofyear.isin(c)))
+
+        ds_out[var] = xr.concat(bc_result, dim='time').sortby('time')
+
+    return ds_out
+
+
+def construct_analogs(
     ds_gcm: xr.Dataset,
     ds_obs_coarse: xr.Dataset,
     ds_obs_fine: xr.Dataset,
     label: str,
     n_analogs: int = 10,
     doy_range: int = 45,
-    **kwargs,
-) -> xr.DataArray:
+) -> xr.Dataset:
     """
     Find analog days for each coarse scale GCM day from coarsened observations, then use the fine scale versions of
     these analogs to construct the downscaled GCM data. The fine scale analogs are combined together using a linear
@@ -160,46 +224,64 @@ def maca_construct_analogs(
 
     Parameters
     ----------
-    ds_gcm: xr.Dataset
+    ds_gcm : xr.Dataset
         GCM dataset, original/coarse resolution
-    ds_obs_coarse: xr.Dataset
+    ds_obs_coarse : xr.Dataset
         Observation dataset coarsened to the GCM resolution
-    ds_obs_fine: xr.Dataset
+    ds_obs_fine : xr.Dataset
         Observation dataset, original/fine resolution
-    label: str
+    label : str
         Name of variable to be downscaled
-    n_analogs: int
+    n_analogs : int
         Number of analog days to look for
-    doy_range: int
+    doy_range : int
         The range of day of year to look for analogs within
 
     Returns
     -------
-    downscaled: xr.Dataset
+    downscaled : xr.Dataset
         The downscaled dataset
     """
-    # make sure the input data is valid with the correct shape and dims
     for dim in ['time', 'lat', 'lon']:
         for ds in [ds_gcm, ds_obs_coarse, ds_obs_fine]:
             assert dim in ds.dims
-    assert len(ds_obs_coarse.time) == len(ds_obs_fine.time)
+    if len(ds_obs_coarse.time) != len(ds_obs_fine.time):
+        raise ValueError('ds_obs_coarse is not the same length as ds_obs_fine')
 
     # work with dataarrays instead of datasets
-    ds_gcm = ds_gcm[label]
-    ds_obs_coarse = ds_obs_coarse[label]
-    ds_obs_fine = ds_obs_fine[label]
+    da_gcm = ds_gcm[label]
+    da_obs_coarse = ds_obs_coarse[label]
+    da_obs_fine = ds_obs_fine[label]
+
+    # initialize a regridder to interpolate the residuals from coarse to fine scale later
+    def _make_template(da):
+        temp = da.isel(time=0)
+        template = temp.to_dataset(name=da.name)
+        template['mask'] = temp.notnull()
+        return template
+
+    coarse_template = _make_template(da_obs_coarse)
+    fine_template = _make_template(da_obs_fine)
+
+    regridder = xe.Regridder(
+        coarse_template,
+        fine_template,
+        "bilinear",
+        extrap_method="nearest_s2d",
+    )
 
     # get dimension sizes from input data
-    domain_shape_coarse = (len(ds_obs_coarse.lat), len(ds_obs_coarse.lon))
+    domain_shape_coarse = (len(da_obs_coarse.lat), len(da_obs_coarse.lon))
     n_pixel_coarse = domain_shape_coarse[0] * domain_shape_coarse[1]
 
     # rename the time dimension to keep track of them
-    X = ds_obs_coarse.rename({'time': 'ndays_in_obs'})  # coarse obs
-    y = ds_gcm.rename({'time': 'ndays_in_gcm'})  # coarse gcm
+    X = da_obs_coarse.rename({'time': 'ndays_in_obs'})  # coarse obs
+    y = da_gcm.rename({'time': 'ndays_in_gcm'})  # coarse gcm
 
     # get rmse between each GCM slices to be downscaled and each observation slices
     # will have the shape ndays_in_gcm x ndays_in_obs
     rmse = np.sqrt(((X - y) ** 2).sum(dim=['lat', 'lon'])) / n_pixel_coarse
+    rmse = rmse.load()
 
     # get a day of year mask in the same shape of rmse according to the day range input
     mask = get_doy_mask(
@@ -211,72 +293,67 @@ def maca_construct_analogs(
     # find the indices with the lowest rmse within the day of year constraint
     dim_order = ['ndays_in_gcm', 'ndays_in_obs']
     inds = (
-        xr.apply_ufunc(
-            np.argsort,
-            rmse.where(mask),
-            vectorize=True,
-            input_core_dims=[['ndays_in_obs']],
-            output_core_dims=[['ndays_in_obs']],
-            dask='parallelized',
-            output_dtypes=['int'],
-            dask_gufunc_kwargs={'allow_rechunk': True},
-        )
+        rmse.where(mask)
+        .argsort(axis=rmse.get_axis_num('ndays_in_obs'))
         .isel(ndays_in_obs=slice(0, n_analogs))
         .transpose(*dim_order)
-        .compute()
     )
-
     # rearrage the data into tabular format in order to train linear regression models to get coefficients
     X = X.stack(pixel_coarse=['lat', 'lon'])
+    X = X.where(X.notnull(), drop=True)
     y = y.stack(pixel_coarse=['lat', 'lon'])
+    y = y.where(y.notnull(), drop=True)
 
     # initialize models to be used
     lr_model = LinearRegression()
 
-    # TODO: check if the rechunk can be removed
-    # initialize a regridder to interpolate the residuals from coarse to fine scale later
-    coarse_template = ds_obs_coarse.isel(time=0).chunk({'lat': -1, 'lon': -1})
-    fine_template = ds_obs_fine.isel(time=0).chunk({'lat': -1, 'lon': -1})
-    regridder = xe.Regridder(
-        coarse_template,
-        fine_template,
-        "bilinear",
-        extrap_method="nearest_s2d",
-    )
-
-    # initialize output
+    # initialize temporary variables
     downscaled = []
+    residuals = []
+    coefs = []
+    intercepts = []
+    obs_analogs = []
+
+    # pre-load to speed up for loop (lots of indexing into x & y)
+    X = X.load()
+    y = y.load()
+
     # train a linear regression model for each day in coarsen GCM dataset, where the features are each coarsened observation
     # analogs, and examples are each pixels within the coarsened domain
     for i in range(len(y)):
+
         # get data from the GCM day being downscaled
         yi = y.isel(ndays_in_gcm=i)
         # get data from the coarsened obs analogs
         ind = inds.isel(ndays_in_gcm=i).values
+
+        # save obs_analogs for later
+        obs_analogs.append(da_obs_fine.isel(time=ind).rename({'time': 'analog'}).drop('analog'))
+
         xi = X.isel(ndays_in_obs=ind).transpose('pixel_coarse', 'ndays_in_obs')
 
         # fit model
-        lr_model.fit(xi, yi)
-        # save the residuals to be interpolated and included in the final prediction
-        residual = yi - lr_model.predict(xi)
-
-        # chunk so that residual is spatially contiguous before interpolation
-        # TODO: check if the rechunk can be removed
-        residual = residual.unstack('pixel_coarse').chunk({'lat': -1, 'lon': -1})
-        interpolated_residual = regridder(residual)
-
-        # construct fine scale prediction by combining the fine scale analogs with the coefficients found in the linear model
-        # then add the intercept and interpolated residuals
-        fine_pred = (
-            (ds_obs_fine.isel(time=ind).transpose('lat', 'lon', 'time') * lr_model.coef_).sum(
-                dim='time'
-            )
-            + lr_model.intercept_
-            + interpolated_residual
-        )
-        fine_pred = fine_pred.rename({'ndays_in_gcm': 'time'}).drop('dayofyear')
-        downscaled.append(fine_pred)
+        lr_model.fit(xi.data, yi.data)
+        coefs.append(lr_model.coef_)
+        intercepts.append(lr_model.intercept_)
+
+        residual = yi - lr_model.predict(xi.data)
+        residual = residual.unstack('pixel_coarse')
+
+        residuals.append(residual)
+
+    # reconstruct xarray objects from temporary lists
+    residuals = xr.concat(residuals, dim='ndays_in_gcm').rename({'ndays_in_gcm': 'time'})
+    coefs = xr.DataArray(coefs, dims=('time', 'analog'), coords={'time': residuals.time})
+    intercepts = xr.DataArray(intercepts, coords={'time': residuals.time})
+    obs_analogs = xr.concat(obs_analogs, dim='time')
+
+    # interpolate residuals to fine grid
+    interpolated_residual = regridder(residuals)
+
+    # combine obs analogs with residuals
+    fine_pred = (obs_analogs * coefs).sum(dim='analog') + intercepts + interpolated_residual
+
+    downscaled = fine_pred.to_dataset(name=label)
 
-    downscaled = xr.concat(downscaled, dim='time').sortby('time')
-    downscaled = downscaled.to_dataset(name=label)
     return downscaled
diff --git a/cmip6_downscaling/methods/maca/tasks.py b/cmip6_downscaling/methods/maca/tasks.py
new file mode 100644
index 00000000..778ee4c2
--- /dev/null
+++ b/cmip6_downscaling/methods/maca/tasks.py
@@ -0,0 +1,394 @@
+from __future__ import annotations
+
+from datetime import timedelta
+
+import dask
+import regionmask
+import xarray as xr
+import zarr
+from carbonplan_data.metadata import get_cf_global_attrs
+from prefect import task
+from upath import UPath
+
+from cmip6_downscaling import __version__ as version, config
+from cmip6_downscaling.methods.common.containers import RunParameters
+from cmip6_downscaling.methods.common.utils import blocking_to_zarr, is_cached
+from cmip6_downscaling.methods.maca import core as maca_core
+from cmip6_downscaling.methods.maca.utils import (
+    initialize_out_store,
+    make_regions_mask,
+    merge_block_to_zarr,
+)
+from cmip6_downscaling.utils import str_to_hash
+
+intermediate_dir = UPath(config.get("storage.intermediate.uri")) / version
+results_dir = UPath(config.get("storage.results.uri")) / version
+use_cache = config.get('run_options.use_cache')
+
+
+@task(log_stdout=True)
+def bias_correction(x_path: UPath, y_path: UPath, run_parameters: RunParameters) -> UPath:
+    """
+    MACA bias correction task
+
+    Parameters
+    ----------
+    x_path : UPath
+        Path to dataset to be bias corrected
+    y_path : UPath
+        Path to target dataset
+    run_parameters : RunParameters
+        Downscaling run parameter container
+
+    Returns
+    -------
+    target : UPath
+    """
+
+    ds_hash = str_to_hash(str(x_path) + str(y_path))
+    target = intermediate_dir / 'bias_correction' / ds_hash
+
+    if use_cache and is_cached(target):
+        print(f"found existing target: {target}")
+        return target
+
+    x_ds = xr.open_zarr(x_path).chunk({'time': -1})
+    y_ds = xr.open_zarr(y_path).chunk({'time': -1})
+
+    bc_ds = xr.map_blocks(
+        maca_core.bias_correction,
+        x_ds,
+        args=(y_ds.rename({'time': 't2'}),),
+        kwargs=dict(variables=[run_parameters.variable]),
+        template=x_ds,
+    )
+
+    bc_ds.attrs.update({'title': 'bias_correction'}, **get_cf_global_attrs(version=version))
+
+    blocking_to_zarr(ds=bc_ds, target=target, validate=True, write_empty_chunks=True)
+
+    return target
+
+
+@task(log_stdout=True, nout=2)
+def epoch_trend(
+    data_path: UPath,
+    run_parameters: RunParameters,
+) -> tuple[UPath, UPath]:
+    """
+    Task to calculate the epoch trends in MACA.
+
+    The epoch trend is a long term rolling average, and thus the first and last few years of the
+    output suffers from edge effects. Thus, this task gets additional years for calculating the
+    rolling averages.
+
+    Parameters
+    ----------
+    data_path : UPath
+        Input data path
+    run_parameters : RunParameters
+        Downscaling run parameter container
+
+    Returns
+    -------
+    trend_target : UPath
+        Path to the trend
+    detrend_target : UPath
+        Path to the detrended data
+
+    See also
+    --------
+    replace_epoch_trend
+    """
+
+    # TODO: figure out how this task should differ for ['pr', 'huss', 'vas', 'uas']
+
+    ds_hash = str_to_hash(str(data_path) + run_parameters.run_id_hash)
+    trend_target = intermediate_dir / 'epoch_trend' / ds_hash
+    detrend_target = intermediate_dir / 'epoch_detrend' / ds_hash
+
+    if use_cache and is_cached(trend_target) and is_cached(detrend_target):
+        print(f'found existing targets: {trend_target} and {detrend_target}')
+        return trend_target, detrend_target
+
+    # obtain a buffer period for both training and prediction period equal to half of the year_rolling_window
+    y_offset = int((run_parameters.year_rolling_window - 1) / 2)
+
+    train_end = int(run_parameters.train_period.stop) + y_offset
+    predict_start = int(run_parameters.predict_period.start) - y_offset
+    # TODO: why do we do this step?
+    # make sure there are no overlapping years
+    if train_end > int(run_parameters.train_period.start):
+        train_end = int(run_parameters.train_period.start) - 1
+        predict_start = int(run_parameters.train_period.start)
+    elif train_end > predict_start:
+        predict_start = train_end + 1
+
+    ds_gcm_full_time = xr.open_zarr(data_path).load()
+
+    # note that this is the non-buffered slice
+    # TODO: this could probably be sped up with map_blocks
+    historical_period = run_parameters.train_period.time_slice
+    trend = maca_core.epoch_trend(
+        data=ds_gcm_full_time,
+        historical_period=historical_period,
+        day_rolling_window=run_parameters.day_rolling_window,
+        year_rolling_window=run_parameters.year_rolling_window,
+    )
+
+    if 'dayofyear' in trend.variables:
+        trend = trend.drop('dayofyear')
+
+    trend = trend.chunk({'lat': 48, 'lon': 48})
+    trend.attrs.update({'title': 'epoch_trend'}, **get_cf_global_attrs(version=version))
+
+    blocking_to_zarr(ds=trend, target=trend_target, validate=True, write_empty_chunks=True)
+
+    detrended_data = ds_gcm_full_time - trend
+    detrended_data = detrended_data.chunk({'time': -1, 'lat': 48, 'lon': 48})
+
+    detrended_data.attrs.update(
+        {'title': 'epoch_trend - detrended'}, **get_cf_global_attrs(version=version)
+    )
+    blocking_to_zarr(
+        ds=detrended_data, target=detrend_target, validate=True, write_empty_chunks=True
+    )
+
+    return trend_target, detrend_target
+
+
+@task(log_stdout=True, max_retries=5, retry_delay=timedelta(seconds=1))
+def construct_analogs(
+    gcm_path: UPath,
+    coarse_obs_path: UPath,
+    fine_obs_path: UPath,
+    run_parameters: RunParameters,
+) -> UPath:
+    """
+    MACA analog construction
+
+    Parameters
+    ----------
+    gcm_path : UPath
+        Path to GCM dataset
+    coarse_obs_path : UPath
+        Path to coarse obs dataset
+    fine_obs_path : UPath
+        Path to fine obs dataset
+    run_parameters : RunParameters
+        Downscaling run parameter container
+
+    Returns
+    -------
+    target : UPath
+    """
+
+    ds_hash = str_to_hash(
+        str(gcm_path) + str(coarse_obs_path) + str(fine_obs_path) + run_parameters.run_id_hash
+    )
+    target = intermediate_dir / 'construct_analogs' / ds_hash
+
+    if use_cache and is_cached(target):
+        print(f"found existing target: {target}")
+        return target
+
+    # TODO: see if this can be removed
+    # Originally put here to avoid killed-worker / memory errors
+    with dask.config.set(scheduler='threads'):
+        ds_gcm = xr.open_zarr(gcm_path)
+        ds_obs_coarse = xr.open_zarr(coarse_obs_path)
+        ds_obs_fine = xr.open_zarr(fine_obs_path)
+
+        analogs = maca_core.construct_analogs(
+            ds_gcm, ds_obs_coarse, ds_obs_fine, run_parameters.variable
+        )
+
+        analogs = analogs.chunk({'lat': -1, 'lon': -1, 'time': 365})
+        analogs.attrs.update(
+            {'title': 'construct_analogs ' + ds_gcm.attrs['title']},
+            **get_cf_global_attrs(version=version),
+        )
+        blocking_to_zarr(ds=analogs, target=target, validate=True, write_empty_chunks=True)
+
+    return target
+
+
+def _get_regions(region_def):
+    """helper function to extract regionmaks regions"""
+
+    if region_def == 'ar6.land':
+        regions = regionmask.defined_regions.ar6.land
+    else:
+        regions = getattr(regionmask.defined_regions, region_def)
+
+    return regions
+
+
+@task(log_stdout=True)
+def get_region_numbers(region_def: str = 'ar6.land'):
+    regions = _get_regions(region_def)
+    return regions.numbers[:-2]  # drop antarctica
+
+
+@task(log_stdout=True)
+def split_by_region(region: int, data_path: UPath, region_def: str = 'ar6.land') -> list[UPath]:
+    """
+    Split dataset into separate regions
+
+    Parameters
+    ----------
+    region : int
+        Region key
+    data_path : UPath
+        Path to dataset
+    region_def : str, optional
+        Regionmask key, by default `ar6.land`
+
+    Returns
+    -------
+    target : UPath
+
+    See also
+    --------
+    combine_regions
+    """
+
+    regions = _get_regions(region_def)
+
+    target = (
+        intermediate_dir
+        / 'split_by_region'
+        / str_to_hash(str(data_path) + region_def + str(region))
+    )
+
+    if use_cache and is_cached(target):
+        print(f"found existing target: {target}")
+        return target
+
+    ds = xr.open_zarr(data_path)
+
+    with dask.config.set(**{'array.slicing.split_large_chunks': False}):
+        mask = regions.mask(ds)
+        ds_region = ds.where(mask == region, drop=True)
+        ds_region = ds_region.chunk({'lat': -1, 'lon': -1, 'time': 365})
+    ds_region.attrs.update({'title': f'region {region}'}, **get_cf_global_attrs(version=version))
+    blocking_to_zarr(ds=ds_region, target=target, validate=True, write_empty_chunks=True)
+
+    return target
+
+
+@task(log_stdout=True)
+def combine_regions(
+    regions: list,
+    region_paths: list[UPath],
+    template_path: UPath,
+) -> UPath:
+    """
+    Combine regions
+
+    Parameters
+    ----------
+    regions : list of ints
+        Region keys
+    region_paths : list of UPath
+        Paths to region datasets
+    template_path : UPath
+        Path to template dataset, defines output schema
+
+    Returns
+    -------
+    target : UPath
+
+    See also
+    --------
+    split_by_region
+    """
+
+    target = (
+        results_dir
+        / 'combine_regions'
+        / str_to_hash(str(regions) + str(region_paths) + str(template_path))
+    )
+
+    if use_cache and is_cached(target):
+        print(f"found existing target: {target}")
+        return target
+
+    n_chunk_per_block = 4
+    chunk_size = 48
+
+    out_time_index = xr.open_zarr(region_paths[0]).time.values
+    template = initialize_out_store(template_path, target, out_time_index, chunk_size=chunk_size)
+
+    mask = make_regions_mask(template.isel(time=0), chunk_size=chunk_size)
+
+    region_paths_dict = dict(zip(regions, region_paths))
+
+    block_size = chunk_size * n_chunk_per_block
+    # Iterate over chunks and merge pieces within each chunk
+    total = []
+    for ilon in range(0, mask.sizes['lon'], block_size):
+        if ilon <= mask.sizes['lon'] - block_size:
+            xslice = slice(ilon, ilon + block_size)
+        else:
+            xslice = slice(ilon, mask.sizes['lon'])
+        for ilat in range(0, mask.sizes['lat'], block_size):
+            if ilat <= mask.sizes['lat'] - block_size:
+                yslice = slice(ilat, ilat + block_size)
+            else:
+                yslice = slice(ilat, mask.sizes['lat'])
+            total.append(
+                merge_block_to_zarr(
+                    mask.isel(lon=xslice, lat=yslice),
+                    template.isel(lon=xslice, lat=yslice),
+                    region_paths_dict,
+                    target,
+                    xslice=xslice,
+                    yslice=yslice,
+                )
+            )
+    # TODO: merge_block_to_zarr was originally design to be a dask.delayed function
+    # But this wasn't playing nice with dask+prefect.
+    # result = dask.compute(*total, scheduler='threads')
+    zarr.consolidate_metadata(target)
+    return target
+
+
+@task(log_stdout=True)
+def replace_epoch_trend(analogs_path: UPath, trend_path: UPath) -> UPath:
+    """
+    Replace epoch trend
+
+    Parameters
+    ----------
+    analogs_path : UPath
+        Path to constructed analogs
+    trend_path : UPath
+        Path to trend dataset
+
+    Returns
+    -------
+    target : UPath
+
+    See also
+    --------
+    epoch_trend
+    """
+
+    target = results_dir / 'replace_epoch_trend' / str_to_hash(str(analogs_path) + str(trend_path))
+
+    if use_cache and is_cached(target):
+        print(f"found existing target: {target}")
+        return target
+
+    analogs_ds = xr.open_zarr(analogs_path)
+    trend_ds = xr.open_zarr(trend_path)
+
+    downscaled = analogs_ds + trend_ds
+
+    downscaled.attrs.update(
+        {'title': 'replace_epoch_trend'}, **get_cf_global_attrs(version=version)
+    )
+    blocking_to_zarr(ds=downscaled, target=target, validate=True, write_empty_chunks=True)
+
+    return target
diff --git a/cmip6_downscaling/methods/maca/utils.py b/cmip6_downscaling/methods/maca/utils.py
new file mode 100644
index 00000000..fb479498
--- /dev/null
+++ b/cmip6_downscaling/methods/maca/utils.py
@@ -0,0 +1,274 @@
+from __future__ import annotations
+
+import dask
+import numpy as np
+import pandas as pd
+import regionmask
+import xarray as xr
+from carbonplan_data.metadata import get_cf_global_attrs
+from upath import UPath
+
+from ... import __version__ as version
+
+
+def add_circular_temporal_pad(data: xr.Dataset, offset: int, timeunit: str = 'D') -> xr.Dataset:
+    """
+    Pad the beginning of data with the last values of data, and pad the end of data with the first values of data.
+
+    Parameters
+    ----------
+    data : xr.Dataset
+        data to be padded, must have time as a dimension
+    offset : int
+        number of time points to be padded
+    timeunit : str
+        unit of offset. Default is 'D' = days
+
+    Returns
+    -------
+    padded: xr.Dataset
+        The padded dataset
+    """
+
+    padded = data.pad(time=offset, mode='wrap')
+
+    time_coord = padded.time.values
+    time_coord[:offset] = (data.time[:offset] - pd.Timedelta(offset, timeunit)).values
+    time_coord[-offset:] = (data.time[-offset:] + pd.Timedelta(offset, timeunit)).values
+
+    padded = padded.assign_coords({'time': time_coord})
+
+    return padded
+
+
+def days_in_year(year: int, use_leap_year: bool = False) -> int:
+    """
+    Returns the number of days in the year input.
+
+    Parameters
+    ----------
+    year : int
+        year in question
+    use_leap_year : bool
+        whether to return 366 for leap years
+
+    Returns
+    -------
+    n_days_in_year : int
+        Number of days in the year of question
+    """
+    if (year % 4 == 0) and use_leap_year:
+        return 366
+    return 365
+
+
+def pad_with_edge_year(data: xr.Dataset) -> xr.Dataset:
+    """
+    Pad data with the first and last year available. similar to the behavior of np.pad(mode='edge')
+    but uses the 365 edge values instead of repeating only 1 edge value (366 if leap year)
+
+    Parameters
+    ----------
+    data : xr.Dataset
+        data to be padded, must have time as a dimension
+
+    Returns
+    -------
+    padded : xr.Dataset
+        The padded dataset
+    """
+
+    def pad_with(vector, pad_width, iaxis, kwargs):
+        pstart = pad_width[0]
+        pend = pad_width[1]
+        if pstart > 0:
+            start = vector[pstart : pstart * 2]
+            vector[:pstart] = start
+        if pend > 0:
+            end = vector[-pend * 2 : -pend]
+            vector[-pend:] = end
+
+    prev_year = days_in_year(data.time[0].dt.year.values - 1, use_leap_year=True)
+    next_year = days_in_year(data.time[-1].dt.year.values + 1, use_leap_year=True)
+    padded = data.pad({'time': (prev_year, next_year)}, mode=pad_with)
+    time_coord = padded.time.values
+    time_coord[:prev_year] = (data.time[:prev_year] - pd.Timedelta(prev_year, 'D')).values
+    time_coord[-next_year:] = (data.time[-next_year:] + pd.Timedelta(next_year, 'D')).values
+
+    padded = padded.assign_coords({'time': time_coord})
+
+    return padded
+
+
+def generate_batches(
+    n: int, batch_size: int, buffer_size: int, one_indexed: bool = False
+) -> tuple[list, list]:
+    """
+    Given the max value n, batch_size, and buffer_size, returns batches (include the buffer) and
+    cores (exclude the buffer). For the smallest numbers, the largest values would be included in the buffer, and
+    vice versa. For example, with n=10, batch_size=5, buffer_size=3, one_indexed=False. The `cores` output will contain
+    [[0, 1, 2, 3, 4], [5, 6, 7, 8, 9]], and `batches` output will contain [[7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7], [2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2]].
+
+    Parameters
+    ----------
+    n : int
+        The max value to be included.
+    batch_size : int
+        The number of core values to include in each batch.
+    buffer_size : int
+        The number of buffer values to include in each batch in both directions.
+    one_indexed : bool
+        Whether we should consider n to be one indexed or not. With n = 2, one_indexed=False would generate cores containing [0, 1].
+        ``one_indexed=True`` would generate cores containing [1, 2].
+
+    Returns
+    -------
+    batches : List
+        List of batches including buffer values.
+    cores : List
+        List of core values in each batch excluding buffer values.
+    """
+    cores = []
+    batches = []
+    if one_indexed:
+        xmin = 1
+        xmax = n + 1
+    else:
+        xmin = 0
+        xmax = n
+    for start in range(xmin, xmax, batch_size):
+        end = min(start + batch_size, xmax)
+        cores.append(np.arange(start, end))
+
+        # add buffer
+        end = end + buffer_size
+        start = start - buffer_size
+        batch = np.arange(start, end)
+        batch[batch < xmin] += n
+        batch[batch > xmax - 1] -= n
+        batches.append(batch)
+
+    return batches, cores
+
+
+def initialize_out_store(
+    template_path: UPath, out_path: str, time_index: xr.DataArray, chunk_size: int = 48
+) -> xr.Dataset:
+    """Write the empty zarr store where you'll write your final outputs chunk-by-chunk.
+
+    Parameters
+    ----------
+    template_path : UPath
+
+    out_path : str
+        Path where you want the final output store to exist
+    time_index : xr.DataArray
+        Time index to apply to the output store
+    chunk_size : int, optional
+        Length and width of chunk size spatially (will be full time), by default 48
+
+    Returns
+    -------
+    xr.Dataset
+        Template dataset
+    """
+
+    template = xr.full_like(xr.open_zarr(template_path), np.nan)
+    template = template.chunk({'lon': chunk_size, 'lat': chunk_size, 'time': -1})
+    template.attrs.update({'title': 'combine_regions'}, **get_cf_global_attrs(version=version))
+
+    for v in template.data_vars:
+        template[v].encoding['write_empty_chunks'] = True
+
+    template.to_zarr(
+        out_path,
+        compute=False,
+        mode="w",
+    )
+    return template
+
+
+def make_regions_mask(template_one_timeslice: xr.DataArray, chunk_size=48) -> xr.Dataset:
+    """Make the categorical mask showing which region each pixel is assigned to.
+
+    Parameters
+    ----------
+    template_one_timeslice : xr.DataArray
+        Example time slice of the final output dataset.
+    chunk_size : int, optional
+        Width and length of chunk that will be written (will be full-time), by default 48
+
+    Returns
+    -------
+    xr.Dataset
+        Integer mask with pixels numbered according to each region number.
+    """
+    mask = (
+        regionmask.defined_regions.ar6.land.mask(template_one_timeslice).fillna(46).astype(np.byte)
+    )
+    mask = mask.chunk({'lon': chunk_size, 'lat': chunk_size})
+    return mask
+
+
+# @dask.delayed
+def merge_block_to_zarr(
+    mask: xr.DataArray,
+    template: xr.Dataset,
+    split_region_paths: dict[int, UPath],
+    out_path: UPath,
+    *,
+    xslice: slice,
+    yslice: slice,
+):
+    """
+    Find ar6 regions in each block, merge and reindex, write to zarr
+
+    Parameters
+    ----------
+    mask : xr.DataArray
+        Integer mask to determine which region to pull data from
+    template : xr.Dataset
+        Dataset defining the target schema
+    split_region_paths : dict
+        Mapping of region key to region path
+    out_path : UPath
+        Target dataset path
+    xslice : slice
+        Latitude slice
+    yslice : slice
+        Longitude slice
+
+    """
+    print(xslice, yslice)
+
+    components = pd.unique(mask.values.ravel())
+    components = [c for c in components if c in split_region_paths]
+    if len(components) > 0:
+        with dask.config.set(**{'array.slicing.split_large_chunks': False}):
+            merged = (
+                xr.merge(
+                    xr.open_zarr(split_region_paths[ind])
+                    .where(mask.isin(ind), drop=True)
+                    .sortby(["lon", "lat"])
+                    for ind in components
+                )
+                .reindex_like(template)
+                .sortby("lat", ascending=True)
+            ).load()
+
+        for variable in merged.data_vars:
+            merged[variable].encoding['write_empty_chunks'] = True
+
+        merged.drop(['lat', 'lon', 'time']).to_zarr(
+            out_path,
+            region={'lat': yslice, 'lon': xslice, 'time': slice(0, merged.sizes['time'])},
+            mode="r+",
+        )
+    else:
+        print('writing blank')
+        template.drop(['lat', 'lon', 'time']).to_zarr(
+            out_path,
+            region={'lat': yslice, 'lon': xslice, 'time': slice(0, template.sizes['time'])},
+            mode="r+",
+        )
+    return out_path
diff --git a/cmip6_downscaling/methods/regions.py b/cmip6_downscaling/methods/regions.py
deleted file mode 100644
index 4ecac3bb..00000000
--- a/cmip6_downscaling/methods/regions.py
+++ /dev/null
@@ -1,111 +0,0 @@
-from __future__ import annotations
-
-from typing import Any
-
-import numpy as np
-import regionmask
-import xarray as xr
-
-
-def generate_subdomains(
-    ex_output_grid: xr.Dataset | xr.DataArray,
-    buffer_size: float | int,
-    region_def: str = 'ar6',
-) -> tuple[dict[int | float, Any], xr.DataArray]:
-    """
-    Given an example output grid, determine all subdomains that need to be process in order to generate the final output.
-    Outputs the list of bounding boxes for each subdomain considering the buffer size, as well as a mask in the resolution of the example output grid specifying
-    which subdomain's value to use for each grid cell. Longitudes are in -180 to 180
-
-    Parameters
-    ----------
-    ex_output_grid : xarray.DataArray or xarray.Dataset
-        Example output grid definition. both the bounding box and resolution in lat/lon directions will be used.
-    buffer_size : int or float
-        Buffer size in unit of degree. for each subdomain, how much extra area to run for each subdomain
-    region_def : str
-        Subregion definition name. Options are `'ar6'` or `'srex'`. See the docs https://regionmask.readthedocs.io/en/stable/defined_scientific.html for more details.
-
-    Returns
-    -------
-    subdomains : dict
-        Dictionary mapping subdomain code to bounding boxes ([min_lon, min_lat, max_lon, max_lat]) for each subdomain
-    mask : xarray.DataArray
-        Mask of which subdomain code to use for each grid cell
-    """
-    if region_def == 'ar6':
-        # if we want to include oceans, do regionmask.defined_regions.ar6.all
-        regions = regionmask.defined_regions.ar6.land
-    elif region_def == 'srex':
-        regions = regionmask.defined_regions.srex
-    else:
-        raise NotImplementedError('region_def must be eitehr ar6 or srex')
-
-    mask = regions.mask(ex_output_grid)
-    region_codes = np.unique(mask.values)
-    region_codes = region_codes[np.isfinite(region_codes)]
-
-    subdomains = {}
-    for n, bound in zip(regions.numbers, regions.bounds):
-        if n in region_codes:
-            # max(low, min(high, value))
-            min_lon = max(min(bound[0] - buffer_size, 180), -180)
-            min_lat = max(min(bound[1] - buffer_size, 90), -90)
-            max_lon = max(min(bound[2] + buffer_size, 180), -180)
-            max_lat = max(min(bound[3] + buffer_size, 90), -90)
-            # there is a small region of eastern siberia that is part of region 28 of AR6
-            # but since it's difficult to get a subdomain crossing the -180 longitude line, add this region into region 1 instead
-            if n == 1 and region_def == 'ar6':
-                min_lon = -180
-            elif n == 28 and region_def == 'ar6':
-                min_lon = 40.0 - buffer_size
-            mask = xr.where((mask == 28) & (mask.lon > 180), x=1, y=mask)
-
-            subdomains[n] = (min(min_lon, max_lon), min_lat, max(min_lon, max_lon), max_lat)
-    return subdomains, mask
-
-
-def combine_outputs(
-    ds_dict: dict[float | int, xr.Dataset],
-    mask: xr.DataArray,
-) -> xr.Dataset:
-    """
-    Combines values in ds_dict according to mask. Mask should be a 2D dataarray with lat/lon as the dimensions. The values in mask should
-    correspond to the keys in ds_dict.
-
-    Parameters
-    ----------
-    ds_dict : dict
-        Dictionary mapping subdomain code to output (subdomain code : xr.Dataset)
-    mask : xarray.DataArray
-        Mask of which subdomain code to use for each grid cell (2D, lat/lon)
-
-    Returns
-    --------
-    ds : xarray.Dataset
-        The combined output where values come from the respective ds in ds_dict according to mask
-    """
-    # compare the region codes in ds_dict and in mask to make sure that they match with each other
-    region_codes_available = list(ds_dict.keys())
-    region_codes = np.unique(mask.values)
-    region_codes = region_codes[np.isfinite(region_codes)]
-    for code in region_codes:
-        assert code in region_codes_available
-
-    # construct the output
-    out = xr.Dataset()
-    template = ds_dict[region_codes_available[0]]
-    for v in template.data_vars:
-        # TODO: this might be prohibitive in terms of memory usage
-        out[v] = xr.DataArray(
-            np.nan,
-            dims=template.dims,
-            coords={'time': template.time, 'lat': mask.lat, 'lon': mask.lon},
-        )
-        for code in region_codes:
-            # this is the values in ds_dict, which contains the information for each region separately
-            single_region_output = ds_dict[code][v]
-            single_region_output = single_region_output.reindex({'lat': mask.lat, 'lon': mask.lon})
-            out[v] = xr.where(mask == code, single_region_output, out[v])
-
-    return out
diff --git a/configs/generate_valid_configs/generate_valid_json_parameters.ipynb b/configs/generate_valid_configs/generate_valid_json_parameters.ipynb
index 9715e9e2..d2432bf0 100644
--- a/configs/generate_valid_configs/generate_valid_json_parameters.ipynb
+++ b/configs/generate_valid_configs/generate_valid_json_parameters.ipynb
@@ -10,7 +10,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 2,
    "id": "f52aa05a-d924-4704-b98b-4195eb199d8d",
    "metadata": {},
    "outputs": [],
@@ -23,7 +23,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 11,
    "id": "8381f7da-dbcd-4850-9695-e480e01bbdd5",
    "metadata": {},
    "outputs": [],
@@ -73,12 +73,30 @@
     "        \"model_type\": \"PureRegression\",\n",
     "        \"model_params\": {},\n",
     "    },\n",
+    "    \"maca\": {\n",
+    "        \"method\": \"\",\n",
+    "        \"obs\": \"ERA5\",\n",
+    "        \"model\": \"\",\n",
+    "        \"member\": \"\",\n",
+    "        \"grid_label\": \"gn\",\n",
+    "        \"table_id\": \"day\",\n",
+    "        \"scenario\": \"\",\n",
+    "        \"variable\": \"\",\n",
+    "        \"train_dates\": [\"1981\", \"2010\"],\n",
+    "        \"predict_dates\": \"\",\n",
+    "        \"latmin\": \"-90\",\n",
+    "        \"latmax\": \"90\",\n",
+    "        \"lonmin\": \"-180\",\n",
+    "        \"lonmax\": \"180\",\n",
+    "        \"year_rolling_window\": 31,\n",
+    "        \"day_rolling_window\": 21,\n",
+    "    },\n",
     "}"
    ]
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 12,
    "id": "18086475-1332-43ef-bbc8-dec49b0e822b",
    "metadata": {},
    "outputs": [],
@@ -98,39 +116,68 @@
   },
   {
    "cell_type": "code",
-   "execution_count": null,
+   "execution_count": 13,
    "id": "62b4cafc-976b-4a00-a4e0-05f7cac2b37e",
-   "metadata": {
-    "tags": []
-   },
-   "outputs": [],
+   "metadata": {},
+   "outputs": [
+    {
+     "name": "stderr",
+     "output_type": "stream",
+     "text": [
+      "4it [00:00, 29.80it/s]\n",
+      "4it [00:00, 34.49it/s]\n",
+      "4it [00:00, 42.07it/s]\n",
+      "4it [00:00, 41.90it/s]\n",
+      "4it [00:00, 42.15it/s]\n",
+      "4it [00:00, 42.39it/s]\n",
+      "4it [00:00, 42.25it/s]\n",
+      "4it [00:00, 48.47it/s]\n",
+      "4it [00:00, 42.34it/s]\n",
+      "4it [00:00, 43.26it/s]\n",
+      "4it [00:00, 41.83it/s]\n",
+      "4it [00:00, 41.79it/s]\n",
+      "4it [00:00, 42.70it/s]\n",
+      "4it [00:00, 42.71it/s]\n",
+      "4it [00:00, 42.69it/s]\n"
+     ]
+    }
+   ],
    "source": [
-    "for method in ['bcsd', 'gard']:\n",
+    "for method in ['maca']:  # ['bcsd', 'gard', 'maca']:\n",
     "    for gcm in gcms:\n",
     "        for variable in variables:\n",
     "            subset = valid_df[valid_df['source_id'] == gcm]\n",
     "\n",
     "            subset = subset[subset['member_id'] == member]\n",
     "            subset = subset[subset['variable_id_y'] == variable]\n",
+    "\n",
     "            for index, row in tqdm(subset.iterrows()):\n",
     "                filled_template = template[method].copy()\n",
     "                filled_template[\"method\"] = method\n",
     "                filled_template[\"member\"] = row[\"member_id\"]\n",
     "                filled_template[\"model\"] = row[\"source_id\"]\n",
-    "                filled_template[\"features\"] = [row[\"variable_id_y\"]]\n",
     "                filled_template[\"variable\"] = row[\"variable_id_y\"]\n",
+    "                if method == 'maca' and 'hist' in row[\"experiment_id_y\"]:\n",
+    "                    continue\n",
     "                filled_template[\"scenario\"] = row[\"experiment_id_y\"]\n",
     "\n",
-    "                if 'ssp' in filled_template[\"scenario\"]:\n",
-    "                    filled_template[\"predict_dates\"] = [\"2015\", \"2099\"]\n",
-    "                else:\n",
-    "                    filled_template[\"predict_dates\"] = [\"1950\", \"2014\"]\n",
+    "                if method == 'maca':\n",
+    "                    filled_template[\"predict_dates\"] = [\"1950\", \"2099\"]\n",
     "\n",
-    "                if variable == 'pr':\n",
-    "                    filled_template['model_params'] = {'thresh': 0}\n",
-    "                filled_template['bias_correction_kwargs'] = bias_correction_kwargs\n",
-    "                filled_template['train_dates'] = filled_template.pop('train_dates')\n",
-    "                filled_template['predict_dates'] = filled_template.pop('predict_dates')\n",
+    "                elif method == 'gard':\n",
+    "                    filled_template[\"features\"] = [row[\"variable_id_y\"]]\n",
+    "                    if 'ssp' in filled_template[\"scenario\"]:\n",
+    "                        filled_template[\"predict_dates\"] = [\"2015\", \"2099\"]\n",
+    "                    else:\n",
+    "                        filled_template[\"predict_dates\"] = [\"1950\", \"2014\"]\n",
+    "\n",
+    "                    if variable == 'pr':\n",
+    "                        filled_template['model_params'] = {'thresh': 0}\n",
+    "                        filled_template['bias_correction_kwargs'] = {\"detrend\": \"False\"}\n",
+    "                    elif variable in ['tasmax', 'tasmin']:\n",
+    "                        filled_template['bias_correction_kwargs'] = {\"detrend\": \"True\"}\n",
+    "                    filled_template['train_dates'] = filled_template.pop('train_dates')\n",
+    "                    filled_template['predict_dates'] = filled_template.pop('predict_dates')\n",
     "\n",
     "                run_parameters = RunParameters(**filled_template)\n",
     "\n",
@@ -141,63 +188,17 @@
   {
    "cell_type": "code",
    "execution_count": null,
-   "id": "6fc57c86-74dc-4df7-8079-39dc829079fe",
+   "id": "acf21a3e-8283-4423-a4f3-e0ff7e123ceb",
    "metadata": {},
    "outputs": [],
    "source": [
-    "variable_feature_combinations = {\n",
-    "    'tasmax': [\n",
-    "        ['ua', 'va', 'tasmax'],\n",
-    "        ['ua', 'va', 'psl', 'tasmax'],\n",
-    "    ],\n",
-    "    'pr': [\n",
-    "        ['ua', 'va', 'psl', 'pr'],\n",
-    "        ['ua', 'va', 'psl'],\n",
-    "        ['ua', 'va', 'psl', 'tasmax'],\n",
-    "        ['ua', 'va', 'pr'],\n",
-    "    ],\n",
-    "}\n",
-    "\n",
-    "for method in ['gard_multivariate']:\n",
-    "    for variable, feature_combinations in variable_feature_combinations.items():\n",
-    "        for features in feature_combinations:\n",
-    "            for gcm in gcms[0:1]:\n",
-    "                print(gcm, variable)\n",
-    "                subset = valid_df[valid_df['source_id'] == gcm]\n",
-    "\n",
-    "                subset = subset[subset['member_id'] == member]\n",
-    "                subset = subset[subset['variable_id_y'] == variable]\n",
-    "                print(len(subset))\n",
-    "\n",
-    "                for index, row in tqdm(subset.iterrows()):\n",
-    "                    filled_template = template['gard'].copy()\n",
-    "                    filled_template[\"method\"] = method\n",
-    "                    filled_template[\"member\"] = row[\"member_id\"]\n",
-    "                    filled_template[\"model\"] = row[\"source_id\"]\n",
-    "                    filled_template[\"features\"] = features\n",
-    "                    filled_template[\"variable\"] = row[\"variable_id_y\"]\n",
-    "                    filled_template[\"scenario\"] = row[\"experiment_id_y\"]\n",
-    "\n",
-    "                    if 'ssp' in filled_template[\"scenario\"]:\n",
-    "                        filled_template[\"predict_dates\"] = [\"2015\", \"2099\"]\n",
-    "                    else:\n",
-    "                        filled_template[\"predict_dates\"] = [\"1950\", \"2014\"]\n",
-    "                    if variable == 'pr':\n",
-    "                        filled_template['model_params'] = {'thresh': 0}\n",
-    "                    filled_template['bias_correction_kwargs'] = bias_correction_kwargs\n",
-    "                    filled_template['train_dates'] = filled_template.pop('train_dates')\n",
-    "                    filled_template['predict_dates'] = filled_template.pop('predict_dates')\n",
-    "\n",
-    "                    run_parameters = RunParameters(**filled_template)\n",
-    "\n",
-    "                    with open(f'{method}' + '/' + run_parameters.run_id + '.json', 'w') as f:\n",
-    "                        f.write(json.dumps(filled_template))"
+    "obs"
    ]
   },
   {
    "cell_type": "code",
    "execution_count": null,
-   "id": "875270a2-d740-4f69-8795-1ec657f864d8",
+   "id": "ed7f61d1-f997-4a6c-8d3b-f7e72350ba7d",
    "metadata": {},
    "outputs": [],
    "source": []
@@ -205,9 +206,9 @@
  ],
  "metadata": {
   "kernelspec": {
-   "display_name": "Python 3 (ipykernel)",
+   "display_name": "Python [conda env:notebook] *",
    "language": "python",
-   "name": "python3"
+   "name": "conda-env-notebook-py"
   },
   "language_info": {
    "codemirror_mode": {
diff --git a/configs/generate_valid_configs/maca/maca_ERA5_BCC-CSM2-MR_r1i1p1f1_ssp245_pr_-90_90_-180_180_1981_2010_1950_2099.json b/configs/generate_valid_configs/maca/maca_ERA5_BCC-CSM2-MR_r1i1p1f1_ssp245_pr_-90_90_-180_180_1981_2010_1950_2099.json
new file mode 100644
index 00000000..25b8bd49
--- /dev/null
+++ b/configs/generate_valid_configs/maca/maca_ERA5_BCC-CSM2-MR_r1i1p1f1_ssp245_pr_-90_90_-180_180_1981_2010_1950_2099.json
@@ -0,0 +1,18 @@
+{
+  "method": "maca",
+  "obs": "ERA5",
+  "model": "BCC-CSM2-MR",
+  "member": "r1i1p1f1",
+  "grid_label": "gn",
+  "table_id": "day",
+  "scenario": "ssp245",
+  "variable": "pr",
+  "train_dates": ["1981", "2010"],
+  "predict_dates": ["1950", "2099"],
+  "latmin": "-90",
+  "latmax": "90",
+  "lonmin": "-180",
+  "lonmax": "180",
+  "year_rolling_window": 31,
+  "day_rolling_window": 21
+}
diff --git a/configs/generate_valid_configs/maca/maca_ERA5_BCC-CSM2-MR_r1i1p1f1_ssp245_tasmax_-90_90_-180_180_1981_2010_1950_2099.json b/configs/generate_valid_configs/maca/maca_ERA5_BCC-CSM2-MR_r1i1p1f1_ssp245_tasmax_-90_90_-180_180_1981_2010_1950_2099.json
new file mode 100644
index 00000000..7a96901c
--- /dev/null
+++ b/configs/generate_valid_configs/maca/maca_ERA5_BCC-CSM2-MR_r1i1p1f1_ssp245_tasmax_-90_90_-180_180_1981_2010_1950_2099.json
@@ -0,0 +1,18 @@
+{
+  "method": "maca",
+  "obs": "ERA5",
+  "model": "BCC-CSM2-MR",
+  "member": "r1i1p1f1",
+  "grid_label": "gn",
+  "table_id": "day",
+  "scenario": "ssp245",
+  "variable": "tasmax",
+  "train_dates": ["1981", "2010"],
+  "predict_dates": ["1950", "2099"],
+  "latmin": "-90",
+  "latmax": "90",
+  "lonmin": "-180",
+  "lonmax": "180",
+  "year_rolling_window": 31,
+  "day_rolling_window": 21
+}
diff --git a/configs/generate_valid_configs/maca/maca_ERA5_BCC-CSM2-MR_r1i1p1f1_ssp245_tasmin_-90_90_-180_180_1981_2010_1950_2099.json b/configs/generate_valid_configs/maca/maca_ERA5_BCC-CSM2-MR_r1i1p1f1_ssp245_tasmin_-90_90_-180_180_1981_2010_1950_2099.json
new file mode 100644
index 00000000..32576b69
--- /dev/null
+++ b/configs/generate_valid_configs/maca/maca_ERA5_BCC-CSM2-MR_r1i1p1f1_ssp245_tasmin_-90_90_-180_180_1981_2010_1950_2099.json
@@ -0,0 +1,18 @@
+{
+  "method": "maca",
+  "obs": "ERA5",
+  "model": "BCC-CSM2-MR",
+  "member": "r1i1p1f1",
+  "grid_label": "gn",
+  "table_id": "day",
+  "scenario": "ssp245",
+  "variable": "tasmin",
+  "train_dates": ["1981", "2010"],
+  "predict_dates": ["1950", "2099"],
+  "latmin": "-90",
+  "latmax": "90",
+  "lonmin": "-180",
+  "lonmax": "180",
+  "year_rolling_window": 31,
+  "day_rolling_window": 21
+}
diff --git a/configs/generate_valid_configs/maca/maca_ERA5_BCC-CSM2-MR_r1i1p1f1_ssp370_pr_-90_90_-180_180_1981_2010_1950_2099.json b/configs/generate_valid_configs/maca/maca_ERA5_BCC-CSM2-MR_r1i1p1f1_ssp370_pr_-90_90_-180_180_1981_2010_1950_2099.json
new file mode 100644
index 00000000..1862b840
--- /dev/null
+++ b/configs/generate_valid_configs/maca/maca_ERA5_BCC-CSM2-MR_r1i1p1f1_ssp370_pr_-90_90_-180_180_1981_2010_1950_2099.json
@@ -0,0 +1,18 @@
+{
+  "method": "maca",
+  "obs": "ERA5",
+  "model": "BCC-CSM2-MR",
+  "member": "r1i1p1f1",
+  "grid_label": "gn",
+  "table_id": "day",
+  "scenario": "ssp370",
+  "variable": "pr",
+  "train_dates": ["1981", "2010"],
+  "predict_dates": ["1950", "2099"],
+  "latmin": "-90",
+  "latmax": "90",
+  "lonmin": "-180",
+  "lonmax": "180",
+  "year_rolling_window": 31,
+  "day_rolling_window": 21
+}
diff --git a/configs/generate_valid_configs/maca/maca_ERA5_BCC-CSM2-MR_r1i1p1f1_ssp370_tasmax_-90_90_-180_180_1981_2010_1950_2099.json b/configs/generate_valid_configs/maca/maca_ERA5_BCC-CSM2-MR_r1i1p1f1_ssp370_tasmax_-90_90_-180_180_1981_2010_1950_2099.json
new file mode 100644
index 00000000..39e2ea27
--- /dev/null
+++ b/configs/generate_valid_configs/maca/maca_ERA5_BCC-CSM2-MR_r1i1p1f1_ssp370_tasmax_-90_90_-180_180_1981_2010_1950_2099.json
@@ -0,0 +1,18 @@
+{
+  "method": "maca",
+  "obs": "ERA5",
+  "model": "BCC-CSM2-MR",
+  "member": "r1i1p1f1",
+  "grid_label": "gn",
+  "table_id": "day",
+  "scenario": "ssp370",
+  "variable": "tasmax",
+  "train_dates": ["1981", "2010"],
+  "predict_dates": ["1950", "2099"],
+  "latmin": "-90",
+  "latmax": "90",
+  "lonmin": "-180",
+  "lonmax": "180",
+  "year_rolling_window": 31,
+  "day_rolling_window": 21
+}
diff --git a/configs/generate_valid_configs/maca/maca_ERA5_BCC-CSM2-MR_r1i1p1f1_ssp370_tasmin_-90_90_-180_180_1981_2010_1950_2099.json b/configs/generate_valid_configs/maca/maca_ERA5_BCC-CSM2-MR_r1i1p1f1_ssp370_tasmin_-90_90_-180_180_1981_2010_1950_2099.json
new file mode 100644
index 00000000..c703b361
--- /dev/null
+++ b/configs/generate_valid_configs/maca/maca_ERA5_BCC-CSM2-MR_r1i1p1f1_ssp370_tasmin_-90_90_-180_180_1981_2010_1950_2099.json
@@ -0,0 +1,18 @@
+{
+  "method": "maca",
+  "obs": "ERA5",
+  "model": "BCC-CSM2-MR",
+  "member": "r1i1p1f1",
+  "grid_label": "gn",
+  "table_id": "day",
+  "scenario": "ssp370",
+  "variable": "tasmin",
+  "train_dates": ["1981", "2010"],
+  "predict_dates": ["1950", "2099"],
+  "latmin": "-90",
+  "latmax": "90",
+  "lonmin": "-180",
+  "lonmax": "180",
+  "year_rolling_window": 31,
+  "day_rolling_window": 21
+}
diff --git a/configs/generate_valid_configs/maca/maca_ERA5_BCC-CSM2-MR_r1i1p1f1_ssp585_pr_-90_90_-180_180_1981_2010_1950_2099.json b/configs/generate_valid_configs/maca/maca_ERA5_BCC-CSM2-MR_r1i1p1f1_ssp585_pr_-90_90_-180_180_1981_2010_1950_2099.json
new file mode 100644
index 00000000..d7c669bf
--- /dev/null
+++ b/configs/generate_valid_configs/maca/maca_ERA5_BCC-CSM2-MR_r1i1p1f1_ssp585_pr_-90_90_-180_180_1981_2010_1950_2099.json
@@ -0,0 +1,18 @@
+{
+  "method": "maca",
+  "obs": "ERA5",
+  "model": "BCC-CSM2-MR",
+  "member": "r1i1p1f1",
+  "grid_label": "gn",
+  "table_id": "day",
+  "scenario": "ssp585",
+  "variable": "pr",
+  "train_dates": ["1981", "2010"],
+  "predict_dates": ["1950", "2099"],
+  "latmin": "-90",
+  "latmax": "90",
+  "lonmin": "-180",
+  "lonmax": "180",
+  "year_rolling_window": 31,
+  "day_rolling_window": 21
+}
diff --git a/configs/generate_valid_configs/maca/maca_ERA5_BCC-CSM2-MR_r1i1p1f1_ssp585_tasmax_-90_90_-180_180_1981_2010_1950_2099.json b/configs/generate_valid_configs/maca/maca_ERA5_BCC-CSM2-MR_r1i1p1f1_ssp585_tasmax_-90_90_-180_180_1981_2010_1950_2099.json
new file mode 100644
index 00000000..47846a85
--- /dev/null
+++ b/configs/generate_valid_configs/maca/maca_ERA5_BCC-CSM2-MR_r1i1p1f1_ssp585_tasmax_-90_90_-180_180_1981_2010_1950_2099.json
@@ -0,0 +1,18 @@
+{
+  "method": "maca",
+  "obs": "ERA5",
+  "model": "BCC-CSM2-MR",
+  "member": "r1i1p1f1",
+  "grid_label": "gn",
+  "table_id": "day",
+  "scenario": "ssp585",
+  "variable": "tasmax",
+  "train_dates": ["1981", "2010"],
+  "predict_dates": ["1950", "2099"],
+  "latmin": "-90",
+  "latmax": "90",
+  "lonmin": "-180",
+  "lonmax": "180",
+  "year_rolling_window": 31,
+  "day_rolling_window": 21
+}
diff --git a/configs/generate_valid_configs/maca/maca_ERA5_BCC-CSM2-MR_r1i1p1f1_ssp585_tasmin_-90_90_-180_180_1981_2010_1950_2099.json b/configs/generate_valid_configs/maca/maca_ERA5_BCC-CSM2-MR_r1i1p1f1_ssp585_tasmin_-90_90_-180_180_1981_2010_1950_2099.json
new file mode 100644
index 00000000..765ec4fd
--- /dev/null
+++ b/configs/generate_valid_configs/maca/maca_ERA5_BCC-CSM2-MR_r1i1p1f1_ssp585_tasmin_-90_90_-180_180_1981_2010_1950_2099.json
@@ -0,0 +1,18 @@
+{
+  "method": "maca",
+  "obs": "ERA5",
+  "model": "BCC-CSM2-MR",
+  "member": "r1i1p1f1",
+  "grid_label": "gn",
+  "table_id": "day",
+  "scenario": "ssp585",
+  "variable": "tasmin",
+  "train_dates": ["1981", "2010"],
+  "predict_dates": ["1950", "2099"],
+  "latmin": "-90",
+  "latmax": "90",
+  "lonmin": "-180",
+  "lonmax": "180",
+  "year_rolling_window": 31,
+  "day_rolling_window": 21
+}
diff --git a/configs/generate_valid_configs/maca/maca_ERA5_CanESM5_r1i1p1f1_ssp245_pr_-90_90_-180_180_1981_2010_1950_2099.json b/configs/generate_valid_configs/maca/maca_ERA5_CanESM5_r1i1p1f1_ssp245_pr_-90_90_-180_180_1981_2010_1950_2099.json
new file mode 100644
index 00000000..f2190c4e
--- /dev/null
+++ b/configs/generate_valid_configs/maca/maca_ERA5_CanESM5_r1i1p1f1_ssp245_pr_-90_90_-180_180_1981_2010_1950_2099.json
@@ -0,0 +1,18 @@
+{
+  "method": "maca",
+  "obs": "ERA5",
+  "model": "CanESM5",
+  "member": "r1i1p1f1",
+  "grid_label": "gn",
+  "table_id": "day",
+  "scenario": "ssp245",
+  "variable": "pr",
+  "train_dates": ["1981", "2010"],
+  "predict_dates": ["1950", "2099"],
+  "latmin": "-90",
+  "latmax": "90",
+  "lonmin": "-180",
+  "lonmax": "180",
+  "year_rolling_window": 31,
+  "day_rolling_window": 21
+}
diff --git a/configs/generate_valid_configs/maca/maca_ERA5_CanESM5_r1i1p1f1_ssp245_tasmax_-90_90_-180_180_1981_2010_1950_2099.json b/configs/generate_valid_configs/maca/maca_ERA5_CanESM5_r1i1p1f1_ssp245_tasmax_-90_90_-180_180_1981_2010_1950_2099.json
new file mode 100644
index 00000000..b2282b63
--- /dev/null
+++ b/configs/generate_valid_configs/maca/maca_ERA5_CanESM5_r1i1p1f1_ssp245_tasmax_-90_90_-180_180_1981_2010_1950_2099.json
@@ -0,0 +1,18 @@
+{
+  "method": "maca",
+  "obs": "ERA5",
+  "model": "CanESM5",
+  "member": "r1i1p1f1",
+  "grid_label": "gn",
+  "table_id": "day",
+  "scenario": "ssp245",
+  "variable": "tasmax",
+  "train_dates": ["1981", "2010"],
+  "predict_dates": ["1950", "2099"],
+  "latmin": "-90",
+  "latmax": "90",
+  "lonmin": "-180",
+  "lonmax": "180",
+  "year_rolling_window": 31,
+  "day_rolling_window": 21
+}
diff --git a/configs/generate_valid_configs/maca/maca_ERA5_CanESM5_r1i1p1f1_ssp245_tasmin_-90_90_-180_180_1981_2010_1950_2099.json b/configs/generate_valid_configs/maca/maca_ERA5_CanESM5_r1i1p1f1_ssp245_tasmin_-90_90_-180_180_1981_2010_1950_2099.json
new file mode 100644
index 00000000..d5b32fde
--- /dev/null
+++ b/configs/generate_valid_configs/maca/maca_ERA5_CanESM5_r1i1p1f1_ssp245_tasmin_-90_90_-180_180_1981_2010_1950_2099.json
@@ -0,0 +1,18 @@
+{
+  "method": "maca",
+  "obs": "ERA5",
+  "model": "CanESM5",
+  "member": "r1i1p1f1",
+  "grid_label": "gn",
+  "table_id": "day",
+  "scenario": "ssp245",
+  "variable": "tasmin",
+  "train_dates": ["1981", "2010"],
+  "predict_dates": ["1950", "2099"],
+  "latmin": "-90",
+  "latmax": "90",
+  "lonmin": "-180",
+  "lonmax": "180",
+  "year_rolling_window": 31,
+  "day_rolling_window": 21
+}
diff --git a/configs/generate_valid_configs/maca/maca_ERA5_CanESM5_r1i1p1f1_ssp370_pr_-90_90_-180_180_1981_2010_1950_2099.json b/configs/generate_valid_configs/maca/maca_ERA5_CanESM5_r1i1p1f1_ssp370_pr_-90_90_-180_180_1981_2010_1950_2099.json
new file mode 100644
index 00000000..a2c8b23a
--- /dev/null
+++ b/configs/generate_valid_configs/maca/maca_ERA5_CanESM5_r1i1p1f1_ssp370_pr_-90_90_-180_180_1981_2010_1950_2099.json
@@ -0,0 +1,18 @@
+{
+  "method": "maca",
+  "obs": "ERA5",
+  "model": "CanESM5",
+  "member": "r1i1p1f1",
+  "grid_label": "gn",
+  "table_id": "day",
+  "scenario": "ssp370",
+  "variable": "pr",
+  "train_dates": ["1981", "2010"],
+  "predict_dates": ["1950", "2099"],
+  "latmin": "-90",
+  "latmax": "90",
+  "lonmin": "-180",
+  "lonmax": "180",
+  "year_rolling_window": 31,
+  "day_rolling_window": 21
+}
diff --git a/configs/generate_valid_configs/maca/maca_ERA5_CanESM5_r1i1p1f1_ssp370_tasmax_-90_90_-180_180_1981_2010_1950_2099.json b/configs/generate_valid_configs/maca/maca_ERA5_CanESM5_r1i1p1f1_ssp370_tasmax_-90_90_-180_180_1981_2010_1950_2099.json
new file mode 100644
index 00000000..404a8173
--- /dev/null
+++ b/configs/generate_valid_configs/maca/maca_ERA5_CanESM5_r1i1p1f1_ssp370_tasmax_-90_90_-180_180_1981_2010_1950_2099.json
@@ -0,0 +1,18 @@
+{
+  "method": "maca",
+  "obs": "ERA5",
+  "model": "CanESM5",
+  "member": "r1i1p1f1",
+  "grid_label": "gn",
+  "table_id": "day",
+  "scenario": "ssp370",
+  "variable": "tasmax",
+  "train_dates": ["1981", "2010"],
+  "predict_dates": ["1950", "2099"],
+  "latmin": "-90",
+  "latmax": "90",
+  "lonmin": "-180",
+  "lonmax": "180",
+  "year_rolling_window": 31,
+  "day_rolling_window": 21
+}
diff --git a/configs/generate_valid_configs/maca/maca_ERA5_CanESM5_r1i1p1f1_ssp370_tasmin_-90_90_-180_180_1981_2010_1950_2099.json b/configs/generate_valid_configs/maca/maca_ERA5_CanESM5_r1i1p1f1_ssp370_tasmin_-90_90_-180_180_1981_2010_1950_2099.json
new file mode 100644
index 00000000..80abc84f
--- /dev/null
+++ b/configs/generate_valid_configs/maca/maca_ERA5_CanESM5_r1i1p1f1_ssp370_tasmin_-90_90_-180_180_1981_2010_1950_2099.json
@@ -0,0 +1,18 @@
+{
+  "method": "maca",
+  "obs": "ERA5",
+  "model": "CanESM5",
+  "member": "r1i1p1f1",
+  "grid_label": "gn",
+  "table_id": "day",
+  "scenario": "ssp370",
+  "variable": "tasmin",
+  "train_dates": ["1981", "2010"],
+  "predict_dates": ["1950", "2099"],
+  "latmin": "-90",
+  "latmax": "90",
+  "lonmin": "-180",
+  "lonmax": "180",
+  "year_rolling_window": 31,
+  "day_rolling_window": 21
+}
diff --git a/configs/generate_valid_configs/maca/maca_ERA5_CanESM5_r1i1p1f1_ssp585_pr_-90_90_-180_180_1981_2010_1950_2099.json b/configs/generate_valid_configs/maca/maca_ERA5_CanESM5_r1i1p1f1_ssp585_pr_-90_90_-180_180_1981_2010_1950_2099.json
new file mode 100644
index 00000000..b6f81ddd
--- /dev/null
+++ b/configs/generate_valid_configs/maca/maca_ERA5_CanESM5_r1i1p1f1_ssp585_pr_-90_90_-180_180_1981_2010_1950_2099.json
@@ -0,0 +1,18 @@
+{
+  "method": "maca",
+  "obs": "ERA5",
+  "model": "CanESM5",
+  "member": "r1i1p1f1",
+  "grid_label": "gn",
+  "table_id": "day",
+  "scenario": "ssp585",
+  "variable": "pr",
+  "train_dates": ["1981", "2010"],
+  "predict_dates": ["1950", "2099"],
+  "latmin": "-90",
+  "latmax": "90",
+  "lonmin": "-180",
+  "lonmax": "180",
+  "year_rolling_window": 31,
+  "day_rolling_window": 21
+}
diff --git a/configs/generate_valid_configs/maca/maca_ERA5_CanESM5_r1i1p1f1_ssp585_tasmax_-90_90_-180_180_1981_2010_1950_2099.json b/configs/generate_valid_configs/maca/maca_ERA5_CanESM5_r1i1p1f1_ssp585_tasmax_-90_90_-180_180_1981_2010_1950_2099.json
new file mode 100644
index 00000000..96b13f1a
--- /dev/null
+++ b/configs/generate_valid_configs/maca/maca_ERA5_CanESM5_r1i1p1f1_ssp585_tasmax_-90_90_-180_180_1981_2010_1950_2099.json
@@ -0,0 +1,18 @@
+{
+  "method": "maca",
+  "obs": "ERA5",
+  "model": "CanESM5",
+  "member": "r1i1p1f1",
+  "grid_label": "gn",
+  "table_id": "day",
+  "scenario": "ssp585",
+  "variable": "tasmax",
+  "train_dates": ["1981", "2010"],
+  "predict_dates": ["1950", "2099"],
+  "latmin": "-90",
+  "latmax": "90",
+  "lonmin": "-180",
+  "lonmax": "180",
+  "year_rolling_window": 31,
+  "day_rolling_window": 21
+}
diff --git a/configs/generate_valid_configs/maca/maca_ERA5_CanESM5_r1i1p1f1_ssp585_tasmin_-90_90_-180_180_1981_2010_1950_2099.json b/configs/generate_valid_configs/maca/maca_ERA5_CanESM5_r1i1p1f1_ssp585_tasmin_-90_90_-180_180_1981_2010_1950_2099.json
new file mode 100644
index 00000000..ee0b41e7
--- /dev/null
+++ b/configs/generate_valid_configs/maca/maca_ERA5_CanESM5_r1i1p1f1_ssp585_tasmin_-90_90_-180_180_1981_2010_1950_2099.json
@@ -0,0 +1,18 @@
+{
+  "method": "maca",
+  "obs": "ERA5",
+  "model": "CanESM5",
+  "member": "r1i1p1f1",
+  "grid_label": "gn",
+  "table_id": "day",
+  "scenario": "ssp585",
+  "variable": "tasmin",
+  "train_dates": ["1981", "2010"],
+  "predict_dates": ["1950", "2099"],
+  "latmin": "-90",
+  "latmax": "90",
+  "lonmin": "-180",
+  "lonmax": "180",
+  "year_rolling_window": 31,
+  "day_rolling_window": 21
+}
diff --git a/configs/generate_valid_configs/maca/maca_ERA5_MIROC6_r1i1p1f1_ssp245_pr_-90_90_-180_180_1981_2010_1950_2099.json b/configs/generate_valid_configs/maca/maca_ERA5_MIROC6_r1i1p1f1_ssp245_pr_-90_90_-180_180_1981_2010_1950_2099.json
new file mode 100644
index 00000000..e0ad515b
--- /dev/null
+++ b/configs/generate_valid_configs/maca/maca_ERA5_MIROC6_r1i1p1f1_ssp245_pr_-90_90_-180_180_1981_2010_1950_2099.json
@@ -0,0 +1,18 @@
+{
+  "method": "maca",
+  "obs": "ERA5",
+  "model": "MIROC6",
+  "member": "r1i1p1f1",
+  "grid_label": "gn",
+  "table_id": "day",
+  "scenario": "ssp245",
+  "variable": "pr",
+  "train_dates": ["1981", "2010"],
+  "predict_dates": ["1950", "2099"],
+  "latmin": "-90",
+  "latmax": "90",
+  "lonmin": "-180",
+  "lonmax": "180",
+  "year_rolling_window": 31,
+  "day_rolling_window": 21
+}
diff --git a/configs/generate_valid_configs/maca/maca_ERA5_MIROC6_r1i1p1f1_ssp245_tasmax_-90_90_-180_180_1981_2010_1950_2099.json b/configs/generate_valid_configs/maca/maca_ERA5_MIROC6_r1i1p1f1_ssp245_tasmax_-90_90_-180_180_1981_2010_1950_2099.json
new file mode 100644
index 00000000..45c7de03
--- /dev/null
+++ b/configs/generate_valid_configs/maca/maca_ERA5_MIROC6_r1i1p1f1_ssp245_tasmax_-90_90_-180_180_1981_2010_1950_2099.json
@@ -0,0 +1,18 @@
+{
+  "method": "maca",
+  "obs": "ERA5",
+  "model": "MIROC6",
+  "member": "r1i1p1f1",
+  "grid_label": "gn",
+  "table_id": "day",
+  "scenario": "ssp245",
+  "variable": "tasmax",
+  "train_dates": ["1981", "2010"],
+  "predict_dates": ["1950", "2099"],
+  "latmin": "-90",
+  "latmax": "90",
+  "lonmin": "-180",
+  "lonmax": "180",
+  "year_rolling_window": 31,
+  "day_rolling_window": 21
+}
diff --git a/configs/generate_valid_configs/maca/maca_ERA5_MIROC6_r1i1p1f1_ssp245_tasmin_-90_90_-180_180_1981_2010_1950_2099.json b/configs/generate_valid_configs/maca/maca_ERA5_MIROC6_r1i1p1f1_ssp245_tasmin_-90_90_-180_180_1981_2010_1950_2099.json
new file mode 100644
index 00000000..11ec3865
--- /dev/null
+++ b/configs/generate_valid_configs/maca/maca_ERA5_MIROC6_r1i1p1f1_ssp245_tasmin_-90_90_-180_180_1981_2010_1950_2099.json
@@ -0,0 +1,18 @@
+{
+  "method": "maca",
+  "obs": "ERA5",
+  "model": "MIROC6",
+  "member": "r1i1p1f1",
+  "grid_label": "gn",
+  "table_id": "day",
+  "scenario": "ssp245",
+  "variable": "tasmin",
+  "train_dates": ["1981", "2010"],
+  "predict_dates": ["1950", "2099"],
+  "latmin": "-90",
+  "latmax": "90",
+  "lonmin": "-180",
+  "lonmax": "180",
+  "year_rolling_window": 31,
+  "day_rolling_window": 21
+}
diff --git a/configs/generate_valid_configs/maca/maca_ERA5_MIROC6_r1i1p1f1_ssp370_pr_-90_90_-180_180_1981_2010_1950_2099.json b/configs/generate_valid_configs/maca/maca_ERA5_MIROC6_r1i1p1f1_ssp370_pr_-90_90_-180_180_1981_2010_1950_2099.json
new file mode 100644
index 00000000..caa1800f
--- /dev/null
+++ b/configs/generate_valid_configs/maca/maca_ERA5_MIROC6_r1i1p1f1_ssp370_pr_-90_90_-180_180_1981_2010_1950_2099.json
@@ -0,0 +1,18 @@
+{
+  "method": "maca",
+  "obs": "ERA5",
+  "model": "MIROC6",
+  "member": "r1i1p1f1",
+  "grid_label": "gn",
+  "table_id": "day",
+  "scenario": "ssp370",
+  "variable": "pr",
+  "train_dates": ["1981", "2010"],
+  "predict_dates": ["1950", "2099"],
+  "latmin": "-90",
+  "latmax": "90",
+  "lonmin": "-180",
+  "lonmax": "180",
+  "year_rolling_window": 31,
+  "day_rolling_window": 21
+}
diff --git a/configs/generate_valid_configs/maca/maca_ERA5_MIROC6_r1i1p1f1_ssp370_tasmax_-90_90_-180_180_1981_2010_1950_2099.json b/configs/generate_valid_configs/maca/maca_ERA5_MIROC6_r1i1p1f1_ssp370_tasmax_-90_90_-180_180_1981_2010_1950_2099.json
new file mode 100644
index 00000000..c230c761
--- /dev/null
+++ b/configs/generate_valid_configs/maca/maca_ERA5_MIROC6_r1i1p1f1_ssp370_tasmax_-90_90_-180_180_1981_2010_1950_2099.json
@@ -0,0 +1,18 @@
+{
+  "method": "maca",
+  "obs": "ERA5",
+  "model": "MIROC6",
+  "member": "r1i1p1f1",
+  "grid_label": "gn",
+  "table_id": "day",
+  "scenario": "ssp370",
+  "variable": "tasmax",
+  "train_dates": ["1981", "2010"],
+  "predict_dates": ["1950", "2099"],
+  "latmin": "-90",
+  "latmax": "90",
+  "lonmin": "-180",
+  "lonmax": "180",
+  "year_rolling_window": 31,
+  "day_rolling_window": 21
+}
diff --git a/configs/generate_valid_configs/maca/maca_ERA5_MIROC6_r1i1p1f1_ssp370_tasmin_-90_90_-180_180_1981_2010_1950_2099.json b/configs/generate_valid_configs/maca/maca_ERA5_MIROC6_r1i1p1f1_ssp370_tasmin_-90_90_-180_180_1981_2010_1950_2099.json
new file mode 100644
index 00000000..a182cdb8
--- /dev/null
+++ b/configs/generate_valid_configs/maca/maca_ERA5_MIROC6_r1i1p1f1_ssp370_tasmin_-90_90_-180_180_1981_2010_1950_2099.json
@@ -0,0 +1,18 @@
+{
+  "method": "maca",
+  "obs": "ERA5",
+  "model": "MIROC6",
+  "member": "r1i1p1f1",
+  "grid_label": "gn",
+  "table_id": "day",
+  "scenario": "ssp370",
+  "variable": "tasmin",
+  "train_dates": ["1981", "2010"],
+  "predict_dates": ["1950", "2099"],
+  "latmin": "-90",
+  "latmax": "90",
+  "lonmin": "-180",
+  "lonmax": "180",
+  "year_rolling_window": 31,
+  "day_rolling_window": 21
+}
diff --git a/configs/generate_valid_configs/maca/maca_ERA5_MIROC6_r1i1p1f1_ssp585_pr_-90_90_-180_180_1981_2010_1950_2099.json b/configs/generate_valid_configs/maca/maca_ERA5_MIROC6_r1i1p1f1_ssp585_pr_-90_90_-180_180_1981_2010_1950_2099.json
new file mode 100644
index 00000000..bdb4c6df
--- /dev/null
+++ b/configs/generate_valid_configs/maca/maca_ERA5_MIROC6_r1i1p1f1_ssp585_pr_-90_90_-180_180_1981_2010_1950_2099.json
@@ -0,0 +1,18 @@
+{
+  "method": "maca",
+  "obs": "ERA5",
+  "model": "MIROC6",
+  "member": "r1i1p1f1",
+  "grid_label": "gn",
+  "table_id": "day",
+  "scenario": "ssp585",
+  "variable": "pr",
+  "train_dates": ["1981", "2010"],
+  "predict_dates": ["1950", "2099"],
+  "latmin": "-90",
+  "latmax": "90",
+  "lonmin": "-180",
+  "lonmax": "180",
+  "year_rolling_window": 31,
+  "day_rolling_window": 21
+}
diff --git a/configs/generate_valid_configs/maca/maca_ERA5_MIROC6_r1i1p1f1_ssp585_tasmax_-90_90_-180_180_1981_2010_1950_2099.json b/configs/generate_valid_configs/maca/maca_ERA5_MIROC6_r1i1p1f1_ssp585_tasmax_-90_90_-180_180_1981_2010_1950_2099.json
new file mode 100644
index 00000000..ab624fa6
--- /dev/null
+++ b/configs/generate_valid_configs/maca/maca_ERA5_MIROC6_r1i1p1f1_ssp585_tasmax_-90_90_-180_180_1981_2010_1950_2099.json
@@ -0,0 +1,18 @@
+{
+  "method": "maca",
+  "obs": "ERA5",
+  "model": "MIROC6",
+  "member": "r1i1p1f1",
+  "grid_label": "gn",
+  "table_id": "day",
+  "scenario": "ssp585",
+  "variable": "tasmax",
+  "train_dates": ["1981", "2010"],
+  "predict_dates": ["1950", "2099"],
+  "latmin": "-90",
+  "latmax": "90",
+  "lonmin": "-180",
+  "lonmax": "180",
+  "year_rolling_window": 31,
+  "day_rolling_window": 21
+}
diff --git a/configs/generate_valid_configs/maca/maca_ERA5_MIROC6_r1i1p1f1_ssp585_tasmin_-90_90_-180_180_1981_2010_1950_2099.json b/configs/generate_valid_configs/maca/maca_ERA5_MIROC6_r1i1p1f1_ssp585_tasmin_-90_90_-180_180_1981_2010_1950_2099.json
new file mode 100644
index 00000000..6919e75c
--- /dev/null
+++ b/configs/generate_valid_configs/maca/maca_ERA5_MIROC6_r1i1p1f1_ssp585_tasmin_-90_90_-180_180_1981_2010_1950_2099.json
@@ -0,0 +1,18 @@
+{
+  "method": "maca",
+  "obs": "ERA5",
+  "model": "MIROC6",
+  "member": "r1i1p1f1",
+  "grid_label": "gn",
+  "table_id": "day",
+  "scenario": "ssp585",
+  "variable": "tasmin",
+  "train_dates": ["1981", "2010"],
+  "predict_dates": ["1950", "2099"],
+  "latmin": "-90",
+  "latmax": "90",
+  "lonmin": "-180",
+  "lonmax": "180",
+  "year_rolling_window": 31,
+  "day_rolling_window": 21
+}
diff --git a/configs/generate_valid_configs/maca/maca_ERA5_MPI-ESM1-2-HR_r1i1p1f1_ssp245_pr_-90_90_-180_180_1981_2010_1950_2099.json b/configs/generate_valid_configs/maca/maca_ERA5_MPI-ESM1-2-HR_r1i1p1f1_ssp245_pr_-90_90_-180_180_1981_2010_1950_2099.json
new file mode 100644
index 00000000..7ccf2cf9
--- /dev/null
+++ b/configs/generate_valid_configs/maca/maca_ERA5_MPI-ESM1-2-HR_r1i1p1f1_ssp245_pr_-90_90_-180_180_1981_2010_1950_2099.json
@@ -0,0 +1,18 @@
+{
+  "method": "maca",
+  "obs": "ERA5",
+  "model": "MPI-ESM1-2-HR",
+  "member": "r1i1p1f1",
+  "grid_label": "gn",
+  "table_id": "day",
+  "scenario": "ssp245",
+  "variable": "pr",
+  "train_dates": ["1981", "2010"],
+  "predict_dates": ["1950", "2099"],
+  "latmin": "-90",
+  "latmax": "90",
+  "lonmin": "-180",
+  "lonmax": "180",
+  "year_rolling_window": 31,
+  "day_rolling_window": 21
+}
diff --git a/configs/generate_valid_configs/maca/maca_ERA5_MPI-ESM1-2-HR_r1i1p1f1_ssp245_tasmax_-90_90_-180_180_1981_2010_1950_2099.json b/configs/generate_valid_configs/maca/maca_ERA5_MPI-ESM1-2-HR_r1i1p1f1_ssp245_tasmax_-90_90_-180_180_1981_2010_1950_2099.json
new file mode 100644
index 00000000..9f08bb2b
--- /dev/null
+++ b/configs/generate_valid_configs/maca/maca_ERA5_MPI-ESM1-2-HR_r1i1p1f1_ssp245_tasmax_-90_90_-180_180_1981_2010_1950_2099.json
@@ -0,0 +1,18 @@
+{
+  "method": "maca",
+  "obs": "ERA5",
+  "model": "MPI-ESM1-2-HR",
+  "member": "r1i1p1f1",
+  "grid_label": "gn",
+  "table_id": "day",
+  "scenario": "ssp245",
+  "variable": "tasmax",
+  "train_dates": ["1981", "2010"],
+  "predict_dates": ["1950", "2099"],
+  "latmin": "-90",
+  "latmax": "90",
+  "lonmin": "-180",
+  "lonmax": "180",
+  "year_rolling_window": 31,
+  "day_rolling_window": 21
+}
diff --git a/configs/generate_valid_configs/maca/maca_ERA5_MPI-ESM1-2-HR_r1i1p1f1_ssp245_tasmin_-90_90_-180_180_1981_2010_1950_2099.json b/configs/generate_valid_configs/maca/maca_ERA5_MPI-ESM1-2-HR_r1i1p1f1_ssp245_tasmin_-90_90_-180_180_1981_2010_1950_2099.json
new file mode 100644
index 00000000..07f6c193
--- /dev/null
+++ b/configs/generate_valid_configs/maca/maca_ERA5_MPI-ESM1-2-HR_r1i1p1f1_ssp245_tasmin_-90_90_-180_180_1981_2010_1950_2099.json
@@ -0,0 +1,18 @@
+{
+  "method": "maca",
+  "obs": "ERA5",
+  "model": "MPI-ESM1-2-HR",
+  "member": "r1i1p1f1",
+  "grid_label": "gn",
+  "table_id": "day",
+  "scenario": "ssp245",
+  "variable": "tasmin",
+  "train_dates": ["1981", "2010"],
+  "predict_dates": ["1950", "2099"],
+  "latmin": "-90",
+  "latmax": "90",
+  "lonmin": "-180",
+  "lonmax": "180",
+  "year_rolling_window": 31,
+  "day_rolling_window": 21
+}
diff --git a/configs/generate_valid_configs/maca/maca_ERA5_MPI-ESM1-2-HR_r1i1p1f1_ssp370_pr_-90_90_-180_180_1981_2010_1950_2099.json b/configs/generate_valid_configs/maca/maca_ERA5_MPI-ESM1-2-HR_r1i1p1f1_ssp370_pr_-90_90_-180_180_1981_2010_1950_2099.json
new file mode 100644
index 00000000..a2018d5c
--- /dev/null
+++ b/configs/generate_valid_configs/maca/maca_ERA5_MPI-ESM1-2-HR_r1i1p1f1_ssp370_pr_-90_90_-180_180_1981_2010_1950_2099.json
@@ -0,0 +1,18 @@
+{
+  "method": "maca",
+  "obs": "ERA5",
+  "model": "MPI-ESM1-2-HR",
+  "member": "r1i1p1f1",
+  "grid_label": "gn",
+  "table_id": "day",
+  "scenario": "ssp370",
+  "variable": "pr",
+  "train_dates": ["1981", "2010"],
+  "predict_dates": ["1950", "2099"],
+  "latmin": "-90",
+  "latmax": "90",
+  "lonmin": "-180",
+  "lonmax": "180",
+  "year_rolling_window": 31,
+  "day_rolling_window": 21
+}
diff --git a/configs/generate_valid_configs/maca/maca_ERA5_MPI-ESM1-2-HR_r1i1p1f1_ssp370_tasmax_-90_90_-180_180_1981_2010_1950_2099.json b/configs/generate_valid_configs/maca/maca_ERA5_MPI-ESM1-2-HR_r1i1p1f1_ssp370_tasmax_-90_90_-180_180_1981_2010_1950_2099.json
new file mode 100644
index 00000000..caf23e1a
--- /dev/null
+++ b/configs/generate_valid_configs/maca/maca_ERA5_MPI-ESM1-2-HR_r1i1p1f1_ssp370_tasmax_-90_90_-180_180_1981_2010_1950_2099.json
@@ -0,0 +1,18 @@
+{
+  "method": "maca",
+  "obs": "ERA5",
+  "model": "MPI-ESM1-2-HR",
+  "member": "r1i1p1f1",
+  "grid_label": "gn",
+  "table_id": "day",
+  "scenario": "ssp370",
+  "variable": "tasmax",
+  "train_dates": ["1981", "2010"],
+  "predict_dates": ["1950", "2099"],
+  "latmin": "-90",
+  "latmax": "90",
+  "lonmin": "-180",
+  "lonmax": "180",
+  "year_rolling_window": 31,
+  "day_rolling_window": 21
+}
diff --git a/configs/generate_valid_configs/maca/maca_ERA5_MPI-ESM1-2-HR_r1i1p1f1_ssp370_tasmin_-90_90_-180_180_1981_2010_1950_2099.json b/configs/generate_valid_configs/maca/maca_ERA5_MPI-ESM1-2-HR_r1i1p1f1_ssp370_tasmin_-90_90_-180_180_1981_2010_1950_2099.json
new file mode 100644
index 00000000..fd3d36d9
--- /dev/null
+++ b/configs/generate_valid_configs/maca/maca_ERA5_MPI-ESM1-2-HR_r1i1p1f1_ssp370_tasmin_-90_90_-180_180_1981_2010_1950_2099.json
@@ -0,0 +1,18 @@
+{
+  "method": "maca",
+  "obs": "ERA5",
+  "model": "MPI-ESM1-2-HR",
+  "member": "r1i1p1f1",
+  "grid_label": "gn",
+  "table_id": "day",
+  "scenario": "ssp370",
+  "variable": "tasmin",
+  "train_dates": ["1981", "2010"],
+  "predict_dates": ["1950", "2099"],
+  "latmin": "-90",
+  "latmax": "90",
+  "lonmin": "-180",
+  "lonmax": "180",
+  "year_rolling_window": 31,
+  "day_rolling_window": 21
+}
diff --git a/configs/generate_valid_configs/maca/maca_ERA5_MPI-ESM1-2-HR_r1i1p1f1_ssp585_pr_-90_90_-180_180_1981_2010_1950_2099.json b/configs/generate_valid_configs/maca/maca_ERA5_MPI-ESM1-2-HR_r1i1p1f1_ssp585_pr_-90_90_-180_180_1981_2010_1950_2099.json
new file mode 100644
index 00000000..eb973df1
--- /dev/null
+++ b/configs/generate_valid_configs/maca/maca_ERA5_MPI-ESM1-2-HR_r1i1p1f1_ssp585_pr_-90_90_-180_180_1981_2010_1950_2099.json
@@ -0,0 +1,18 @@
+{
+  "method": "maca",
+  "obs": "ERA5",
+  "model": "MPI-ESM1-2-HR",
+  "member": "r1i1p1f1",
+  "grid_label": "gn",
+  "table_id": "day",
+  "scenario": "ssp585",
+  "variable": "pr",
+  "train_dates": ["1981", "2010"],
+  "predict_dates": ["1950", "2099"],
+  "latmin": "-90",
+  "latmax": "90",
+  "lonmin": "-180",
+  "lonmax": "180",
+  "year_rolling_window": 31,
+  "day_rolling_window": 21
+}
diff --git a/configs/generate_valid_configs/maca/maca_ERA5_MPI-ESM1-2-HR_r1i1p1f1_ssp585_tasmax_-90_90_-180_180_1981_2010_1950_2099.json b/configs/generate_valid_configs/maca/maca_ERA5_MPI-ESM1-2-HR_r1i1p1f1_ssp585_tasmax_-90_90_-180_180_1981_2010_1950_2099.json
new file mode 100644
index 00000000..b08f42b7
--- /dev/null
+++ b/configs/generate_valid_configs/maca/maca_ERA5_MPI-ESM1-2-HR_r1i1p1f1_ssp585_tasmax_-90_90_-180_180_1981_2010_1950_2099.json
@@ -0,0 +1,18 @@
+{
+  "method": "maca",
+  "obs": "ERA5",
+  "model": "MPI-ESM1-2-HR",
+  "member": "r1i1p1f1",
+  "grid_label": "gn",
+  "table_id": "day",
+  "scenario": "ssp585",
+  "variable": "tasmax",
+  "train_dates": ["1981", "2010"],
+  "predict_dates": ["1950", "2099"],
+  "latmin": "-90",
+  "latmax": "90",
+  "lonmin": "-180",
+  "lonmax": "180",
+  "year_rolling_window": 31,
+  "day_rolling_window": 21
+}
diff --git a/configs/generate_valid_configs/maca/maca_ERA5_MPI-ESM1-2-HR_r1i1p1f1_ssp585_tasmin_-90_90_-180_180_1981_2010_1950_2099.json b/configs/generate_valid_configs/maca/maca_ERA5_MPI-ESM1-2-HR_r1i1p1f1_ssp585_tasmin_-90_90_-180_180_1981_2010_1950_2099.json
new file mode 100644
index 00000000..b464596b
--- /dev/null
+++ b/configs/generate_valid_configs/maca/maca_ERA5_MPI-ESM1-2-HR_r1i1p1f1_ssp585_tasmin_-90_90_-180_180_1981_2010_1950_2099.json
@@ -0,0 +1,18 @@
+{
+  "method": "maca",
+  "obs": "ERA5",
+  "model": "MPI-ESM1-2-HR",
+  "member": "r1i1p1f1",
+  "grid_label": "gn",
+  "table_id": "day",
+  "scenario": "ssp585",
+  "variable": "tasmin",
+  "train_dates": ["1981", "2010"],
+  "predict_dates": ["1950", "2099"],
+  "latmin": "-90",
+  "latmax": "90",
+  "lonmin": "-180",
+  "lonmax": "180",
+  "year_rolling_window": 31,
+  "day_rolling_window": 21
+}
diff --git a/configs/generate_valid_configs/maca/maca_ERA5_MRI-ESM2-0_r1i1p1f1_ssp245_pr_-90_90_-180_180_1981_2010_1950_2099.json b/configs/generate_valid_configs/maca/maca_ERA5_MRI-ESM2-0_r1i1p1f1_ssp245_pr_-90_90_-180_180_1981_2010_1950_2099.json
new file mode 100644
index 00000000..a46a7f1b
--- /dev/null
+++ b/configs/generate_valid_configs/maca/maca_ERA5_MRI-ESM2-0_r1i1p1f1_ssp245_pr_-90_90_-180_180_1981_2010_1950_2099.json
@@ -0,0 +1,18 @@
+{
+  "method": "maca",
+  "obs": "ERA5",
+  "model": "MRI-ESM2-0",
+  "member": "r1i1p1f1",
+  "grid_label": "gn",
+  "table_id": "day",
+  "scenario": "ssp245",
+  "variable": "pr",
+  "train_dates": ["1981", "2010"],
+  "predict_dates": ["1950", "2099"],
+  "latmin": "-90",
+  "latmax": "90",
+  "lonmin": "-180",
+  "lonmax": "180",
+  "year_rolling_window": 31,
+  "day_rolling_window": 21
+}
diff --git a/configs/generate_valid_configs/maca/maca_ERA5_MRI-ESM2-0_r1i1p1f1_ssp245_tasmax_-90_90_-180_180_1981_2010_1950_2099.json b/configs/generate_valid_configs/maca/maca_ERA5_MRI-ESM2-0_r1i1p1f1_ssp245_tasmax_-90_90_-180_180_1981_2010_1950_2099.json
new file mode 100644
index 00000000..fb63f089
--- /dev/null
+++ b/configs/generate_valid_configs/maca/maca_ERA5_MRI-ESM2-0_r1i1p1f1_ssp245_tasmax_-90_90_-180_180_1981_2010_1950_2099.json
@@ -0,0 +1,18 @@
+{
+  "method": "maca",
+  "obs": "ERA5",
+  "model": "MRI-ESM2-0",
+  "member": "r1i1p1f1",
+  "grid_label": "gn",
+  "table_id": "day",
+  "scenario": "ssp245",
+  "variable": "tasmax",
+  "train_dates": ["1981", "2010"],
+  "predict_dates": ["1950", "2099"],
+  "latmin": "-90",
+  "latmax": "90",
+  "lonmin": "-180",
+  "lonmax": "180",
+  "year_rolling_window": 31,
+  "day_rolling_window": 21
+}
diff --git a/configs/generate_valid_configs/maca/maca_ERA5_MRI-ESM2-0_r1i1p1f1_ssp245_tasmin_-90_90_-180_180_1981_2010_1950_2099.json b/configs/generate_valid_configs/maca/maca_ERA5_MRI-ESM2-0_r1i1p1f1_ssp245_tasmin_-90_90_-180_180_1981_2010_1950_2099.json
new file mode 100644
index 00000000..68918238
--- /dev/null
+++ b/configs/generate_valid_configs/maca/maca_ERA5_MRI-ESM2-0_r1i1p1f1_ssp245_tasmin_-90_90_-180_180_1981_2010_1950_2099.json
@@ -0,0 +1,18 @@
+{
+  "method": "maca",
+  "obs": "ERA5",
+  "model": "MRI-ESM2-0",
+  "member": "r1i1p1f1",
+  "grid_label": "gn",
+  "table_id": "day",
+  "scenario": "ssp245",
+  "variable": "tasmin",
+  "train_dates": ["1981", "2010"],
+  "predict_dates": ["1950", "2099"],
+  "latmin": "-90",
+  "latmax": "90",
+  "lonmin": "-180",
+  "lonmax": "180",
+  "year_rolling_window": 31,
+  "day_rolling_window": 21
+}
diff --git a/configs/generate_valid_configs/maca/maca_ERA5_MRI-ESM2-0_r1i1p1f1_ssp370_pr_-90_90_-180_180_1981_2010_1950_2099.json b/configs/generate_valid_configs/maca/maca_ERA5_MRI-ESM2-0_r1i1p1f1_ssp370_pr_-90_90_-180_180_1981_2010_1950_2099.json
new file mode 100644
index 00000000..fb4344aa
--- /dev/null
+++ b/configs/generate_valid_configs/maca/maca_ERA5_MRI-ESM2-0_r1i1p1f1_ssp370_pr_-90_90_-180_180_1981_2010_1950_2099.json
@@ -0,0 +1,18 @@
+{
+  "method": "maca",
+  "obs": "ERA5",
+  "model": "MRI-ESM2-0",
+  "member": "r1i1p1f1",
+  "grid_label": "gn",
+  "table_id": "day",
+  "scenario": "ssp370",
+  "variable": "pr",
+  "train_dates": ["1981", "2010"],
+  "predict_dates": ["1950", "2099"],
+  "latmin": "-90",
+  "latmax": "90",
+  "lonmin": "-180",
+  "lonmax": "180",
+  "year_rolling_window": 31,
+  "day_rolling_window": 21
+}
diff --git a/configs/generate_valid_configs/maca/maca_ERA5_MRI-ESM2-0_r1i1p1f1_ssp370_tasmax_-90_90_-180_180_1981_2010_1950_2099.json b/configs/generate_valid_configs/maca/maca_ERA5_MRI-ESM2-0_r1i1p1f1_ssp370_tasmax_-90_90_-180_180_1981_2010_1950_2099.json
new file mode 100644
index 00000000..18565b82
--- /dev/null
+++ b/configs/generate_valid_configs/maca/maca_ERA5_MRI-ESM2-0_r1i1p1f1_ssp370_tasmax_-90_90_-180_180_1981_2010_1950_2099.json
@@ -0,0 +1,18 @@
+{
+  "method": "maca",
+  "obs": "ERA5",
+  "model": "MRI-ESM2-0",
+  "member": "r1i1p1f1",
+  "grid_label": "gn",
+  "table_id": "day",
+  "scenario": "ssp370",
+  "variable": "tasmax",
+  "train_dates": ["1981", "2010"],
+  "predict_dates": ["1950", "2099"],
+  "latmin": "-90",
+  "latmax": "90",
+  "lonmin": "-180",
+  "lonmax": "180",
+  "year_rolling_window": 31,
+  "day_rolling_window": 21
+}
diff --git a/configs/generate_valid_configs/maca/maca_ERA5_MRI-ESM2-0_r1i1p1f1_ssp370_tasmin_-90_90_-180_180_1981_2010_1950_2099.json b/configs/generate_valid_configs/maca/maca_ERA5_MRI-ESM2-0_r1i1p1f1_ssp370_tasmin_-90_90_-180_180_1981_2010_1950_2099.json
new file mode 100644
index 00000000..57ebb0e0
--- /dev/null
+++ b/configs/generate_valid_configs/maca/maca_ERA5_MRI-ESM2-0_r1i1p1f1_ssp370_tasmin_-90_90_-180_180_1981_2010_1950_2099.json
@@ -0,0 +1,18 @@
+{
+  "method": "maca",
+  "obs": "ERA5",
+  "model": "MRI-ESM2-0",
+  "member": "r1i1p1f1",
+  "grid_label": "gn",
+  "table_id": "day",
+  "scenario": "ssp370",
+  "variable": "tasmin",
+  "train_dates": ["1981", "2010"],
+  "predict_dates": ["1950", "2099"],
+  "latmin": "-90",
+  "latmax": "90",
+  "lonmin": "-180",
+  "lonmax": "180",
+  "year_rolling_window": 31,
+  "day_rolling_window": 21
+}
diff --git a/configs/generate_valid_configs/maca/maca_ERA5_MRI-ESM2-0_r1i1p1f1_ssp585_pr_-90_90_-180_180_1981_2010_1950_2099.json b/configs/generate_valid_configs/maca/maca_ERA5_MRI-ESM2-0_r1i1p1f1_ssp585_pr_-90_90_-180_180_1981_2010_1950_2099.json
new file mode 100644
index 00000000..ff3c2771
--- /dev/null
+++ b/configs/generate_valid_configs/maca/maca_ERA5_MRI-ESM2-0_r1i1p1f1_ssp585_pr_-90_90_-180_180_1981_2010_1950_2099.json
@@ -0,0 +1,18 @@
+{
+  "method": "maca",
+  "obs": "ERA5",
+  "model": "MRI-ESM2-0",
+  "member": "r1i1p1f1",
+  "grid_label": "gn",
+  "table_id": "day",
+  "scenario": "ssp585",
+  "variable": "pr",
+  "train_dates": ["1981", "2010"],
+  "predict_dates": ["1950", "2099"],
+  "latmin": "-90",
+  "latmax": "90",
+  "lonmin": "-180",
+  "lonmax": "180",
+  "year_rolling_window": 31,
+  "day_rolling_window": 21
+}
diff --git a/configs/generate_valid_configs/maca/maca_ERA5_MRI-ESM2-0_r1i1p1f1_ssp585_tasmax_-90_90_-180_180_1981_2010_1950_2099.json b/configs/generate_valid_configs/maca/maca_ERA5_MRI-ESM2-0_r1i1p1f1_ssp585_tasmax_-90_90_-180_180_1981_2010_1950_2099.json
new file mode 100644
index 00000000..1cb2292a
--- /dev/null
+++ b/configs/generate_valid_configs/maca/maca_ERA5_MRI-ESM2-0_r1i1p1f1_ssp585_tasmax_-90_90_-180_180_1981_2010_1950_2099.json
@@ -0,0 +1,18 @@
+{
+  "method": "maca",
+  "obs": "ERA5",
+  "model": "MRI-ESM2-0",
+  "member": "r1i1p1f1",
+  "grid_label": "gn",
+  "table_id": "day",
+  "scenario": "ssp585",
+  "variable": "tasmax",
+  "train_dates": ["1981", "2010"],
+  "predict_dates": ["1950", "2099"],
+  "latmin": "-90",
+  "latmax": "90",
+  "lonmin": "-180",
+  "lonmax": "180",
+  "year_rolling_window": 31,
+  "day_rolling_window": 21
+}
diff --git a/configs/generate_valid_configs/maca/maca_ERA5_MRI-ESM2-0_r1i1p1f1_ssp585_tasmin_-90_90_-180_180_1981_2010_1950_2099.json b/configs/generate_valid_configs/maca/maca_ERA5_MRI-ESM2-0_r1i1p1f1_ssp585_tasmin_-90_90_-180_180_1981_2010_1950_2099.json
new file mode 100644
index 00000000..8e0a72c2
--- /dev/null
+++ b/configs/generate_valid_configs/maca/maca_ERA5_MRI-ESM2-0_r1i1p1f1_ssp585_tasmin_-90_90_-180_180_1981_2010_1950_2099.json
@@ -0,0 +1,18 @@
+{
+  "method": "maca",
+  "obs": "ERA5",
+  "model": "MRI-ESM2-0",
+  "member": "r1i1p1f1",
+  "grid_label": "gn",
+  "table_id": "day",
+  "scenario": "ssp585",
+  "variable": "tasmin",
+  "train_dates": ["1981", "2010"],
+  "predict_dates": ["1950", "2099"],
+  "latmin": "-90",
+  "latmax": "90",
+  "lonmin": "-180",
+  "lonmax": "180",
+  "year_rolling_window": 31,
+  "day_rolling_window": 21
+}
diff --git a/configs/generate_valid_configs/maca/maca_ERA5_NorESM2-LM_r1i1p1f1_ssp245_pr_-90_90_-180_180_1981_2010_1950_2099.json b/configs/generate_valid_configs/maca/maca_ERA5_NorESM2-LM_r1i1p1f1_ssp245_pr_-90_90_-180_180_1981_2010_1950_2099.json
new file mode 100644
index 00000000..39c2a923
--- /dev/null
+++ b/configs/generate_valid_configs/maca/maca_ERA5_NorESM2-LM_r1i1p1f1_ssp245_pr_-90_90_-180_180_1981_2010_1950_2099.json
@@ -0,0 +1,18 @@
+{
+  "method": "maca",
+  "obs": "ERA5",
+  "model": "NorESM2-LM",
+  "member": "r1i1p1f1",
+  "grid_label": "gn",
+  "table_id": "day",
+  "scenario": "ssp245",
+  "variable": "pr",
+  "train_dates": ["1981", "2010"],
+  "predict_dates": ["1950", "2099"],
+  "latmin": "-90",
+  "latmax": "90",
+  "lonmin": "-180",
+  "lonmax": "180",
+  "year_rolling_window": 31,
+  "day_rolling_window": 21
+}
diff --git a/configs/generate_valid_configs/maca/maca_ERA5_NorESM2-LM_r1i1p1f1_ssp245_tasmax_-90_90_-180_180_1981_2010_1950_2099.json b/configs/generate_valid_configs/maca/maca_ERA5_NorESM2-LM_r1i1p1f1_ssp245_tasmax_-90_90_-180_180_1981_2010_1950_2099.json
new file mode 100644
index 00000000..9a492f8c
--- /dev/null
+++ b/configs/generate_valid_configs/maca/maca_ERA5_NorESM2-LM_r1i1p1f1_ssp245_tasmax_-90_90_-180_180_1981_2010_1950_2099.json
@@ -0,0 +1,18 @@
+{
+  "method": "maca",
+  "obs": "ERA5",
+  "model": "NorESM2-LM",
+  "member": "r1i1p1f1",
+  "grid_label": "gn",
+  "table_id": "day",
+  "scenario": "ssp245",
+  "variable": "tasmax",
+  "train_dates": ["1981", "2010"],
+  "predict_dates": ["1950", "2099"],
+  "latmin": "-90",
+  "latmax": "90",
+  "lonmin": "-180",
+  "lonmax": "180",
+  "year_rolling_window": 31,
+  "day_rolling_window": 21
+}
diff --git a/configs/generate_valid_configs/maca/maca_ERA5_NorESM2-LM_r1i1p1f1_ssp245_tasmin_-90_90_-180_180_1981_2010_1950_2099.json b/configs/generate_valid_configs/maca/maca_ERA5_NorESM2-LM_r1i1p1f1_ssp245_tasmin_-90_90_-180_180_1981_2010_1950_2099.json
new file mode 100644
index 00000000..43977f19
--- /dev/null
+++ b/configs/generate_valid_configs/maca/maca_ERA5_NorESM2-LM_r1i1p1f1_ssp245_tasmin_-90_90_-180_180_1981_2010_1950_2099.json
@@ -0,0 +1,18 @@
+{
+  "method": "maca",
+  "obs": "ERA5",
+  "model": "NorESM2-LM",
+  "member": "r1i1p1f1",
+  "grid_label": "gn",
+  "table_id": "day",
+  "scenario": "ssp245",
+  "variable": "tasmin",
+  "train_dates": ["1981", "2010"],
+  "predict_dates": ["1950", "2099"],
+  "latmin": "-90",
+  "latmax": "90",
+  "lonmin": "-180",
+  "lonmax": "180",
+  "year_rolling_window": 31,
+  "day_rolling_window": 21
+}
diff --git a/flows/maca_flow.py b/flows/maca_flow.py
deleted file mode 100644
index 1523b30a..00000000
--- a/flows/maca_flow.py
+++ /dev/null
@@ -1,660 +0,0 @@
-from typing import Any, Dict, List, Optional, Tuple, Union
-
-import xarray as xr
-from prefect import Flow, Parameter, task
-from xpersist import CacheStore
-from xpersist.prefect.result import XpersistResult
-
-from cmip6_downscaling import config, runtimes
-from cmip6_downscaling.methods.detrend import calc_epoch_trend, remove_epoch_trend
-from cmip6_downscaling.methods.maca import maca_bias_correction, maca_construct_analogs
-from cmip6_downscaling.methods.regions import combine_outputs, generate_subdomains
-
-# TODO: figure out where these are located
-from cmip6_downscaling.tasks.common_tasks import (
-    get_coarse_obs_task,
-    get_gcm_task,
-    get_obs_task,
-    path_builder_task,
-    rechunker_task,
-)
-from cmip6_downscaling.workflows.paths import (
-    make_bias_corrected_gcm_path,
-    make_epoch_adjusted_downscaled_gcm_path,
-    make_epoch_adjusted_gcm_path,
-    make_epoch_replaced_downscaled_gcm_path,
-    make_epoch_trend_path,
-    make_maca_output_path,
-)
-from cmip6_downscaling.workflows.utils import rechunk_zarr_array_with_caching, regrid_ds
-
-runtime = runtimes.get_runtime()
-
-
-intermediate_cache_store = CacheStore(
-    config.get('storage.intermediate.uri'),
-    storage_options=config.get('storage.intermediate.storage_options'),
-)
-results_cache_store = CacheStore(
-    config.get('storage.results.uri'), storage_options=config.get('storage.results.storage_options')
-)
-serializer_dump_kwargs = config.get('storage.xpersist_overwrite')
-
-
-@task(
-    checkpoint=True,
-    result=XpersistResult(
-        intermediate_cache_store,
-        serializer="xarray.zarr",
-        serializer_dump_kwargs=serializer_dump_kwargs,
-    ),
-    target=make_epoch_trend_path,
-)
-def calc_epoch_trend_task(
-    gcm: str,
-    scenario: str,
-    variables: List[str],
-    train_period_start: str,
-    train_period_end: str,
-    predict_period_start: str,
-    predict_period_end: str,
-    day_rolling_window: int = 21,
-    year_rolling_window: int = 31,
-    **kwargs,
-):
-    """
-    Task to calculate the epoch trends in MACA. The epoch trend is a long term rolling average, and thus the first and
-    last few years of the output suffers from edge effects. Thus, this task gets additional years for calculating the
-    rolling averages.
-
-    Parameters
-    ----------
-    gcm: str
-        Name of gcm model
-    scenario: str
-        Name of future emission scenario
-    variables: List[str]
-        Names of the variables used in obs and gcm dataset (including features and label)
-    train_period_start: str
-        Start year of training/historical period
-    train_period_end: str
-        End year of training/historical period
-    predict_period_start: str
-        Start year of predict/future period
-    predict_period_end: str
-        End year of predict/future period
-    day_rolling_window: int
-        Number of days to include when calculating the rolling average
-    year_rolling_window: int
-        Number of years to include when calculating the rolling average
-
-    Returns
-    -------
-    trend: xr.Dataset
-        The long term average trend
-    """
-    # obtain a buffer period for both training and prediction period equal to half of the year_rolling_window
-    y_offset = int((year_rolling_window - 1) / 2)
-
-    train_start = int(train_period_start) - y_offset
-    train_end = int(train_period_end) + y_offset
-    predict_start = int(predict_period_start) - y_offset
-    predict_end = int(predict_period_end) + y_offset
-
-    # make sure there are no overlapping years
-    if train_end > int(predict_period_start):
-        train_end = int(predict_period_start) - 1
-        predict_start = int(predict_period_start)
-    elif train_end > predict_start:
-        predict_start = train_end + 1
-
-    ds_gcm_full_time = get_gcm_task.run(
-        gcm=gcm,
-        scenario=scenario,
-        variables=[label],
-        train_period_start=str(int(train_start)),
-        train_period_end=str(int(train_end)),
-        predict_period_start=str(int(predict_start)),
-        predict_period_end=str(int(predict_end)),
-        chunking_approach='full_time',
-        cache_within_rechunk=True,
-    )
-
-    # note that this is the non-buffered slice
-    historical_period = slice(train_period_start, train_period_end)
-    predict_period = slice(predict_period_start, predict_period_end)
-    trend = calc_epoch_trend(
-        data=ds_gcm_full_time,
-        historical_period=historical_period,
-        day_rolling_window=day_rolling_window,
-        year_rolling_window=year_rolling_window,
-    )
-
-    hist_trend = trend.sel(time=historical_period)
-    pred_trend = trend.sel(time=predict_period)
-
-    trend = xr.combine_by_coords([hist_trend, pred_trend], combine_attrs='drop_conflicts')
-    return trend
-
-
-remove_epoch_trend_task = task(
-    remove_epoch_trend,
-    result=XpersistResult(
-        intermediate_cache_store,
-        serializer="xarray.zarr",
-        serializer_dump_kwargs=serializer_dump_kwargs,
-    ),
-    target=make_epoch_adjusted_gcm_path,
-)
-
-
-@task(
-    checkpoint=True,
-    result=XpersistResult(
-        intermediate_cache_store,
-        serializer="xarray.zarr",
-        serializer_dump_kwargs=serializer_dump_kwargs,
-    ),
-    target=make_bias_corrected_gcm_path,
-)
-def maca_coarse_bias_correction_task(
-    ds_gcm: xr.Dataset,
-    ds_obs: xr.Dataset,
-    train_period_start: str,
-    train_period_end: str,
-    variables: Union[str, List[str]],
-    chunking_approach: str,
-    batch_size: Optional[int] = 15,
-    buffer_size: Optional[int] = 15,
-    **kwargs,
-):
-    """
-    Task that implements the coarse scale bias correction in MACA. The historical GCM is mapped to historical
-    coarsened observation in the bias correction. Rechunks the GCM data to match observation data because
-    the bias correction model in skdownscale requires these datasets to have the same chunks/blocks.
-
-    ds_gcm: xr.Dataset
-        GCM dataset
-    ds_obs: xr.Dataset
-        Observation dataset
-    train_period_start: str
-        Start year of training/historical period
-    train_period_end: str
-        End year of training/historical period
-    variables: List[str]
-        Names of the variables used in obs and gcm dataset (including features and label)
-    chunking_approach: str
-        'full_space', 'full_time', 'matched' or None
-    batch_size: Optional[int]
-        The batch size in terms of day of year to bias correct together
-    buffer_size: Optional[int]
-        The buffer size in terms of day of year to include in the bias correction
-
-    Returns
-    -------
-    bias_corrected: xr.Dataset
-        Bias corrected GCM dataset
-    """
-    # TODO: test if this is needed if both ds_gcm and ds_obs are chunked in full time
-    ds_gcm_rechunked = rechunk_zarr_array_with_caching(
-        zarr_array=ds_gcm, template_chunk_array=ds_obs
-    )
-
-    historical_period = slice(train_period_start, train_period_end)
-    bias_corrected = maca_bias_correction(
-        ds_gcm=ds_gcm_rechunked,
-        ds_obs=ds_obs,
-        historical_period=historical_period,
-        variables=variables,
-        batch_size=batch_size,
-        buffer_size=buffer_size,
-    )
-
-    return bias_corrected
-
-
-@task(nout=4)
-def get_subdomains_task(
-    ds_obs: xr.Dataset, buffer_size: Union[float, int] = 5, region_def: str = 'ar6'
-):
-    """
-    Get the definition of subdomains according to region_def specified.
-
-    Parameters
-    ----------
-    ds_obs: xr.Dataset
-        Observation dataset
-    buffer_size : int or float
-        Buffer size in unit of degree. for each subdomain, how much extra area to run for each subdomain
-    region_def : str
-        Subregion definition name. Options are `'ar6'` or `'srex'`. See the docs https://regionmask.readthedocs.io/en/stable/defined_scientific.html for more details.
-
-    Returns
-    -------
-    subdomains_list: List
-        List of all subdomain boundaries sorted by the region code
-    subdomains_dict : dict
-        Dictionary mapping subdomain code to bounding boxes ([min_lon, min_lat, max_lon, max_lat]) for each subdomain
-    mask : xarray.DataArray
-        Mask of which subdomain code to use for each grid cell
-    n_subdomains: int
-        The number of subdomains that are included
-    """
-    subdomains_dict, mask = generate_subdomains(
-        ex_output_grid=ds_obs.isel(time=0),
-        buffer_size=buffer_size,
-        region_def=region_def,
-    )
-
-    subdomains_list = [subdomains_dict[k] for k in sorted(subdomains_dict.keys())]
-    return subdomains_list, subdomains_dict, mask, len(subdomains_list)
-
-
-@task(nout=3)
-def subset_task(
-    ds_gcm: xr.Dataset,
-    ds_obs_coarse: xr.Dataset,
-    ds_obs_fine: xr.Dataset,
-    subdomains_list: List[Tuple[float, float, float, float]],
-):
-    """
-    Subset each dataset spatially into areas within each subdomain bound.
-
-    Parameters
-    ----------
-    ds_gcm: xr.Dataset
-        GCM dataset, original/coarse resolution
-    ds_obs_coarse: xr.Dataset
-        Observation dataset coarsened to the GCM resolution
-    ds_obs_fine: xr.Dataset
-        Observation dataset, original/fine resolution
-    subdomains_list: List
-        List of all subdomain boundaries sorted by the region code
-
-    Returns
-    -------
-    ds_gcm_list: List
-        List of subsetted GCM datasets in the same order of subdomains_list
-    ds_obs_coarse_list: List
-        List of subsetted coarened obs datasets in the same order of subdomains_list
-    ds_obs_fine_list: List
-        List of subsetted fine obs datasets in the same order of subdomains_list
-    """
-    ds_gcm_list, ds_obs_coarse_list, ds_obs_fine_list = [], [], []
-    for (min_lon, min_lat, max_lon, max_lat) in subdomains_list:
-        lat_slice = slice(max_lat, min_lat)
-        lon_slice = slice(min_lon, max_lon)
-        ds_gcm_list.append(ds_gcm.sel(lat=lat_slice, lon=lon_slice))
-        ds_obs_coarse_list.append(ds_obs_coarse.sel(lat=lat_slice, lon=lon_slice))
-        ds_obs_fine_list.append(ds_obs_fine.sel(lat=lat_slice, lon=lon_slice))
-
-    return ds_gcm_list, ds_obs_coarse_list, ds_obs_fine_list
-
-
-maca_construct_analogs_task = task(
-    maca_construct_analogs,
-    result=XpersistResult(
-        intermediate_cache_store,
-        serializer="xarray.zarr",
-        serializer_dump_kwargs=serializer_dump_kwargs,
-    ),
-    target=make_epoch_adjusted_downscaled_gcm_path,
-)
-
-
-@task(
-    checkpoint=True,
-    result=XpersistResult(
-        intermediate_cache_store,
-        serializer="xarray.zarr",
-        serializer_dump_kwargs=serializer_dump_kwargs,
-    ),
-    target=make_epoch_adjusted_downscaled_gcm_path,
-)
-def combine_outputs_task(
-    ds_list: List[xr.Dataset],
-    subdomains_dict: Dict[Union[int, float], Any],
-    mask: xr.DataArray,
-    **kwargs,
-):
-    """
-    Combine a list of datasets spatially according to the subdomain list and mask.
-
-    Parameters
-    ----------
-    ds_list: List[xr.Dataset]
-        List of datasets to be combined
-    subdomains_dict : dict
-        Dictionary mapping subdomain code to bounding boxes ([min_lon, min_lat, max_lon, max_lat]) for each subdomain
-    mask : xarray.DataArray
-        Mask of which subdomain code to use for each grid cell
-
-    Returns
-    -------
-    combined_output: xr.Dataset
-        The combined output
-    """
-    ds_dict = {k: ds_list.pop(0) for k in sorted(subdomains_dict.keys())}
-    return combine_outputs(ds_dict=ds_dict, mask=mask)
-
-
-@task(
-    checkpoint=True,
-    result=XpersistResult(
-        intermediate_cache_store,
-        serializer="xarray.zarr",
-        serializer_dump_kwargs=serializer_dump_kwargs,
-    ),
-    target=make_epoch_replaced_downscaled_gcm_path,
-)
-def maca_epoch_replacement_task(
-    ds_gcm_fine: xr.Dataset,
-    trend_coarse: xr.Dataset,
-    **kwargs,
-) -> xr.Dataset:
-    """
-    Replace the epoch trend. The trend was calculated on coarse scale GCM, so the trend is first interpolated
-    into the finer grid before being added back into the downscaled GCM.
-
-    Parameters
-    ----------
-    ds_gcm_fine: xr.Dataset
-        Downscaled GCM, fine/observation resolution
-    trend_coarse: xr.Dataset
-        The epoch trend, coarse/original GCM resolution
-
-    Returns
-    -------
-    epoch_replaced_gcm: xr.Dataset
-        The downscaled GCM dataset with the epoch trend replaced back
-    """
-    trend_fine = regrid_ds(
-        ds=trend_coarse,
-        target_grid_ds=ds_gcm_fine.isel(time=0).chunk({'lat': -1, 'lon': -1}),
-    )
-
-    return ds_gcm_fine + trend_fine
-
-
-@task(
-    checkpoint=True,
-    result=XpersistResult(
-        intermediate_cache_store,
-        serializer="xarray.zarr",
-        serializer_dump_kwargs=serializer_dump_kwargs,
-    ),
-    target=make_maca_output_path,
-)
-def maca_fine_bias_correction_task(
-    ds_gcm: xr.Dataset,
-    ds_obs: xr.Dataset,
-    train_period_start: str,
-    train_period_end: str,
-    label: str,
-    batch_size: Optional[int] = 15,
-    buffer_size: Optional[int] = 15,
-    **kwargs,
-):
-    """
-    Task that implements the fine scale bias correction in MACA. The historical GCM is mapped to historical
-    coarsened observation in the bias correction. Rechunks the GCM data to match observation data because
-    the bias correction model in skdownscale requires these datasets to have the same chunks/blocks.
-
-    ds_gcm: xr.Dataset
-        GCM dataset
-    ds_obs: xr.Dataset
-        Observation dataset
-    train_period_start: str
-        Start year of training/historical period
-    train_period_end: str
-        End year of training/historical period
-    variables: List[str]
-        Names of the variables used in obs and gcm dataset (including features and label)
-    chunking_approach: str
-        'full_space', 'full_time', 'matched' or None
-    batch_size: Optional[int]
-        The batch size in terms of day of year to bias correct together
-    buffer_size: Optional[int]
-        The buffer size in terms of day of year to include in the bias correction
-
-    Returns
-    -------
-    bias_corrected: xr.Dataset
-        Bias corrected GCM dataset
-    """
-    ds_gcm_rechunked = rechunk_zarr_array_with_caching(
-        zarr_array=ds_gcm, template_chunk_array=ds_obs
-    )
-
-    historical_period = slice(train_period_start, train_period_end)
-    bias_corrected = maca_bias_correction(
-        ds_gcm=ds_gcm_rechunked,
-        ds_obs=ds_obs,
-        historical_period=historical_period,
-        variables=[label],
-        batch_size=batch_size,
-        buffer_size=buffer_size,
-    )
-
-    return bias_corrected
-
-
-with Flow(
-    name='maca',
-    storage=runtime.storage,
-    run_config=runtime.run_config,
-    executor=runtime.executor,
-) as maca_flow:
-    # following https://climate.northwestknowledge.net/MACA/MACAmethod.php
-
-    obs = Parameter("OBS")
-    gcm = Parameter("GCM")
-    scenario = Parameter("SCENARIO")
-    label = Parameter("LABEL")
-
-    train_period_start = Parameter("TRAIN_PERIOD_START")
-    train_period_end = Parameter("TRAIN_PERIOD_END")
-    predict_period_start = Parameter("PREDICT_PERIOD_START")
-    predict_period_end = Parameter("PREDICT_PERIOD_END")
-
-    epoch_adjustment_day_rolling_window = Parameter("EPOCH_ADJUSTMENT_DAY_ROLLING_WINDOW")
-    epoch_adjustment_year_rolling_window = Parameter("EPOCH_ADJUSTMENT_YEAR_ROLLING_WINDOW")
-    bias_correction_batch_size = Parameter("BIAS_CORRECTION_BATCH_SIZE")
-    bias_correction_buffer_size = Parameter("BIAS_CORRECTION_BUFFER_SIZE")
-    constructed_analog_n_analogs = Parameter("CONSTRUCTED_ANALOG_N_ANALOGS")
-    constructed_analog_doy_range = Parameter("CONSTRUCTED_ANALOG_DOY_RANGE")
-
-    ## Step 0: tasks to get inputs and set up
-    ## Step 1: Common Grid -- this step is skipped since it seems like an unnecessary extra step for convenience
-
-    # dictionary with information to build appropriate paths for caching
-    gcm_grid_spec, obs_identifier, gcm_identifier = path_builder_task(
-        obs=obs,
-        gcm=gcm,
-        scenario=scenario,
-        train_period_start=train_period_start,
-        train_period_end=train_period_end,
-        predict_period_start=predict_period_start,
-        predict_period_end=predict_period_end,
-        variables=[label],
-    )
-
-    # get original resolution observations
-    ds_obs_full_space = get_obs_task(
-        obs=obs,
-        train_period_start=train_period_start,
-        train_period_end=train_period_end,
-        variables=[label],
-        chunking_approach='full_space',
-        cache_within_rechunk=True,
-    )
-
-    # get coarsened resolution observations
-    # this coarse obs is going to be used in bias correction next, so rechunk into full time first
-    ds_obs_coarse_full_time = get_coarse_obs_task(
-        ds_obs=ds_obs_full_space,
-        gcm=gcm,
-        chunking_approach='full_time',
-        gcm_grid_spec=gcm_grid_spec,
-        obs_identifier=obs_identifier,
-    )
-
-    ## Step 2: Epoch Adjustment -- all variables undergo this epoch adjustment
-    # TODO: in order to properly do a 31 year average, might need to run this step with the entire future period in GCMs
-    # but this might be too memory intensive in the later task
-    coarse_epoch_trend = calc_epoch_trend_task(
-        gcm=gcm,
-        scenario=scenario,
-        variables=[label],
-        train_period_start=train_period_start,
-        train_period_end=train_period_end,
-        predict_period_start=predict_period_start,
-        predict_period_end=predict_period_end,
-        day_rolling_window=epoch_adjustment_day_rolling_window,
-        year_rolling_window=epoch_adjustment_year_rolling_window,
-    )
-
-    # get gcm
-    ds_gcm_full_time = get_gcm_task(
-        gcm=gcm,
-        scenario=scenario,
-        variables=[label],
-        train_period_start=train_period_start,
-        train_period_end=train_period_end,
-        predict_period_start=predict_period_start,
-        predict_period_end=predict_period_end,
-        chunking_approach='full_time',
-        cache_within_rechunk=True,
-    )
-
-    epoch_adjusted_gcm = remove_epoch_trend_task(
-        data=ds_gcm_full_time,
-        trend=coarse_epoch_trend,
-        day_rolling_window=epoch_adjustment_day_rolling_window,
-        year_rolling_window=epoch_adjustment_year_rolling_window,
-        gcm_identifier=gcm_identifier,
-    )
-
-    ## Step 3: Coarse Bias Correction
-    bias_corrected_gcm = maca_coarse_bias_correction_task(
-        ds_gcm=epoch_adjusted_gcm,
-        ds_obs=ds_obs_coarse_full_time,
-        train_period_start=train_period_start,
-        train_period_end=train_period_end,
-        variables=[label],
-        batch_size=bias_correction_batch_size,
-        buffer_size=bias_correction_buffer_size,
-        method='maca_edcdfm',
-        gcm_identifier=gcm_identifier,
-        chunking_approach='matched',
-    )
-
-    # do epoch adjustment again for multiplicative variables, see MACA v1 vs. v2 guide for details
-    if label in ['pr', 'huss', 'vas', 'uas']:
-        coarse_epoch_trend_2 = calc_epoch_trend_task(
-            data=bias_corrected_gcm,
-            train_period_start=train_period_start,
-            train_period_end=train_period_end,
-            day_rolling_window=epoch_adjustment_day_rolling_window,
-            year_rolling_window=epoch_adjustment_year_rolling_window,
-            gcm_identifier=f'{gcm_identifier}_2',
-        )
-
-        bias_corrected_gcm = remove_epoch_trend_task(
-            data=bias_corrected_gcm,
-            trend=coarse_epoch_trend_2,
-            day_rolling_window=epoch_adjustment_day_rolling_window,
-            year_rolling_window=epoch_adjustment_year_rolling_window,
-            gcm_identifier=f'{gcm_identifier}_2',
-        )
-
-    ## Step 4: Constructed Analogs
-    # rechunk into full space and cache the output
-    bias_corrected_gcm_full_space = rechunker_task(
-        zarr_array=bias_corrected_gcm,
-        chunking_approach='full_space',
-        naming_func=make_bias_corrected_gcm_path,
-        gcm_identifier=gcm_identifier,
-        method='maca_edcdfm',
-    )
-
-    # subset into regions
-    subdomains_list, subdomains_dict, mask, n_subdomains = get_subdomains_task(
-        ds_obs=ds_obs_full_space
-    )
-
-    # everything should be rechunked to full space and then subset
-    ds_obs_coarse_full_space = get_coarse_obs_task(
-        ds_obs=ds_obs_full_space,
-        gcm=gcm,
-        chunking_approach='full_space',
-        gcm_grid_spec=gcm_grid_spec,
-        obs_identifier=obs_identifier,
-    )
-    # all inputs into the map function needs to be a list
-    ds_gcm_list, ds_obs_coarse_list, ds_obs_fine_list = subset_task(
-        ds_gcm=bias_corrected_gcm_full_space,
-        ds_obs_coarse=ds_obs_coarse_full_space,
-        ds_obs_fine=ds_obs_full_space,
-        subdomains_list=subdomains_list,
-    )
-
-    # downscaling by constructing analogs
-    downscaled_gcm_list = maca_construct_analogs_task.map(
-        ds_gcm=ds_gcm_list,
-        ds_obs_coarse=ds_obs_coarse_list,
-        ds_obs_fine=ds_obs_fine_list,
-        subdomain_bound=subdomains_list,
-        n_analogs=[constructed_analog_n_analogs] * n_subdomains,
-        doy_range=[constructed_analog_doy_range] * n_subdomains,
-        gcm_identifier=[gcm_identifier] * n_subdomains,
-        label=[label] * n_subdomains,
-    )
-
-    # combine back into full domain
-    combined_downscaled_output = combine_outputs_task(
-        ds_list=downscaled_gcm_list,
-        subdomains_dict=subdomains_dict,
-        mask=mask,
-        gcm_identifier=gcm_identifier,
-        label=label,
-    )
-
-    ## Step 5: Epoch Replacement
-    if label in ['pr', 'huss', 'vas', 'uas']:
-        combined_downscaled_output = maca_epoch_replacement_task(
-            ds_gcm_fine=combined_downscaled_output,
-            trend_coarse=coarse_epoch_trend_2,
-            day_rolling_window=epoch_adjustment_day_rolling_window,
-            year_rolling_window=epoch_adjustment_year_rolling_window,
-            gcm_identifier=f'{gcm_identifier}_2',
-        )
-
-    epoch_replaced_gcm = maca_epoch_replacement_task(
-        ds_gcm_fine=combined_downscaled_output,
-        trend_coarse=coarse_epoch_trend,
-        day_rolling_window=epoch_adjustment_day_rolling_window,
-        year_rolling_window=epoch_adjustment_year_rolling_window,
-        gcm_identifier=gcm_identifier,
-    )
-
-    ds_obs_full_time = get_obs_task(
-        obs=obs,
-        train_period_start=train_period_start,
-        train_period_end=train_period_end,
-        variables=[label],
-        chunking_approach='full_time',
-        cache_within_rechunk=True,
-    )
-
-    ## Step 6: Fine Bias Correction
-    final_output = maca_fine_bias_correction_task(
-        ds_gcm=epoch_replaced_gcm,
-        ds_obs=ds_obs_full_time,
-        train_period_start=train_period_start,
-        train_period_end=train_period_end,
-        variables=[label],
-        batch_size=bias_correction_batch_size,
-        buffer_size=bias_correction_buffer_size,
-        gcm_identifier=gcm_identifier,
-    )
diff --git a/flows/methods/maca/flow.py b/flows/methods/maca/flow.py
new file mode 100644
index 00000000..0548b795
--- /dev/null
+++ b/flows/methods/maca/flow.py
@@ -0,0 +1,214 @@
+from prefect import Flow, Parameter, unmapped
+
+from cmip6_downscaling import config, runtimes
+from cmip6_downscaling.methods.common.tasks import (  # run_analyses,
+    finalize,
+    finalize_on_failure,
+    get_experiment,
+    get_obs,
+    get_pyramid_weights,
+    get_weights,
+    make_run_parameters,
+    pyramid,
+    rechunk,
+    regrid,
+    time_summary,
+)
+from cmip6_downscaling.methods.maca.tasks import (
+    bias_correction,
+    combine_regions,
+    construct_analogs,
+    epoch_trend,
+    get_region_numbers,
+    replace_epoch_trend,
+    split_by_region,
+)
+
+runtime = runtimes.get_runtime()
+print(runtime)
+
+
+with Flow(
+    name='maca',
+    storage=runtime.storage,
+    run_config=runtime.run_config,
+    executor=runtime.executor,
+) as flow:
+    # following https://climate.northwestknowledge.net/MACA/MACAmethod.php
+    run_parameters = make_run_parameters(
+        method=Parameter("method"),
+        obs=Parameter("obs"),
+        model=Parameter("model"),
+        member=Parameter("member"),
+        grid_label=Parameter("grid_label"),
+        table_id=Parameter("table_id"),
+        scenario=Parameter("scenario"),
+        variable=Parameter("variable"),
+        latmin=Parameter("latmin"),
+        latmax=Parameter("latmax"),
+        lonmin=Parameter("lonmin"),
+        lonmax=Parameter("lonmax"),
+        train_dates=Parameter("train_dates"),
+        predict_dates=Parameter("predict_dates"),
+        year_rolling_window=Parameter("year_rolling_window"),
+        day_rolling_window=Parameter("day_rolling_window"),
+    )
+
+    p = {}
+
+    ## Step 0: tasks to get inputs and set up
+    ## Step 1: Common Grid -- this step is skipped since it seems like an unnecessary extra step for convenience
+
+    # input datasets
+    p['gcm_to_obs_weights'] = get_weights(run_parameters=run_parameters, direction='gcm_to_obs')
+    p['obs_to_gcm_weights'] = get_weights(run_parameters=run_parameters, direction='obs_to_gcm')
+
+    # get original resolution observations
+    p['obs_path'] = get_obs(run_parameters)
+    p['obs_full_time_path'] = rechunk(path=p['obs_path'], pattern='full_time')
+
+    p['obs_full_space_path'] = rechunk(path=p['obs_path'], pattern='full_space')
+    p['experiment_path'] = get_experiment(run_parameters, time_subset='both')
+
+    # get coarsened resolution observations
+    # this coarse obs is going to be used in bias correction next, so rechunk into full time first
+    p['coarse_obs_full_space_path'] = regrid(
+        p['obs_full_space_path'], p['experiment_path'], weights_path=p['obs_to_gcm_weights']
+    )
+
+    ## Step 2: Epoch Adjustment -- all variables undergo this epoch adjustment
+    # TODO: in order to properly do a 31 year average, might need to run this step with the entire future period in GCMs
+
+    p['coarse_epoch_trend_path'], p['detrended_data_path'] = epoch_trend(
+        p['experiment_path'], run_parameters
+    )
+
+    p['coarse_epoch_trend_full_space_path'] = rechunk(
+        p['coarse_epoch_trend_path'], pattern='full_space'
+    )
+
+    p['fine_epoch_trend_full_space_path'] = regrid(
+        p['coarse_epoch_trend_full_space_path'],
+        p['obs_full_space_path'],
+        weights_path=p['gcm_to_obs_weights'],
+        pre_chunk_def={'time': 30},
+    )
+
+    p['coarse_obs_full_time_path'] = rechunk(
+        p['coarse_obs_full_space_path'], pattern='full_time', template=p['detrended_data_path']
+    )
+
+    # get gcm
+    # 1981-2100 extent time subset
+    p['experiment_predict_full_time_path'] = rechunk(p['experiment_path'], pattern='full_time')
+
+    ## Step 3: Coarse Bias Correction
+    # rechunk to make detrended data match the coarse obs
+    p['detrend_gcm_full_time'] = rechunk(
+        p['detrended_data_path'],
+        template=p[
+            'coarse_obs_full_time_path'
+        ],  # this is not working. time is chunked to match coarse obs
+    )
+
+    # inputs should be in full-time
+    p['bias_corrected_gcm_full_time_path'] = bias_correction(
+        x_path=p['detrend_gcm_full_time'],
+        y_path=p['coarse_obs_full_time_path'],
+        run_parameters=run_parameters,
+    )
+
+    # rechunk into full space and cache the output
+    # p['bias_corrected_gcm_full_space_path'] = rechunk(
+    #     p['bias_corrected_gcm_full_time_path'], pattern='full_space'
+    # )
+
+    p['region_numbers'] = get_region_numbers()
+
+    p['bias_corrected_gcm_region_paths'] = split_by_region.map(
+        p['region_numbers'], unmapped(p['bias_corrected_gcm_full_time_path'])
+    )
+    p['coarse_obs_region_paths'] = split_by_region.map(
+        p['region_numbers'], unmapped(p['coarse_obs_full_time_path'])
+    )
+    p['obs_region_paths'] = split_by_region.map(
+        p['region_numbers'], unmapped(p['obs_full_time_path'])
+    )
+
+    # Step 4: Constructed Analogs
+    # Note: This is option was added to allow this step to be run on the local executor while the rest of the steps can be run with
+    # The dask executor
+    if config.get('run_options.construct_analogs'):
+        p['constructed_analogs_region_paths'] = construct_analogs.map(
+            gcm_path=p['bias_corrected_gcm_region_paths'],
+            coarse_obs_path=p['coarse_obs_region_paths'],
+            fine_obs_path=p['obs_region_paths'],
+            run_parameters=unmapped(run_parameters),
+        )
+
+        if config.get('run_options.combine_regions'):
+
+            p['combined_analogs_full_time_path'] = combine_regions(
+                regions=p['region_numbers'],
+                region_paths=p['constructed_analogs_region_paths'],
+                template_path=p['fine_epoch_trend_full_space_path'],
+            )
+
+            p['fine_epoch_trend_full_time_path'] = rechunk(
+                p['fine_epoch_trend_full_space_path'],
+                pattern='full_time',
+                template=p['combined_analogs_full_time_path'],
+            )
+
+            # Step 5: Epoch Replacement
+            p['epoch_replaced_full_time_path'] = replace_epoch_trend(
+                p['combined_analogs_full_time_path'], p['fine_epoch_trend_full_time_path']
+            )
+
+            # Step 6: Fine Bias Correction
+            p['final_bias_corrected_full_time_path'] = bias_correction(
+                p['epoch_replaced_full_time_path'],
+                p['obs_full_time_path'],
+                run_parameters=run_parameters,
+            )
+
+            # temporary aggregations - these come out in full time
+            p['monthly_summary_path'] = time_summary(
+                p['final_bias_corrected_full_time_path'], freq='1MS'
+            )
+            p['annual_summary_path'] = time_summary(
+                p['final_bias_corrected_full_time_path'], freq='1AS'
+            )
+
+            # analysis notebook
+            # analysis_location = run_analyses(p['final_bias_corrected_full_time_path'], run_parameters)
+
+            if config.get('run_options.generate_pyramids'):
+
+                # make temporal summaries
+                p['monthly_summary_full_space_path'] = rechunk(
+                    p['monthly_summary_path'], pattern='full_space'
+                )
+                p['annual_summary_full_space_path'] = rechunk(
+                    p['annual_summary_path'], pattern='full_space'
+                )
+
+                # pyramids
+                p['pyramid_weights'] = get_pyramid_weights(run_parameters=run_parameters, levels=4)
+
+                p['monthly_pyramid_path'] = pyramid(
+                    p['monthly_summary_full_space_path'],
+                    weights_pyramid_path=p['pyramid_weights'],
+                    levels=4,
+                )
+                p['annual_pyramid_path'] = pyramid(
+                    p['annual_summary_full_space_path'],
+                    weights_pyramid_path=p['pyramid_weights'],
+                    levels=4,
+                )
+
+    # finalize
+    ref = finalize(run_parameters=run_parameters, **p)
+    finalize_on_failure(run_parameters=run_parameters, **p)
+
+flow.set_reference_tasks([ref])
diff --git a/notebooks/maca_workflow.ipynb b/notebooks/maca_workflow.ipynb
deleted file mode 100644
index 9af806c9..00000000
--- a/notebooks/maca_workflow.ipynb
+++ /dev/null
@@ -1,141 +0,0 @@
-{
- "cells": [
-  {
-   "cell_type": "code",
-   "execution_count": null,
-   "id": "b31621b4-3bb2-49cc-ba66-761c347167db",
-   "metadata": {},
-   "outputs": [],
-   "source": [
-    "%load_ext autoreload\n",
-    "%autoreload 2"
-   ]
-  },
-  {
-   "cell_type": "code",
-   "execution_count": null,
-   "id": "cb5cf5ef-7a5c-44f3-b9a5-a8ea33859217",
-   "metadata": {},
-   "outputs": [],
-   "source": [
-    "import os\n",
-    "import time\n",
-    "\n",
-    "os.environ[\"PREFECT__FLOWS__CHECKPOINTING\"] = \"true\"\n",
-    "\n",
-    "from funnel import CacheStore\n",
-    "from funnel.prefect.result import FunnelResult\n",
-    "from prefect import task, Flow, Parameter\n",
-    "from prefect.executors import DaskExecutor\n",
-    "import xarray as xr"
-   ]
-  },
-  {
-   "cell_type": "code",
-   "execution_count": null,
-   "id": "76964487-7065-4f72-a590-e8d5397a1fb6",
-   "metadata": {},
-   "outputs": [],
-   "source": [
-    "os.environ.get(\"AZURE_STORAGE_CONNECTION_STRING\")"
-   ]
-  },
-  {
-   "cell_type": "code",
-   "execution_count": null,
-   "id": "21621ece-a7fb-4bc8-9996-9466c5180831",
-   "metadata": {},
-   "outputs": [],
-   "source": [
-    "import fsspec\n",
-    "import os\n",
-    "\n",
-    "connection_string = os.environ.get(\"AZURE_STORAGE_CONNECTION_STRING\")\n",
-    "\n",
-    "fs = fsspec.filesystem(\"az\", connection_string=connection_string)\n",
-    "\n",
-    "# # fs.rm('flow-outputs/intermediate/bias_corrected_gcm',recursive=True)\n",
-    "# # fs.rm('flow-outputs/intermediate/funnel_metadata_store/bias_corrected_gcm',recursive=True)\n",
-    "# # fs.rm('flow-outputs/intermediate/bias_corrected_obs',recursive=True)\n",
-    "# # fs.rm('flow-outputs/intermediate/funnel_metadata_store/bias_corrected_obs',recursive=True)\n",
-    "# fs.rm('flow-outputs/intermediate/interpolated_obs',recursive=True)\n",
-    "# fs.rm('flow-outputs/intermediate/epoch_adjusted_gcm', recursive=True)\n",
-    "# fs.rm('flow-outputs/intermediate/gcm_epoch_trend', recursive=True)\n",
-    "# fs.rm('flow-outputs/intermediate/maca_epoch_adjusted_downscaled_output', recursive=True)\n",
-    "# fs.rm('flow-outputs/intermediate/epoch_replaced_gcm', recursive=True)\n",
-    "\n",
-    "# fs.rm('flow-outputs/intermediate/maca_test_bias_correction_rechunk.zarr', recursive=True)\n",
-    "# fs.rm('flow-outputs/intermediate/maca_epoch_adjusted_downscaled_output/MIROC6_ssp370_1991_1995_2071_2075_tasmax_tasmax_0.0_-90.0_360.0_90.0.zarr', recursive=True)\n",
-    "# fs.rm('flow-outputs/intermediate/rechunked_obs/ERA5_1991_1995_tasmax_full_time.zarr', recursive=True)"
-   ]
-  },
-  {
-   "cell_type": "code",
-   "execution_count": null,
-   "id": "8c29b7c8-34cd-4c9d-ae46-f5b2c2409cc6",
-   "metadata": {},
-   "outputs": [],
-   "source": [
-    "run_hyperparameters = {\n",
-    "    \"OBS\": \"ERA5\",\n",
-    "    \"GCM\": \"MIROC6\",\n",
-    "    \"SCENARIO\": \"ssp370\",\n",
-    "    \"LABEL\": \"tasmax\",\n",
-    "    \"TRAIN_PERIOD_START\": \"1991\",\n",
-    "    \"TRAIN_PERIOD_END\": \"1995\",\n",
-    "    \"PREDICT_PERIOD_START\": \"2071\",\n",
-    "    \"PREDICT_PERIOD_END\": \"2075\",\n",
-    "    \"EPOCH_ADJUSTMENT_DAY_ROLLING_WINDOW\": 21,\n",
-    "    \"EPOCH_ADJUSTMENT_YEAR_ROLLING_WINDOW\": 3,\n",
-    "    \"BIAS_CORRECTION_BATCH_SIZE\": 15,\n",
-    "    \"BIAS_CORRECTION_BUFFER_SIZE\": 15,\n",
-    "    \"CONSTRUCTED_ANALOG_N_ANALOGS\": 10,\n",
-    "    \"CONSTRUCTED_ANALOG_DOY_RANGE\": 45,\n",
-    "}"
-   ]
-  },
-  {
-   "cell_type": "code",
-   "execution_count": null,
-   "id": "c2c90bad-8866-4754-ac90-4f4a212c2fcb",
-   "metadata": {},
-   "outputs": [],
-   "source": [
-    "from cmip6_downscaling.workflows.maca_flow import maca_flow"
-   ]
-  },
-  {
-   "cell_type": "code",
-   "execution_count": null,
-   "id": "6c355712-2f6e-4cfb-8da5-a040a124e111",
-   "metadata": {
-    "tags": []
-   },
-   "outputs": [],
-   "source": [
-    "maca_flow.run(parameters=run_hyperparameters)"
-   ]
-  }
- ],
- "metadata": {
-  "kernelspec": {
-   "display_name": "Python [conda env:notebook] *",
-   "language": "python",
-   "name": "conda-env-notebook-py"
-  },
-  "language_info": {
-   "codemirror_mode": {
-    "name": "ipython",
-    "version": 3
-   },
-   "file_extension": ".py",
-   "mimetype": "text/x-python",
-   "name": "python",
-   "nbconvert_exporter": "python",
-   "pygments_lexer": "ipython3",
-   "version": "3.9.7"
-  }
- },
- "nbformat": 4,
- "nbformat_minor": 5
-}
diff --git a/requirements.txt b/requirements.txt
index ab7cc938..8c6643a6 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -10,7 +10,6 @@ intake-esm
 intake-xarray
 ndpyramid == 0.0.6
 pandas
-pangeo-xesmf == 0.6.2
 prefect == 0.15.13
 rechunker
 regionmask
diff --git a/setup.cfg b/setup.cfg
index 139d1ba4..a00b57ee 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -6,7 +6,7 @@ max-complexity = 18
 select = B,C,E,F,W,T4,B9
 
 [isort]
-known_third_party=ESMF,carbonplan_data,cartopy,climate_indices,dask,datatree,donfig,fsspec,geopandas,intake,matplotlib,ndpyramid,numba,numpy,packaging,pandas,prefect,pytest,rechunker,regionmask,scipy,seaborn,setuptools,skdownscale,sklearn,tensorflow,tqdm,upath,xarray,xarray_schema,xclim,xesmf,xpersist,zarr
+known_third_party=ESMF,carbonplan_data,cartopy,climate_indices,dask,datatree,donfig,fsspec,geopandas,intake,matplotlib,ndpyramid,numba,numpy,packaging,pandas,prefect,pytest,rechunker,regionmask,scipy,seaborn,setuptools,skdownscale,sklearn,tensorflow,tqdm,upath,xarray,xarray_schema,xclim,xesmf,zarr
 multi_line_output=3
 include_trailing_comma=True
 force_grid_wrap=0


Test patch:
diff --git a/tests/methods/common/test_tasks.py b/tests/methods/common/test_tasks.py
index 39c4aeef..301d4b6f 100644
--- a/tests/methods/common/test_tasks.py
+++ b/tests/methods/common/test_tasks.py
@@ -47,6 +47,8 @@
         "bias_correction_kwargs": {"detrend": "True"},
         "model_type": "PureRegression",
         "model_params": {},
+        "day_rolling_window": None,
+        "year_rolling_window": None,
     },
     {
         'method': 'gard',
@@ -68,6 +70,8 @@
         "bias_correction_kwargs": {"detrend": "True"},
         "model_type": "PureRegression",
         "model_params": {},
+        "day_rolling_window": None,
+        "year_rolling_window": None,
     },
 ]
 
diff --git a/tests/methods/test_regions.py b/tests/methods/test_regions.py
deleted file mode 100644
index 56449e74..00000000
--- a/tests/methods/test_regions.py
+++ /dev/null
@@ -1,83 +0,0 @@
-import itertools
-
-import numpy as np
-import xarray as xr
-
-from cmip6_downscaling.methods.regions import combine_outputs, generate_subdomains
-
-
-def test_generate_subdomains(example_3d_data_us_domain):
-    ds = example_3d_data_us_domain
-
-    region_def = 'ar6'
-    buffer_size = 4
-    subdomains, mask = generate_subdomains(
-        ex_output_grid=ds.isel(time=0), buffer_size=buffer_size, region_def=region_def
-    )
-
-    # make sure the correct regions are there
-    region_codes = list(subdomains.keys())
-    assert sorted(region_codes) == [1, 2, 3, 4, 5, 6, 7, 8]
-
-    # make sure the subdomain bounds are as expected
-    assert subdomains[1] == (-180, 46.0, -101.0, 85.0)
-    assert subdomains[2] == (-109.0, 46.0, -46.0, 89.0)
-
-    # make sure the regions are consistent between both outputs
-    mask_regions = np.unique(mask.values)
-    mask_regions = mask_regions[np.isfinite(mask_regions)]
-    for r in mask_regions:
-        assert r in region_codes
-
-    # make sure the output domain is as expected
-    assert mask.lat.min().values == ds.lat.min().values
-    assert mask.lat.max().values == ds.lat.max().values
-    assert mask.lon.min().values == ds.lon.min().values
-    assert mask.lon.max().values == ds.lon.max().values
-
-
-def test_combine_outputs(example_3d_data_us_domain):
-    ds = example_3d_data_us_domain
-
-    # get mask using test data
-    region_def = 'ar6'
-    buffer_size = 4
-    subdomains, mask = generate_subdomains(
-        ex_output_grid=ds.isel(time=0), buffer_size=buffer_size, region_def=region_def
-    )
-
-    # make mock output data
-    ds_dict = {}
-    for k, bounds in subdomains.items():
-        # for region k, the values in the mock dataset is k
-        min_lon, min_lat, max_lon, max_lat = bounds
-        temp = ds.sel(lat=slice(min_lat, max_lat), lon=slice(min_lon, max_lon))
-        for v in ds.data_vars:
-            temp[v] = xr.DataArray(k, dims=temp[v].dims, coords=temp[v].coords)
-        ds_dict[k] = temp
-
-    # test that the correct combined output is present
-    out = combine_outputs(ds_dict, mask)
-    for k, v in itertools.product(subdomains.keys(), ds.data_vars):
-        unique_val = np.unique(out[v].where(mask == k).values)
-        unique_val = unique_val[np.isfinite(unique_val)]
-        # ensure that there is only one value for the region
-        assert len(unique_val) == 1
-        # ensure that for region k, the value we get is k
-        assert unique_val[0] == k
-
-
-def test_region_28():
-    # region 28 of ar6 spans across the -180 longitude line, thus making the min/max longitude hard to determine
-    # we hard coded the correct boundary in the code and testing it here
-    ds = xr.DataArray(
-        1,
-        dims=['lat', 'lon', 'time'],
-        coords={'lat': [66, 67], 'lon': [50, 51], 'time': np.arange(5)},
-    )
-    buffer_size = 4
-    subdomains, mask = generate_subdomains(
-        ex_output_grid=ds.isel(time=0), buffer_size=buffer_size, region_def='ar6'
-    )
-
-    assert subdomains[28] == (36.0, 61.0, 180, 86.0)