diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 7ea64e1..6570c85 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -16,28 +16,26 @@ jobs: strategy: matrix: os: [ubuntu-latest] + python-version: ['3.9', '3.11'] env: OS: ${{ matrix.os }} - PYTHON: '3.9' + PYTHON: ${{ matrix.python-version }} steps: - uses: actions/checkout@v1 - name: Set up Python - uses: actions/setup-python@master + uses: actions/setup-python@v5 with: - python-version: 3.9 + python-version: ${{ matrix.python-version }} - name: Install dependencies run: | python -m pip install --upgrade pip - pip install click-default-group - pip install . + pip install -e .[dev] - name: Test and generate coverage report on Linux run: | - pip install pytest - pip install pytest-cov pytest --cov=./ --cov-report=xml diff --git a/.github/workflows/paper.yml b/.github/workflows/paper.yml index 80800ce..6f2e8a8 100644 --- a/.github/workflows/paper.yml +++ b/.github/workflows/paper.yml @@ -1,5 +1,13 @@ --- -on: [push] +name: paper + +on: + push: + branches: + - main + pull_request: + branches: + - main jobs: paper: @@ -15,7 +23,7 @@ jobs: # This should be the path to the paper within your repo. paper-path: paper/paper.md - name: Upload - uses: actions/upload-artifact@v1 + uses: actions/upload-artifact@v4 with: name: paper # This is the output path where Pandoc will write the compiled diff --git a/README.md b/README.md index b110684..6f5fc5b 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ Details on methodology and advanced usage are available on the [User Guide](http Install `tethys` using pip: -`pip install tethys-downscale` +`pip install tethys-downscaling` ## Contributing to Tethys diff --git a/docs/source/conf.py b/docs/source/conf.py index b84a011..4051ccf 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -26,7 +26,7 @@ project = 'tethys' copyright = '2021, Battelle Memorial Institute' -author = 'Chris R. Vernon' +author = 'Isaac Thompson, Chris R. Vernon, Hassan Niazi' # The full version, including alpha/beta/rc tags release = version diff --git a/docs/source/getting_started.rst b/docs/source/getting_started.rst index 4d234e0..64a7032 100644 --- a/docs/source/getting_started.rst +++ b/docs/source/getting_started.rst @@ -21,7 +21,7 @@ As a prerequisite, you'll need to have `Python `_ configuration file. The options in this file correspond to the arguments passed to the :ref:`Tethys class `. Options not present in the config file will use the default. An overview is provided in the following table, with more details and examples below. -======================== ========================================================= -Option Description -======================== ========================================================= -:ref:`years` list of years to be included spatial downscaling -:ref:`resolution` resolution in degrees for spatial downscaling -:ref:`demand_type` choice between "withdrawals" (default) or "consumption" -:ref:`perform_temporal` choice between "false" (default) or "true" -:ref:`gcam_db` relative path to a GCAM database -:ref:`csv` relative path to csv file containing inputs -:ref:`output_file` name of file to write outputs to -:ref:`downscaling_rules` mapping from water demand sectors to proxy variables -:ref:`proxy_files` mapping of spatial proxy files to their years/variables -:ref:`map_files` list of files containing region maps -:ref:`temporal_methods` mapping of sector to temporal downscaling method -:ref:`temporal_files` files that will be accessible during temporal downscaling -======================== ========================================================= +======================================= ============================================================================= +Option Description +======================================= ============================================================================= +:ref:`years` list of years to be included spatial downscaling +:ref:`resolution` resolution in degrees for spatial downscaling +:ref:`demand_type` choice between "withdrawals" (default) or "consumption" +:ref:`perform_temporal` choice between "false" (default) or "true" +:ref:`gcam_db` relative path to a GCAM database +:ref:`csv` relative path to csv file containing inputs +:ref:`output_file` name of file to write outputs to +:ref:`downscaling_rules` mapping from water demand sectors to proxy variables +:ref:`proxy_files` mapping of spatial proxy files to their years/variables +:ref:`map_files` list of files containing region maps +:ref:`temporal_methods` mapping of sector to temporal downscaling method +:ref:`temporal_files` files that will be accessible during temporal downscaling +:ref:`source_disaggregation` if true, share of surface water vs groundwater withdrawal will be written out +:ref:`irrigation_conveyance_efficiency` if set, irrigation withdrawal will be divided by this number +======================================= ============================================================================= years @@ -272,6 +274,23 @@ Mapping of files that will be accessible to temporal downscaling methods. CDD: data/temporal/CDD_monthly.nc +source_disaggregation +^^^^^^^^^^^^^^^^^^^^ +If true, share of surface water vs groundwater withdrawal will be written out to a file called gridded_runoff_shares.nc +.. code-block:: yaml + + source_disaggregation: true + + +irrigation_conveyance_efficiency +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +If set, irrigation withdrawal will be divided by this number, representing GCAM's irrigation conveyance efficiency + +.. code-block:: yaml + + irrigation_conveyance_efficiency: 0.829937 + + Generalization -------------- **tethys** was developed with consideration for GCAM's breakdown of water demand, but was designed to be as flexible as possible with support for user-specified downscaling configurations. diff --git a/requirements.txt b/requirements.txt index 57c8033..dd9e8c9 100755 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ PyYAML>=6.0 -gcamreader>=1.2.5 +gcamreader>=1.4.0 numpy>=1.22 pandas>=1.2.4 netCDF4>=1.6 @@ -7,8 +7,6 @@ xarray>=2022.09.0 rioxarray>=0.12.4 tqdm>=4.66.2 matplotlib>=3.8.3 -dask[complete]>=2022.12.1 -bokeh!=3.0.*,>=2.4.2 -geopandas>=0.14.3 -dask_geopandas>=0.3.1 -hvplot>=0.9.2 +dask>=2022.12.1 +distributed>=2022.12.1 +setuptools<81 diff --git a/setup.py b/setup.py index 2ea637e..63f95ee 100644 --- a/setup.py +++ b/setup.py @@ -23,35 +23,37 @@ def get_requirements(): url='https://github.com/JGCRI/tethys', packages=find_packages(), license='BSD-2-Clause', - author='Isaac Thompson, Chris Vernon', + author='Isaac Thompson, Chris Vernon, Hassan Niazi', python_requires='>=3.9, <4', include_package_data=True, install_requires=[ 'PyYAML>=6.0', - 'gcamreader>=1.2.5', + 'gcamreader==1.4.0', 'numpy>=1.22', 'pandas>=1.2.4', 'netCDF4>=1.6', - 'dask[complete]>=2022.12.1', - 'dask_geopandas>=0.3.1', + 'dask==2024.8.0', + 'distributed==2024.8.0', 'xarray>=2022.09.0', 'rioxarray>=0.12.4', 'tqdm>=4.66.2', 'matplotlib>=3.8.3', + 'setuptools<81', 'bokeh!=3.0.*,>=2.4.2', 'geopandas>=0.14.3', + 'dask_geopandas>=0.3.1', 'hvplot>=0.9.2', ], extras_require={ 'dev': [ - 'build~=0.5.1', - 'nbsphinx~=0.8.6', - 'setuptools~=57.0.0', - 'sphinx~=4.0.2', - 'sphinx-panels~=0.6.0', - 'sphinx-rtd-theme~=0.5.2', - 'sphinx-mathjax-offline~=0.0.1', - 'twine>=3.4.1', + 'build==0.10.0', + 'nbsphinx==0.8.12', + 'sphinx==4.5.0', + 'sphinx-panels==0.6.0', + 'sphinx-rtd-theme==0.5.2', + 'sphinx-mathjax-offline==0.0.2', + 'pytest>=8.3.0', + 'pytest-cov>=5.0.0', 'click-default-group>=1.2.4', ] } diff --git a/tethys/__init__.py b/tethys/__init__.py index a88df2d..3fab7a4 100644 --- a/tethys/__init__.py +++ b/tethys/__init__.py @@ -3,4 +3,4 @@ from .utils.spatial import InputViewer -__version__ = "2.1" +__version__ = "2.2.0" diff --git a/tethys/datareader/easy_query.py b/tethys/datareader/easy_query.py index 723578e..68c660b 100644 --- a/tethys/datareader/easy_query.py +++ b/tethys/datareader/easy_query.py @@ -1,7 +1,7 @@ import gcamreader -def easy_query(variable, year_axis=None, **kwargs): +def easy_query(variable, year_axis=None, replace_filters=False, **kwargs): """Build a query for a GCAM database kwargs act as filters on nodes, eg. "sector='Beef'" is converted to the xpath "[@type='sector' and @name='Beef']" @@ -24,8 +24,11 @@ def easy_query(variable, year_axis=None, **kwargs): :return: gcamreader Query object """ - filters = dict(sector=None) # match nodes where @type='sector' by default - filters.update(kwargs) # update the filters with user-provided kwargs + if replace_filters: + filters = kwargs + else: + filters = dict(sector=None) # match nodes where @type='sector' by default + filters.update(kwargs) # update the filters with user-provided kwargs # filters are separated by "//*" to match any descendant xpath = "*" + "//*".join(handle_filter(k, v) for k, v in filters.items()) + f"//{variable}/node()" diff --git a/tethys/datareader/regional.py b/tethys/datareader/regional.py index ea6b9a6..ddd618f 100644 --- a/tethys/datareader/regional.py +++ b/tethys/datareader/regional.py @@ -41,6 +41,19 @@ def load_region_data(gcam_db, sectors, demand_type='withdrawals'): return df +def extract_resource_name(x): + """Removes '_water withdrawals' or '_water consumption' from GCAM sector or resource name. + + :param x: resource name string. + :return: cleaned resource name without suffix. + """ + if x.endswith('_water withdrawals'): + return x.removesuffix('_water withdrawals') + if x.endswith('_water consumption'): + return x.removesuffix('_water consumption') + return x + + def extract_basin_name(x): """Maps 'water_td_irr_basin_C' to '_basin', and water_td_elec_C to ''""" if x.startswith('water_td_irr_'): diff --git a/tethys/model.py b/tethys/model.py index 54f6365..dbaf361 100644 --- a/tethys/model.py +++ b/tethys/model.py @@ -19,6 +19,7 @@ from tethys.datareader.gridded import load_file, interp_helper from tethys.datareader.maps import load_region_map +from tethys.utils.source_disaggregation import get_source_shares class Tethys: """Model wrapper for Tethys""" @@ -30,6 +31,7 @@ def __init__( resolution=0.125, bounds=None, demand_type='withdrawals', + source_disaggregation=None, gcam_db=None, csv=None, output_dir=None, @@ -37,15 +39,19 @@ def __init__( downscaling_rules=None, proxy_files=None, map_files=None, - temporal_config=None + temporal_config=None, + perform_temporal=False, + temporal_methods=None, + temporal_files=None, ): """Parameters can be specified in a YAML file or passed directly, with the config file taking precedence :param config_file: path to YAML configuration file containing these parameters - :param years: list of years to be included spatial downscaling + :param years: list of years to be included for spatial downscaling :param resolution: resolution in degrees for spatial downscaling :param bounds: list [lat_min, lat_max, lon_min, lon_max] to crop to :param demand_type: choice between “withdrawals” (default) or “consumption” + :param source_disaggregation: decide whether to disaggregate water demand by source (runoff, groundwater, desal) :param gcam_db: relative path to a GCAM database :param csv: relative path to csv file containing inputs :param output_dir: directory to write outputs to @@ -53,6 +59,9 @@ def __init__( :param downscaling_rules: mapping from water demand sectors to proxy variables :param proxy_files: mapping of spatial proxy files to their years/variables :param map_files: list of files containing region maps + :param perform_temporal: if True, apply temporal downscaling + :param temporal_methods: legacy mapping of sector to method names in tethys.tdmethods + :param temporal_files: legacy mapping of file aliases used by temporal methods :param temporal_config: mapping of sector to temporal downscaling method and arguments """ self.root = None @@ -62,6 +71,7 @@ def __init__( self.resolution = resolution self.bounds = bounds self.demand_type = demand_type + self.source_disaggregation = source_disaggregation # GCAM database info self.gcam_db = gcam_db @@ -78,6 +88,9 @@ def __init__( self.proxy_files = proxy_files self.map_files = map_files + self.perform_temporal = perform_temporal + self.temporal_methods = temporal_methods + self.temporal_files = temporal_files self.temporal_config = temporal_config # data we'll load or generate later @@ -85,6 +98,11 @@ def __init__( self.proxies = None self.inputs = None self.outputs = None + self.shares = None + self.griddedshares = None + self.disaggregated_sw = None + self.disaggregated_gw = None + self.irrigation_conveyance_efficiency = None # settings in YAML override settings passed directly to __init__ if config_file is not None: @@ -103,6 +121,47 @@ def __init__( self.output_dir = os.path.join(self.root, self.output_dir) self._parse_proxy_files() + self._normalize_temporal_config() + + def _resolve_path(self, value): + if value is None: + return None + return value if os.path.isabs(value) else os.path.join(self.root, value) + + # maps method name → {kwarg_name: temporal_files key} + _TEMPORAL_FILE_MAP = { + 'domestic': {'tasfile': 'tas', 'rfile': 'domr'}, + 'electricity': {'hddfile': 'hdd', 'cddfile': 'cdd'}, + 'irrigation': {'irrfile': 'irr'}, + 'weights': {'weightfile': 'weight'}, + } + + def _normalize_temporal_config(self): + if self.temporal_config is not None: + for cfg in self.temporal_config.values(): + cfg['kwargs'] = {k: self._resolve_path(v) if k.endswith('file') or k == 'gcam_db' else v + for k, v in cfg.get('kwargs', {}).items()} + return + + if not self.perform_temporal and not self.temporal_methods and not self.temporal_files: + return + + files = self.temporal_files or {} + methods = self.temporal_methods or { + 'Municipal': 'domestic', 'Electricity': 'electricity', 'Irrigation': 'irrigation'} + + config = {} + for sector, method in methods.items(): + kwargs = {k: self._resolve_path(files.get(v)) + for k, v in self._TEMPORAL_FILE_MAP.get(method, {}).items()} + if method in ('electricity', 'irrigation') and self.map_files: + kwargs['regionfile'] = self._resolve_path(self.map_files[0]) + if method == 'electricity': + kwargs['gcam_db'] = self._resolve_path(self.gcam_db) + config[sector] = {'method': method, + 'kwargs': {k: v for k, v in kwargs.items() if v is not None}} + + self.temporal_config = config or None def _parse_proxy_files(self): """Handle several shorthand expressions in the proxy catalog""" @@ -198,13 +257,45 @@ def downscale(self, distribution, inputs, region_masks): return out + def disaggregate_source(self): + """Disaggregate water demand by source (runoff, groundwater, desal)""" + + print('Disaggregating Source') + + # initialize source shares object + get_shares = get_source_shares(gcam_db=self.gcam_db, demand_type=self.demand_type, + region_masks=self.region_masks, years=self.years) + + # query and calculate shares + shares_df = get_shares.calculate_shares() + + # apply shares to the gridded region masks + gridded_shares = get_shares.generate_gridded_shares(shares_df) + + return gridded_shares + + def run_model(self): self.outputs = xr.Dataset() + self.griddedshares = xr.Dataset() + self.disaggregated_sw = xr.Dataset() + self.disaggregated_gw = xr.Dataset() self._load_proxies() self._load_region_masks() self._load_inputs() + # disaggregate water supply source + # disaggregate water supply source + if self.source_disaggregation: + gshares = xr.Dataset(self.disaggregate_source()) + + # store the disaggregated results + self.griddedshares = gshares + if self.output_dir is not None: + filename = os.path.join(self.output_dir, f'gridded_runoff_shares.nc') + gshares['runoff'].to_netcdf(filename) + for supersector, rules in self.downscaling_rules.items(): print(f'Downscaling {supersector}') @@ -244,6 +335,10 @@ def run_model(self): # in a lot of cases this could be optimized by solving the intersections at region scale first, # then downscaling once, but harder to implement, especially if differing regions are not subsets + # handle irrigation withdrawal conveyance efficiency if set + if (supersector=='Irrigation') and (self.demand_type=='withdrawals') and (self.irrigation_conveyance_efficiency is not None): + downscaled = downscaled / self.irrigation_conveyance_efficiency + # write spatial downscaling outputs if self.output_dir is not None: filename = os.path.join(self.output_dir, f'{supersector}_{self.demand_type}.nc') @@ -261,7 +356,7 @@ def run_model(self): years = range(self.years[0], self.years[-1] + 1) kwargs = self.temporal_config[supersector]['kwargs'] distribution = temporal_distribution(years=years, resolution=self.resolution, - bounds=self.bounds, **kwargs) + bounds=self.bounds, **kwargs) else: # fall back to uniform distribution distribution = xr.DataArray(np.full(12, 1/12, np.float32), coords=dict(month=range(1, 13))) @@ -272,7 +367,7 @@ def run_model(self): if self.output_dir is not None: filename = os.path.join(self.output_dir, f'{supersector}_{self.demand_type}_monthly.nc') encoding = {sector: {'zlib': True, 'complevel': 5} for sector in downscaled.sector.data} - downscaled.to_dataset(dim='sector').to_netcdf(filename, encoding=encoding) + downscaled.to_dataset(dim='sector').compute().to_netcdf(filename, encoding=encoding) downscaled = xr.open_dataset(filename).to_array(dim='sector') # hopefully this keeps dask happy self.outputs.update(downscaled.to_dataset(dim='sector')) diff --git a/tethys/tdmethods/domestic.py b/tethys/tdmethods/domestic.py index 443683b..01d7d8b 100644 --- a/tethys/tdmethods/domestic.py +++ b/tethys/tdmethods/domestic.py @@ -2,9 +2,15 @@ from tethys.datareader.gridded import load_file -def temporal_distribution(years, resolution, tasfile, rfile, tasvar='tas', rvar='amplitude', bounds=None): +def temporal_distribution(years, resolution=None, tasfile=None, rfile=None, tasvar='tas', rvar='amplitude', bounds=None): """Temporal downscaling for domestic water demand using algorithm from Wada et al. (2011)""" + if hasattr(years, 'temporal_config'): + model = years + cfg = dict((model.temporal_config or {}).get('Municipal', {}).get('kwargs', {}) or {}) + cfg.setdefault('bounds', model.bounds) + return temporal_distribution(range(model.years[0], model.years[-1] + 1), model.resolution, **cfg) + tas = load_file(tasfile, resolution, years, bounds=bounds, regrid_method='intensive', variables=[tasvar])[tasvar] amplitude = load_file(rfile, resolution, years, bounds=bounds, regrid_method='label', variables=[rvar])[rvar] diff --git a/tethys/tdmethods/electricity.py b/tethys/tdmethods/electricity.py index e8aedcb..4a92d0d 100644 --- a/tethys/tdmethods/electricity.py +++ b/tethys/tdmethods/electricity.py @@ -6,10 +6,16 @@ from tethys.datareader.maps import load_region_map -def temporal_distribution(years, resolution, hddfile, cddfile, regionfile, gcam_db, hddvar='hdd', cddvar='cdd', - bounds=None): +def temporal_distribution(years, resolution=None, hddfile=None, cddfile=None, regionfile=None, gcam_db=None, + hddvar='hdd', cddvar='cdd', bounds=None): """Temporal downscaling of water demand for electricity generation using algorithm from Voisin et al. (2013)""" + if hasattr(years, 'temporal_config'): + model = years + cfg = dict((model.temporal_config or {}).get('Electricity', {}).get('kwargs', {}) or {}) + cfg.setdefault('bounds', model.bounds) + return temporal_distribution(range(model.years[0], model.years[-1] + 1), model.resolution, **cfg) + # get weights of heating/cooling/other by location and time regions = load_region_map(regionfile, masks=True, target_resolution=resolution, bounds=bounds) weights = elec_sector_weights(gcam_db) diff --git a/tethys/tdmethods/irrigation.py b/tethys/tdmethods/irrigation.py index 4915bad..59781fd 100644 --- a/tethys/tdmethods/irrigation.py +++ b/tethys/tdmethods/irrigation.py @@ -2,9 +2,15 @@ from tethys.datareader.maps import load_region_map -def temporal_distribution(years, resolution, regionfile, irrfile, irrvar='pirrww', bounds=None): +def temporal_distribution(years, resolution=None, regionfile=None, irrfile=None, irrvar='pirrww', bounds=None): """Temporal downscaling of irrigation water demand""" + if hasattr(years, 'temporal_config'): + model = years + cfg = dict((model.temporal_config or {}).get('Irrigation', {}).get('kwargs', {}) or {}) + cfg.setdefault('bounds', model.bounds) + return temporal_distribution(range(model.years[0], model.years[-1] + 1), model.resolution, **cfg) + irr = load_file(irrfile, resolution, years, bounds=bounds, regrid_method='label', variables=[irrvar])[irrvar] region_masks = load_region_map(regionfile, masks=True, target_resolution=resolution, bounds=bounds) diff --git a/tethys/tdmethods/weights.py b/tethys/tdmethods/weights.py index bc65799..4af7e33 100644 --- a/tethys/tdmethods/weights.py +++ b/tethys/tdmethods/weights.py @@ -1,10 +1,16 @@ from tethys.datareader.gridded import load_file -def temporal_distribution(years, resolution, weightfile, weightvar='weight', regrid_method='intensive', +def temporal_distribution(years, resolution=None, weightfile=None, weightvar='weight', regrid_method='intensive', prenormalized=False, bounds=None): """Load a monthly distribution from filename""" + if hasattr(years, 'temporal_config'): + model = years + cfg = dict((model.temporal_config or {}).get('Weights', {}).get('kwargs', {}) or {}) + cfg.setdefault('bounds', model.bounds) + return temporal_distribution(range(model.years[0], model.years[-1] + 1), model.resolution, **cfg) + distribution = load_file(weightfile, resolution, years, bounds=bounds, regrid_method=regrid_method, variables=[weightvar])[weightvar] if prenormalized is False: diff --git a/tethys/utils/install_supplement.py b/tethys/utils/install_supplement.py index 793d191..d7aef66 100644 --- a/tethys/utils/install_supplement.py +++ b/tethys/utils/install_supplement.py @@ -17,6 +17,8 @@ '2.0.0': 'https://zenodo.org/record/7569652/files/example.zip?download=1', '2.0.1': 'https://zenodo.org/record/7569652/files/example.zip?download=1', '2.0.2': 'https://zenodo.org/record/7569652/files/example.zip?download=1', + '2.1.0': 'https://zenodo.org/record/7569652/files/example.zip?download=1', + '2.2.0': 'https://zenodo.org/record/7569652/files/example.zip?download=1' } @@ -47,7 +49,7 @@ def get_example_data( example_data_directory = default_download_dir # get the current version of tethys that is installed - current_version = importlib.metadata.version('tethys') + current_version = importlib.metadata.version('tethys-downscaling') try: url = DATA_VERSION_URLS[current_version] diff --git a/tethys/utils/region_to_id_mapping.py b/tethys/utils/region_to_id_mapping.py index 3992f56..1457917 100644 --- a/tethys/utils/region_to_id_mapping.py +++ b/tethys/utils/region_to_id_mapping.py @@ -507,5 +507,479 @@ 'Papua New Guinea Coast': 173, 'Tasmania': 211, 'Solomon Islands': 176 + }, + "basin_name_mapping": { + 'Arctic Ocean Islands': 'ArcticIsl', + 'Northwest Territories': 'NWTerr', + 'Siberia North Coast': 'SiberiaN', + 'Siberia West Coast': 'SiberiaW', + 'Kara Sea Coast': 'KaraSea', + 'Lena': 'LenaR', + 'Pacific and Arctic Coast': 'PacArctic', + 'Scandinavia North Coast': 'ScndnvN', + 'Russia Barents Sea Coast': 'BarentsSea', + 'Mackenzie': 'Mackenzie', + 'Iceland': 'Iceland', + 'Sweden': 'Sweden', + 'Finland': 'Finland', + 'Northern Dvina': 'DvinaRN', + 'Hudson Bay Coast': 'HudsonBay', + 'Scotland': 'Scotland', + 'Neva': 'NevaR', + 'Volga': 'VolgaR', + 'Atlantic Ocean Seaboard': 'CanAtl', + 'Baltic Sea Coast': 'BalticSea', + 'Denmark Germany Coast': 'DnkGrmCst', + 'Narva': 'NarvaR', + 'Saskatchewan Nelson': 'NelsonR', + 'Ireland': 'Ireland', + 'Daugava': 'DaugavaR', + 'England and Wales': 'EngWales', + 'Fraser': 'FraserR', + 'Ems Weser': 'EmsWeserR', + 'Oder': 'OderR', + 'Wisla': 'WislaR', + 'Elbe': 'ElbeR', + 'Rhine': 'RhineR', + 'Poland Coast': 'PolandCst', + 'Churchill': 'ChurchillR', + 'Neman': 'NemanR', + 'Scheldt': 'ScheldtR', + 'Russia South East Coast': 'RusCstSE', + 'Ural': 'UralR', + 'Dnieper': 'DnieperR', + 'St Lawrence': 'StLwrncR', + 'France West Coast': 'FranceCstW', + 'Gobi Interior': 'Gobi', + 'Amur': 'AmurR', + 'Loire': 'LoireR', + 'Caspian Sea Coast': 'CaspianNE', + 'Seine': 'SeineR', + 'Black Sea North Coast': 'BlackSeaN', + 'Yenisei': 'YeniseiR', + 'Dniester': 'DniesterR', + 'Italy East Coast': 'ItalyCstE', + 'Japan': 'Japan', + 'Caspian Sea East Coast': 'CaspianE', + 'Don': 'DonR', + 'Danube': 'DanubeR', + 'Adriatic Sea Greece Black Sea Coast': 'AdrBlkSea', + 'Ob': 'ObR', + 'Po': 'PoR', + 'Amu Darya': 'AmuDaryaR', + 'Italy West Coast': 'ItalyCstW', + 'Spain Portugal Atlantic Coast': 'IberiaCst', + 'France South Coast': 'FranceCstS', + 'Rhone': 'RhoneR', + 'Mediterranean Sea Islands': 'MeditIsl', + 'Gironde': 'Gironde', + 'North and South Korea': 'Korea', + 'Bo Hai Korean Bay North Coast': 'BoHai', + 'Spain South and East Coast': 'SpainCstSE', + 'Lake Balkash': 'LBalkash', + 'Tiber': 'TiberR', + 'Black Sea South Coast': 'BlackSeaS', + 'Tagus': 'TagusR', + 'Caspian Sea South West Coast': 'CaspianSW', + 'Ebro': 'EbroR', + 'Douro': 'DouroR', + 'Mediterranean Sea East Coast': 'MeditE', + 'Syr Darya': 'SyrDaryaR', + 'Ziya He Interior': 'ZiyaHe', + 'China Coast': 'ChinaCst', + 'Huang He': 'HuangHeR', + 'Mediterranean South Coast': 'MeditS', + 'Guadiana': 'GuadianaR', + 'Central Iran': 'Iran', + 'Guadalquivir': 'GuadalqR', + 'Tigris Euphrates': 'TigrEuphR', + 'Tarim Interior': 'Tarim', + 'Africa North West Coast': 'AfrCstNW', + 'Nile': 'NileR', + 'Persian Gulf Coast': 'PersianGulf', + 'Indus': 'IndusR', + 'Farahrud': 'FarahrudR', + 'Baja California': 'MexBaja', + 'Plateau of Tibet Interior': 'Tibet', + 'Red Sea East Coast': 'RedSeaE', + 'Arabian Peninsula': 'ArabianP', + 'Dead Sea': 'DeadSea', + 'Mexico Northwest Coast': 'MexCstNW', + 'Helmand': 'Helmand', + 'Sinai Peninsula': 'SinaiP', + 'Eastern Jordan Syria': 'EJrdnSyr', + 'Africa Red Sea Gulf of Aden Coast': 'AfrCstNE', + 'Caribbean': 'Caribbean', + 'HamuniMashkel': 'HamuMashR', + 'Taiwan': 'Taiwan', + 'Arabian Sea Coast': 'ArabianSea', + 'North Gulf': 'MexGulf', + 'Yangtze': 'Yangtze', + 'Sabarmati': 'SabarmatiR', + 'Xun Jiang': 'XunJiang', + 'Hong (Red River)': 'Hong', + 'Ganges Bramaputra': 'GangesR', + 'Yucatan Peninsula': 'YucatanP', + 'South China Sea Coast': 'SChinaSea', + 'Mahi': 'MahiR', + 'Mexico Interior': 'MexInt', + 'Pacific Central Coast': 'MexCstW', + 'Bay of Bengal North East Coast': 'BengalBay', + 'Tapti': 'TaptiR', + 'Yasai': 'BengalW', + 'Philippines': 'Phlppns', + 'Brahmani': 'BrahmaniR', + 'North Marina Islands and Guam': 'Guam', + 'Mahanadi': 'MahanadiR', + 'Godavari': 'GodavariR', + 'Hainan': 'Hainan', + 'Mekong': 'Mekong', + 'Viet Nam Coast': 'VietnamCst', + 'Salween': 'Salween', + 'India North East Coast': 'IndCstNE', + 'India West Coast': 'IndCstW', + 'Papaloapan': 'Papaloapan', + 'Rio Lerma': 'RioLerma', + 'Rio Verde': 'RioVerde', + 'Grijalva Usumacinta': 'GrijUsuR', + 'Rio Balsas': 'RioBalsas', + 'Southern Central America': 'CntAmer', + 'Isthmus of Tehuantepec': 'Tehuantpc', + 'Irrawaddy': 'IrrawaddyR', + 'Sittaung': 'SittaungR', + 'Peninsula Malaysia': 'MalaysiaP', + 'Krishna': 'KrishnaR', + 'Andaman Nicobar Islands': 'AdnNicIsl', + 'Africa West Coast': 'AfrCstW', + 'Caribbean Coast': 'SAmerCstN', + 'Africa North Interior': 'AfrIntN', + 'India East Coast': 'IndCstE', + 'Chao Phraya': 'ChaoPhrR', + 'Pennar': 'PennarR', + 'Gulf of Thailand Coast': 'ThaiGulf', + 'Niger': 'NigerR', + 'Micronesia': 'Micronesia', + 'Lake Chad': 'LChad', + 'Senegal': 'SenegalR', + 'Cauvery': 'CauveryR', + 'Sri Lanka': 'SriLanka', + 'India South Coast': 'IndCstS', + 'Orinoco': 'OrinocoR', + 'Colombia Ecuador Pacific Coast': 'ColEcuaCst', + 'Palau and East Indonesia': 'IdnE', + 'North Borneo Coast': 'BorneoCstN', + 'Volta': 'VoltaR', + 'Northeast South America South Atlantic Coast': 'SAmerCstNE', + 'Gulf of Guinea': 'GuineaGulf', + 'Sumatra': 'Sumatra', + 'Sulawesi': 'Sulawesi', + 'Kalimantan': 'Kalimantan', + 'Magdalena': 'MagdalenaR', + 'Irian Jaya Coast': 'IrianJaya', + 'Amazon': 'AmazonR', + 'South Chile Pacific Coast': 'ChileCstS', + 'Shebelli Juba': 'ShebJubR', + 'Africa East Central Coast': 'AfrCstE', + 'North Brazil South Atlantic Coast': 'BrzCstN', + 'Papua New Guinea Coast': 'PapuaCst', + 'Tocantins': 'TocantinsR', + 'Java Timor': 'JavaTimor', + 'Solomon Islands': 'SolomonIsl', + 'Madagascar': 'Madagascar', + 'Sepik': 'SepikR', + 'Rift Valley': 'RiftValley', + 'Peru Pacific Coast': 'PeruCst', + 'Fly': 'FlyR', + 'Angola Coast': 'AngolaCst', + 'Congo': 'CongoR', + 'Australia North Coast': 'AusCstN', + 'South Pacific Islands': 'NewCaledn', + 'East Brazil South Atlantic Coast': 'BrzCstE', + 'Parnaiba': 'ParnaibaR', + 'Zambezi': 'ZambeziR', + 'Australia East Coast': 'AusCstE', + 'Africa Indian Ocean Coast': 'AfrCstSE', + 'Australia West Coast': 'AusCstW', + 'Sao Francisco': 'SaoFrancR', + 'Australia Interior': 'AusInt', + 'Orange': 'OrangeR', + 'Uruguay Brazil South Atlantic Coast': 'BrzCstS', + 'Namibia Coast': 'AfrCstSW', + 'Africa South Interior': 'AfrIntS', + 'South Africa South Coast': 'AfrCstS', + 'Limpopo': 'LimpopoR', + 'La Puna Region': 'LaPuna', + 'New Zealand': 'NewZealand', + 'Australia South Coast': 'AusCstS', + 'Mar Chiquita': 'MarChiq', + 'South Africa West Coast': 'AfrCstSSW', + 'Salinas Grandes': 'Salinas', + 'La Plata': 'RioLaPlata', + 'North Chile Pacific Coast': 'ChileCstN', + 'Murray Darling': 'MurrayDrlg', + 'Pampas Region': 'Pampas', + 'North Argentina South Atlantic Coast': 'ArgCstN', + 'Tasmania': 'Tasmania', + 'South America Colorado': 'ArgColoR', + 'Negro': 'NegroR', + 'Central Patagonia Highlands': 'Patagonia', + 'South Argentina South Atlantic Coast': 'ArgCstS', + 'Antarctica': 'Antarctica', + 'California River Basin': 'California', + 'Upper Mississippi Basin': 'MissppRN', + 'Lower Mississippi River Basin': 'MissppRS', + 'Upper Colorado River Basin': 'UsaColoRN', + 'Lower Colorado River Basin': 'UsaColoRS', + 'Great Basin': 'GreatBasin', + 'Missouri River Basin': 'MissouriR', + 'Arkansas White Red Basin': 'ArkWhtRedR', + 'Texas Gulf Coast Basin': 'TexasCst', + 'South Atlantic Gulf Basin': 'UsaCstSE', + 'Great Lakes Basin': 'GreatLakes', + 'Ohio River Basin': 'OhioR', + 'Pacific Northwest Basin': 'UsaPacNW', + 'Tennessee River Basin': 'TennR', + 'Rio Grande River Basin': 'RioGrande', + 'New England Basin': 'UsaCstNE', + 'Mid Atlantic Basin': 'UsaCstE', + 'Hawaii': 'Hawaii', + 'Narmada': 'NarmadaR' + }, + "basin_name_mapping_im3": { + 'Arctic Ocean Islands': 'ArcticIsl', + 'Northwest Territories': 'NWTerr', + 'Siberia North Coast': 'SiberiaN', + 'Siberia West Coast': 'SiberiaW', + 'Kara Sea Coast': 'KaraSea', + 'Lena': 'LenaR', + 'Pacific and Arctic Coast': 'PacArctic', + 'Scandinavia North Coast': 'ScndnvN', + 'Russia Barents Sea Coast': 'BarentsSea', + 'Mackenzie': 'Mackenzie', + 'Iceland': 'Iceland', + 'Sweden': 'Sweden', + 'Finland': 'Finland', + 'Northern Dvina': 'DvinaRN', + 'Hudson Bay Coast': 'HudsonBay', + 'Scotland': 'Scotland', + 'Neva': 'NevaR', + 'Volga': 'VolgaR', + 'Atlantic Ocean Seaboard': 'CanAtl', + 'Baltic Sea Coast': 'BalticSea', + 'Denmark Germany Coast': 'DnkGrmCst', + 'Narva': 'NarvaR', + 'Saskatchewan Nelson': 'NelsonR', + 'Ireland': 'Ireland', + 'Daugava': 'DaugavaR', + 'England and Wales': 'EngWales', + 'Fraser': 'FraserR', + 'Ems Weser': 'EmsWeserR', + 'Oder': 'OderR', + 'Wisla': 'WislaR', + 'Elbe': 'ElbeR', + 'Rhine': 'RhineR', + 'Poland Coast': 'PolandCst', + 'Churchill': 'ChurchillR', + 'Neman': 'NemanR', + 'Scheldt': 'ScheldtR', + 'Russia South East Coast': 'RusCstSE', + 'Ural': 'UralR', + 'Dnieper': 'DnieperR', + 'St Lawrence': 'StLwrncR', + 'France West Coast': 'FranceCstW', + 'Gobi Interior': 'Gobi', + 'Amur': 'AmurR', + 'Loire': 'LoireR', + 'Caspian Sea Coast': 'CaspianNE', + 'Seine': 'SeineR', + 'Black Sea North Coast': 'BlackSeaN', + 'Yenisey': 'YeniseiR', + 'Dniester': 'DniesterR', + 'Italy East Coast': 'ItalyCstE', + 'Japan': 'Japan', + 'Caspian Sea East Coast': 'CaspianE', + 'Don': 'DonR', + 'Danube': 'DanubeR', + 'Adriatic Sea Greece Black Sea Coast': 'AdrBlkSea', + 'Ob': 'ObR', + 'Po': 'PoR', + 'Amu Darya': 'AmuDaryaR', + 'Italy West Coast': 'ItalyCstW', + 'Spain Portugal Atlantic Coast': 'IberiaCst', + 'France South Coast': 'FranceCstS', + 'Rhone': 'RhoneR', + 'Mediterranean Sea Islands': 'MeditIsl', + 'Gironde': 'Gironde', + 'North and South Korea': 'Korea', + 'Bo Hai Korean Bay North Coast': 'BoHai', + 'Spain South and East Coast': 'SpainCstSE', + 'Lake Balkash': 'LBalkash', + 'Tiber': 'TiberR', + 'Black Sea South Coast': 'BlackSeaS', + 'Tagus': 'TagusR', + 'Caspian Sea South West Coast': 'CaspianSW', + 'Ebro': 'EbroR', + 'Douro': 'DouroR', + 'Mediterranean Sea East Coast': 'MeditE', + 'Syr Darya': 'SyrDaryaR', + 'Ziya He Interior': 'ZiyaHe', + 'China Coast': 'ChinaCst', + 'Huang He': 'HuangHeR', + 'Mediterranean South Coast': 'MeditS', + 'Guadiana': 'GuadianaR', + 'Central Iran': 'Iran', + 'Guadalquivir': 'GuadalqR', + 'Tigris Euphrates': 'TigrEuphR', + 'Tarim Interior': 'Tarim', + 'Africa North West Coast': 'AfrCstNW', + 'Nile': 'NileR', + 'Persian Gulf Coast': 'PersianGulf', + 'Indus': 'IndusR', + 'Farahrud': 'FarahrudR', + 'Baja California': 'MexBaja', + 'Plateau of Tibet Interior': 'Tibet', + 'Red Sea East Coast': 'RedSeaE', + 'Arabian Peninsula': 'ArabianP', + 'Dead Sea': 'DeadSea', + 'Mexico Northwest Coast': 'MexCstNW', + 'Helmand': 'Helmand', + 'Sinai Peninsula': 'SinaiP', + 'Eastern Jordan Syria': 'EJrdnSyr', + 'Africa Red Sea Gulf of Aden Coast': 'AfrCstNE', + 'Caribbean': 'Caribbean', + 'Hamun i Mashkel': 'HamuMashR', + 'Taiwan': 'Taiwan', + 'Arabian Sea Coast': 'ArabianSea', + 'North Gulf': 'MexGulf', + 'Yangtze': 'Yangtze', + 'Sabarmati': 'SabarmatiR', + 'Xun Jiang': 'XunJiang', + 'Hong Red River': 'Hong', + 'Ganges Bramaputra': 'GangesR', + 'Yucatan Peninsula': 'YucatanP', + 'South China Sea Coast': 'SChinaSea', + 'Mahi': 'MahiR', + 'Mexico Interior': 'MexInt', + 'Pacific Central Coast': 'MexCstW', + 'Bay of Bengal North East Coast': 'BengalBay', + 'Tapti': 'TaptiR', + 'Yasai': 'BengalW', + 'Philippines': 'Phlppns', + 'Brahamani': 'BrahmaniR', + 'North Marina Islands and Guam': 'Guam', + 'Mahandi': 'MahanadiR', + 'Godavari': 'GodavariR', + 'Hainan': 'Hainan', + 'Mekong': 'Mekong', + 'Viet Nam Coast': 'VietnamCst', + 'Salween': 'Salween', + 'India North East Coast': 'IndCstNE', + 'India West Coast': 'IndCstW', + 'Papaloapan': 'Papaloapan', + 'Rio Lerma': 'RioLerma', + 'Rio Verde': 'RioVerde', + 'Grijalva Usumacinta': 'GrijUsuR', + 'Rio Balsas': 'RioBalsas', + 'Southern Central America': 'CntAmer', + 'Isthmus of Tehuantepec': 'Tehuantpc', + 'Irrawaddy': 'IrrawaddyR', + 'Sittang': 'SittaungR', + 'Peninsula Malaysia': 'MalaysiaP', + 'Krishna': 'KrishnaR', + 'Andaman Nicobar Islands': 'AdnNicIsl', + 'Africa West Coast': 'AfrCstW', + 'Caribbean Coast': 'SAmerCstN', + 'Africa North Interior': 'AfrIntN', + 'India East Coast': 'IndCstE', + 'Chao Phraya': 'ChaoPhrR', + 'Pennar': 'PennarR', + 'Gulf of Thailand Coast': 'ThaiGulf', + 'Niger': 'NigerR', + 'Micronesia': 'Micronesia', + 'Lake Chad': 'LChad', + 'Senegal': 'SenegalR', + 'Cauvery': 'CauveryR', + 'Sri Lanka': 'SriLanka', + 'India South Coast': 'IndCstS', + 'Orinoco': 'OrinocoR', + 'Colombia Ecuador Pacific Coast': 'ColEcuaCst', + 'Palau and East Indonesia': 'IdnE', + 'North Borneo Coast': 'BorneoCstN', + 'Volta': 'VoltaR', + 'Northeast South America South Atlantic Coast': 'SAmerCstNE', + 'Gulf of Guinea': 'GuineaGulf', + 'Sumatra': 'Sumatra', + 'Sulawesi': 'Sulawesi', + 'Kalimantan': 'Kalimantan', + 'Magdalena': 'MagdalenaR', + 'Irian Jaya Coast': 'IrianJaya', + 'Amazon': 'AmazonR', + 'South Chile Pacific Coast': 'ChileCstS', + 'Shebelli Juba': 'ShebJubR', + 'Africa East Central Coast': 'AfrCstE', + 'North Brazil South Atlantic Coast': 'BrzCstN', + 'Papua New Guinea Coast': 'PapuaCst', + 'Tocantins': 'TocantinsR', + 'Java Timor': 'JavaTimor', + 'Solomon Islands': 'SolomonIsl', + 'Madasgacar': 'Madagascar', + 'Sepik': 'SepikR', + 'Rift Valley': 'RiftValley', + 'Peru Pacific Coast': 'PeruCst', + 'Fly': 'FlyR', + 'Angola Coast': 'AngolaCst', + 'Congo': 'CongoR', + 'Australia North Coast': 'AusCstN', + 'South Pacific Islands': 'NewCaledn', + 'East Brazil South Atlantic Coast': 'BrzCstE', + 'Parnaiba': 'ParnaibaR', + 'Zambezi': 'ZambeziR', + 'Australia East Coast': 'AusCstE', + 'Africa Indian Ocean Coast': 'AfrCstSE', + 'Australia West Coast': 'AusCstW', + 'Sao Francisco': 'SaoFrancR', + 'Australia Interior': 'AusInt', + 'Orange': 'OrangeR', + 'Uruguay Brazil South Atlantic Coast': 'BrzCstS', + 'Namibia Coast': 'AfrCstSW', + 'Africa South Interior': 'AfrIntS', + 'South Africa South Coast': 'AfrCstS', + 'Limpopo': 'LimpopoR', + 'La Puna Region': 'LaPuna', + 'New Zealand': 'NewZealand', + 'Australia South Coast': 'AusCstS', + 'Mar Chiquita': 'MarChiq', + 'South Africa West Coast': 'AfrCstSSW', + 'Salinas Grandes': 'Salinas', + 'La Plata': 'RioLaPlata', + 'North Chile Pacific Coast': 'ChileCstN', + 'Murray Darling': 'MurrayDrlg', + 'Pampas Region': 'Pampas', + 'North Argentina South Atlantic Coast': 'ArgCstN', + 'Tasmania': 'Tasmania', + 'South America Colorado': 'ArgColoR', + 'Negro': 'NegroR', + 'Central Patagonia Highlands': 'Patagonia', + 'South Argentina South Atlantic Coast': 'ArgCstS', + 'Antarctica': 'Antarctica', + 'California River': 'California', + 'Upper Mississippi': 'MissppRN', + 'Lower Mississippi River': 'MissppRS', + 'Upper Colorado River': 'UsaColoRN', + 'Lower Colorado River': 'UsaColoRS', + 'Great': 'GreatBasin', + 'Missouri River': 'MissouriR', + 'Arkansas White Red': 'ArkWhtRedR', + 'Texas Gulf Coast': 'TexasCst', + 'South Atlantic Gulf': 'UsaCstSE', + 'Great Lakes': 'GreatLakes', + 'Ohio River': 'OhioR', + 'Pacific Northwest': 'UsaPacNW', + 'Tennessee River': 'TennR', + 'Rio Grande River': 'RioGrande', + 'New England': 'UsaCstNE', + 'Mid Atlantic': 'UsaCstE', + 'Hawaii': 'Hawaii', + 'Narmada': 'NarmadaR' } } diff --git a/tethys/utils/source_disaggregation.py b/tethys/utils/source_disaggregation.py new file mode 100644 index 0000000..cb0976e --- /dev/null +++ b/tethys/utils/source_disaggregation.py @@ -0,0 +1,110 @@ +import os +import pandas as pd +import xarray as xr + +import gcamreader + +from tethys.datareader.easy_query import easy_query +from tethys.datareader.regional import extract_resource_name +from tethys.utils.region_to_id_mapping import name_to_id_mapping + + +class get_source_shares: + def __init__(self, gcam_db, basin_name_mapping="basin_name_mapping_im3", demand_type='withdrawals', region_masks=None, years=None): + """ + Initialize get_source_shares with parameters for GCAM database and basin mapping. + + :param gcam_db: Path to the GCAM database file. + :param basin_name_mapping: Basin name mapping dictionary, can change to other mapping in name_to_id_mapping. + :param demand_type: Type of demand, either 'withdrawals' or 'consumption'. + :param region_masks: xarray DataArray containing region masks. + :param years: List of years to be included in the shares. + """ + if region_masks is None: + raise ValueError("region_masks is required to compute gridded shares") + if years is None: + raise ValueError("years is required to compute gridded shares") + + dbpath, dbfile = os.path.split(gcam_db) + self.conn = gcamreader.LocalDBConn(dbpath, dbfile) + self.basin_name_mapping = basin_name_mapping + self.demand_type = demand_type + self.region_masks = region_masks + self.years = list(years) + + def calculate_shares(self): + """ + Query the GCAM database, process water data for the specified demand type, + and calculate runoff and groundwater shares by region and year. + + :return: Processed DataFrame with calculated shares. + """ + # query GCAM database for water used by source for the configured demand type + if self.demand_type not in ('withdrawals', 'consumption'): + raise ValueError(f"Unsupported demand_type: {self.demand_type!r}") + query_type = f"*_water {self.demand_type}" + shares_df = self.conn.runQuery(easy_query('production', replace_filters=True, resource=query_type)) + # extract and clean resource names (e.g., remove '_water withdrawals') + shares_df['resource'] = shares_df['resource'].apply(extract_resource_name) + + # rest of the mappings should be fine, old GCAM versions have typos in basin names + if self.basin_name_mapping == "basin_name_mapping_im3": + shares_df['resource'] = shares_df['resource'].str.replace('_', ' ').str.replace('-', ' ') # replace '_' and '-' with spaces + + # map resource names to short basin names and concatenate region and resource to work with region_masks + shares_df['resourcemap'] = shares_df['resource'].map(name_to_id_mapping[self.basin_name_mapping]) + shares_df['region_resourcemap'] = shares_df['region'] + '_' + shares_df['resourcemap'] + + # calculate shares of runoff and groundwater + shares_df['share'] = shares_df.groupby( + ['scenario', 'region', 'region_resourcemap', 'year'] + )['value'].transform(lambda x: x / x.sum() if x.sum() != 0 else 0) + + # capture basins that are mostly belonging to other countries + if self.basin_name_mapping == 'basin_name_mapping_im3': + shares_df = shares_df.replace({'region_resourcemap': { + 'Canada_FraserR': 'USA_FraserR', + 'Canada_GreatLakes': 'USA_GreatLakes', + 'Mexico_MexCstNW': 'USA_MexCstNW', + 'Canada_NelsonR': 'USA_NelsonR', + }}) + + # filter shares based on the region masks from the config file, otherwise all regions would be included + shares_df = shares_df[shares_df['region_resourcemap'].isin(self.region_masks.region.values)] + + return shares_df + + def generate_gridded_shares(self, shares_df): + """ + Generate gridded shares by subresource (e.g., runoff, groundwater). + + :param shares_df: DataFrame containing shares calculated by `calculate_shares`. + :return: Dictionary of gridded shares by subresource. + """ + gridded_shares_by_subresource = {} + for subresource in shares_df.subresource.unique(): + gridded_shares_by_year = [] + for year in self.years: + gridded_shares = None + for basin in shares_df.region_resourcemap.unique(): + sel = shares_df[ + (shares_df['year'] == year) & + (shares_df['region_resourcemap'] == basin) & + (shares_df['subresource'] == subresource) + ] + share_val = 0.0 if sel.empty else float(sel.iloc[0].share) + gridded_shares = xr.where( + self.region_masks.sel(region=basin).load(), + share_val, + 0 if gridded_shares is None else gridded_shares, + keep_attrs=False, + ) + gridded_shares_by_year.append(gridded_shares) + + # Stack years for the subresource + gridded_shares_by_subresource[subresource] = xr.concat( + gridded_shares_by_year, + dim=pd.Index(self.years, name="year"), + ).rename('share') + + return gridded_shares_by_subresource