From 2a614788236a94614fbeee68b3230366a75a7821 Mon Sep 17 00:00:00 2001 From: Tom Bland Date: Thu, 19 Dec 2024 09:15:29 +0000 Subject: [PATCH 1/6] Select technology parameters for the current year --- src/muse/carbon_budget.py | 4 ++-- src/muse/constraints.py | 2 +- src/muse/demand_share.py | 2 +- src/muse/investments.py | 4 +--- src/muse/sectors/preset_sector.py | 27 --------------------------- src/muse/sectors/sector.py | 14 ++++---------- 6 files changed, 9 insertions(+), 44 deletions(-) diff --git a/src/muse/carbon_budget.py b/src/muse/carbon_budget.py index e3d032241..eb6ce522e 100644 --- a/src/muse/carbon_budget.py +++ b/src/muse/carbon_budget.py @@ -101,7 +101,7 @@ def fitting( Returns: new_price: adjusted carbon price to meet budget """ - # Calculate the carbon price and emissions threshold in the forecast year + # Calculate the carbon price and emissions threshold in the investment year future = market.year[-1] threshold = carbon_budget.sel(year=future).values.item() price = market.prices.sel(year=future, commodity=commodities).mean().values.item() @@ -332,7 +332,7 @@ def bisection( # Create cache for emissions at different price points emissions_cache = EmissionsCache(market, equilibrium, commodities) - # Carbon price and emissions threshold in the forecast year + # Carbon price and emissions threshold in the investment year future = market.year[-1] target = carbon_budget.sel(year=future).values.item() price = market.prices.sel(year=future, commodity=commodities).mean().values.item() diff --git a/src/muse/constraints.py b/src/muse/constraints.py index 4afd92e44..20113aa19 100644 --- a/src/muse/constraints.py +++ b/src/muse/constraints.py @@ -475,7 +475,7 @@ def max_production( replacement = replacement.drop_vars( [u for u in replacement.coords if u not in replacement.dims] ) - kwargs = dict(technology=replacement, year=year, commodity=commodities) + kwargs = dict(technology=replacement, commodity=commodities) if "region" in search_space.coords and "region" in technologies.dims: kwargs["region"] = search_space.region techs = ( diff --git a/src/muse/demand_share.py b/src/muse/demand_share.py index 9171a816f..1f72e82ce 100644 --- a/src/muse/demand_share.py +++ b/src/muse/demand_share.py @@ -112,7 +112,7 @@ def demand_share( ) check_dimensions( technologies, - ["technology", "year", "region"], + ["technology", "region"], optional=["timeslice", "commodity", "dst_region"], ) diff --git a/src/muse/investments.py b/src/muse/investments.py index 9aee3de84..8836e9630 100644 --- a/src/muse/investments.py +++ b/src/muse/investments.py @@ -188,9 +188,7 @@ def cliff_retirement_profile( if kwargs: technical_life = technical_life.sel(**kwargs) if "year" in technical_life.dims: - technical_life = technical_life.interp( - year=investment_year, method=interpolation - ) + technical_life = technical_life.sel(year=investment_year) # Create profile across all years if len(technical_life) > 0: diff --git a/src/muse/sectors/preset_sector.py b/src/muse/sectors/preset_sector.py index 03bf20080..9a4ad6f8d 100644 --- a/src/muse/sectors/preset_sector.py +++ b/src/muse/sectors/preset_sector.py @@ -17,8 +17,6 @@ class PresetSector(AbstractSector): # type: ignore @classmethod def factory(cls, name: str, settings: Any) -> PresetSector: """Constructs a PresetSectors from input data.""" - from collections.abc import Sequence - from xarray import DataArray, zeros_like from muse.commodities import CommodityUsage @@ -51,15 +49,9 @@ def factory(cls, name: str, settings: Any) -> PresetSector: regression_parameters = read_regression_parameters( getattr(sector_conf, "regression_path", None) ) - forecast = getattr(sector_conf, "forecast", 0) - if isinstance(forecast, Sequence): - forecast = DataArray( - forecast, coords={"forecast": forecast}, dims="forecast" - ) consumption = endogenous_demand( drivers=macro_drivers, regression_parameters=regression_parameters, - forecast=forecast, ) if hasattr(sector_conf, "filters"): consumption = consumption.sel(sector_conf.filters._asdict()) @@ -149,23 +141,4 @@ def next(self, mca_market: Dataset) -> Dataset: return result def _interpolate(self, data: DataArray, years: DataArray) -> DataArray: - """Chooses interpolation depending on whether forecast is available.""" - if "forecast" in data.dims: - baseyear = int(years.min()) - forecasted = (years - baseyear).values - result = ( - data.interp( - year=baseyear, - method=self.interpolation_mode, - kwargs={"fill_value": "extrapolate"}, - ) - .interp( - forecast=forecasted, - method=self.interpolation_mode, - kwargs={"fill_value": "extrapolate"}, - ) - .drop_vars(("year", "forecast")) - ) - result["year"] = "forecast", years.values - return result.set_index(forecast="year").rename(forecast="year") return data.interp(year=years, method=self.interpolation_mode).ffill("year") diff --git a/src/muse/sectors/sector.py b/src/muse/sectors/sector.py index 839afbee4..5308b4a96 100644 --- a/src/muse/sectors/sector.py +++ b/src/muse/sectors/sector.py @@ -175,15 +175,6 @@ def __init__( """Full supply, consumption and costs data for the most recent year.""" self.output_data: xr.Dataset - @property - def forecast(self): - """Maximum forecast horizon across agents. - - It cannot be lower than 1 year. - """ - forecasts = [getattr(agent, "forecast") for agent in self.agents] - return max(1, max(forecasts)) - def next( self, mca_market: xr.Dataset, @@ -218,10 +209,13 @@ def group_assets(x: xr.DataArray) -> xr.DataArray: commodity=self.technologies.commodity, region=self.technologies.region ) + # Select technology data for the current year + techs = self.technologies.sel(year=market.year.values[0], drop=True) + # Investments for subsector in self.subsectors: subsector.invest( - self.technologies, + techs, market, time_period=time_period, current_year=current_year, From 3ea64cf6e05dc4b3233d1ebb0d75c80ab7bec61f Mon Sep 17 00:00:00 2001 From: Tom Bland Date: Thu, 19 Dec 2024 10:23:42 +0000 Subject: [PATCH 2/6] Revert "Select technology parameters for the current year" This reverts commit 2a614788236a94614fbeee68b3230366a75a7821. --- src/muse/carbon_budget.py | 4 ++-- src/muse/constraints.py | 2 +- src/muse/demand_share.py | 2 +- src/muse/investments.py | 4 +++- src/muse/sectors/preset_sector.py | 27 +++++++++++++++++++++++++++ src/muse/sectors/sector.py | 14 ++++++++++---- 6 files changed, 44 insertions(+), 9 deletions(-) diff --git a/src/muse/carbon_budget.py b/src/muse/carbon_budget.py index eb6ce522e..e3d032241 100644 --- a/src/muse/carbon_budget.py +++ b/src/muse/carbon_budget.py @@ -101,7 +101,7 @@ def fitting( Returns: new_price: adjusted carbon price to meet budget """ - # Calculate the carbon price and emissions threshold in the investment year + # Calculate the carbon price and emissions threshold in the forecast year future = market.year[-1] threshold = carbon_budget.sel(year=future).values.item() price = market.prices.sel(year=future, commodity=commodities).mean().values.item() @@ -332,7 +332,7 @@ def bisection( # Create cache for emissions at different price points emissions_cache = EmissionsCache(market, equilibrium, commodities) - # Carbon price and emissions threshold in the investment year + # Carbon price and emissions threshold in the forecast year future = market.year[-1] target = carbon_budget.sel(year=future).values.item() price = market.prices.sel(year=future, commodity=commodities).mean().values.item() diff --git a/src/muse/constraints.py b/src/muse/constraints.py index 20113aa19..4afd92e44 100644 --- a/src/muse/constraints.py +++ b/src/muse/constraints.py @@ -475,7 +475,7 @@ def max_production( replacement = replacement.drop_vars( [u for u in replacement.coords if u not in replacement.dims] ) - kwargs = dict(technology=replacement, commodity=commodities) + kwargs = dict(technology=replacement, year=year, commodity=commodities) if "region" in search_space.coords and "region" in technologies.dims: kwargs["region"] = search_space.region techs = ( diff --git a/src/muse/demand_share.py b/src/muse/demand_share.py index 1f72e82ce..9171a816f 100644 --- a/src/muse/demand_share.py +++ b/src/muse/demand_share.py @@ -112,7 +112,7 @@ def demand_share( ) check_dimensions( technologies, - ["technology", "region"], + ["technology", "year", "region"], optional=["timeslice", "commodity", "dst_region"], ) diff --git a/src/muse/investments.py b/src/muse/investments.py index 8836e9630..9aee3de84 100644 --- a/src/muse/investments.py +++ b/src/muse/investments.py @@ -188,7 +188,9 @@ def cliff_retirement_profile( if kwargs: technical_life = technical_life.sel(**kwargs) if "year" in technical_life.dims: - technical_life = technical_life.sel(year=investment_year) + technical_life = technical_life.interp( + year=investment_year, method=interpolation + ) # Create profile across all years if len(technical_life) > 0: diff --git a/src/muse/sectors/preset_sector.py b/src/muse/sectors/preset_sector.py index 9a4ad6f8d..03bf20080 100644 --- a/src/muse/sectors/preset_sector.py +++ b/src/muse/sectors/preset_sector.py @@ -17,6 +17,8 @@ class PresetSector(AbstractSector): # type: ignore @classmethod def factory(cls, name: str, settings: Any) -> PresetSector: """Constructs a PresetSectors from input data.""" + from collections.abc import Sequence + from xarray import DataArray, zeros_like from muse.commodities import CommodityUsage @@ -49,9 +51,15 @@ def factory(cls, name: str, settings: Any) -> PresetSector: regression_parameters = read_regression_parameters( getattr(sector_conf, "regression_path", None) ) + forecast = getattr(sector_conf, "forecast", 0) + if isinstance(forecast, Sequence): + forecast = DataArray( + forecast, coords={"forecast": forecast}, dims="forecast" + ) consumption = endogenous_demand( drivers=macro_drivers, regression_parameters=regression_parameters, + forecast=forecast, ) if hasattr(sector_conf, "filters"): consumption = consumption.sel(sector_conf.filters._asdict()) @@ -141,4 +149,23 @@ def next(self, mca_market: Dataset) -> Dataset: return result def _interpolate(self, data: DataArray, years: DataArray) -> DataArray: + """Chooses interpolation depending on whether forecast is available.""" + if "forecast" in data.dims: + baseyear = int(years.min()) + forecasted = (years - baseyear).values + result = ( + data.interp( + year=baseyear, + method=self.interpolation_mode, + kwargs={"fill_value": "extrapolate"}, + ) + .interp( + forecast=forecasted, + method=self.interpolation_mode, + kwargs={"fill_value": "extrapolate"}, + ) + .drop_vars(("year", "forecast")) + ) + result["year"] = "forecast", years.values + return result.set_index(forecast="year").rename(forecast="year") return data.interp(year=years, method=self.interpolation_mode).ffill("year") diff --git a/src/muse/sectors/sector.py b/src/muse/sectors/sector.py index 5308b4a96..839afbee4 100644 --- a/src/muse/sectors/sector.py +++ b/src/muse/sectors/sector.py @@ -175,6 +175,15 @@ def __init__( """Full supply, consumption and costs data for the most recent year.""" self.output_data: xr.Dataset + @property + def forecast(self): + """Maximum forecast horizon across agents. + + It cannot be lower than 1 year. + """ + forecasts = [getattr(agent, "forecast") for agent in self.agents] + return max(1, max(forecasts)) + def next( self, mca_market: xr.Dataset, @@ -209,13 +218,10 @@ def group_assets(x: xr.DataArray) -> xr.DataArray: commodity=self.technologies.commodity, region=self.technologies.region ) - # Select technology data for the current year - techs = self.technologies.sel(year=market.year.values[0], drop=True) - # Investments for subsector in self.subsectors: subsector.invest( - techs, + self.technologies, market, time_period=time_period, current_year=current_year, From 6b45163b9bff47a36134de81f11958ad996c507d Mon Sep 17 00:00:00 2001 From: Tom Bland Date: Thu, 19 Dec 2024 10:26:44 +0000 Subject: [PATCH 3/6] Select technology parameters for the current year --- src/muse/constraints.py | 4 +++- src/muse/demand_share.py | 2 +- src/muse/sectors/sector.py | 5 ++++- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/muse/constraints.py b/src/muse/constraints.py index 4afd92e44..64d0ffb84 100644 --- a/src/muse/constraints.py +++ b/src/muse/constraints.py @@ -466,6 +466,8 @@ def max_production( from muse.commodities import is_enduse + assert "year" not in technologies.dims + if year is None: year = int(market.year.min()) commodities = technologies.commodity.sel( @@ -475,7 +477,7 @@ def max_production( replacement = replacement.drop_vars( [u for u in replacement.coords if u not in replacement.dims] ) - kwargs = dict(technology=replacement, year=year, commodity=commodities) + kwargs = dict(technology=replacement, commodity=commodities) if "region" in search_space.coords and "region" in technologies.dims: kwargs["region"] = search_space.region techs = ( diff --git a/src/muse/demand_share.py b/src/muse/demand_share.py index 9171a816f..1f72e82ce 100644 --- a/src/muse/demand_share.py +++ b/src/muse/demand_share.py @@ -112,7 +112,7 @@ def demand_share( ) check_dimensions( technologies, - ["technology", "year", "region"], + ["technology", "region"], optional=["timeslice", "commodity", "dst_region"], ) diff --git a/src/muse/sectors/sector.py b/src/muse/sectors/sector.py index 839afbee4..e1c73aa65 100644 --- a/src/muse/sectors/sector.py +++ b/src/muse/sectors/sector.py @@ -218,10 +218,13 @@ def group_assets(x: xr.DataArray) -> xr.DataArray: commodity=self.technologies.commodity, region=self.technologies.region ) + # Select technology data for the current year + techs = self.technologies.sel(year=current_year) + # Investments for subsector in self.subsectors: subsector.invest( - self.technologies, + techs, market, time_period=time_period, current_year=current_year, From ed8c7fb5997e048e94b8564c9f00041b9a6ddbc9 Mon Sep 17 00:00:00 2001 From: Tom Bland Date: Thu, 19 Dec 2024 11:00:34 +0000 Subject: [PATCH 4/6] Rewrite functions to be explicit about year --- src/muse/demand_share.py | 132 +++++++++++++++++++-------------------- 1 file changed, 65 insertions(+), 67 deletions(-) diff --git a/src/muse/demand_share.py b/src/muse/demand_share.py index 1f72e82ce..d7402a5f0 100644 --- a/src/muse/demand_share.py +++ b/src/muse/demand_share.py @@ -260,14 +260,18 @@ def decommissioning(capacity): timeslice_level=timeslice_level, ).squeeze("year") + # Select market and capacity data + current_market = market.isel(year=0, drop=True) + future_market = market.isel(year=1, drop=True) capacity = reduce_assets([u.assets.capacity for u in agents]) + # current_capacity = capacity.interp(year=market.year[0]) # TODO: change to sel + future_capacity = capacity.interp(year=market.year[1]) # TODO: change to sel demands = new_and_retro_demands( - capacity, - market, - technologies, - current_year=current_year, - forecast=forecast, + future_capacity=future_capacity, + current_market=current_market, + future_market=future_market, + technologies=technologies, timeslice_level=timeslice_level, ) @@ -394,16 +398,19 @@ def decommissioning(capacity): if agent.category == "retrofit": raise RetrofitAgentInStandardDemandShare() - # Calculate existing capacity + # Select market and capacity data + current_market = market.isel(year=0, drop=True) + future_market = market.isel(year=1, drop=True) capacity = reduce_assets([agent.assets.capacity for agent in agents]) + current_capacity = capacity.interp(year=market.year[0]) # TODO: change to sel + future_capacity = capacity.interp(year=market.year[1]) # TODO: change to sel # Calculate new and retrofit demands demands = new_and_retro_demands( - capacity, - market, - technologies, - current_year=current_year, - forecast=forecast, + future_capacity=future_capacity, + current_market=current_market, + future_market=future_market, + technologies=technologies, timeslice_level=timeslice_level, ) @@ -474,10 +481,11 @@ def unmet_forecasted_demand( capacity = reduce_assets([u.assets.capacity.interp(year=year) for u in agents]) capacity = cast(xr.DataArray, capacity) result = unmet_demand( - smarket, capacity, technologies, timeslice_level=timeslice_level + market=smarket, + capacity=capacity, + technologies=technologies, + timeslice_level=timeslice_level, ) - if "year" in result.dims: - result = result.squeeze("year") return result @@ -553,9 +561,14 @@ def unmet_demand( """ from muse.quantities import maximum_production + assert "year" not in market.dims + assert "year" not in capacity.dims + # Calculate maximum production by existing assets produced = maximum_production( - capacity=capacity, technologies=technologies, timeslice_level=timeslice_level + capacity=capacity, + technologies=technologies, + timeslice_level=timeslice_level, ) # Total commodity production by summing over assets @@ -567,16 +580,16 @@ def unmet_demand( produced = produced.sum("asset") # Unmet demand is the difference between the consumption and the production - unmet_demand = (market.consumption - produced).clip(min=0) - return unmet_demand + _unmet_demand = (market.consumption - produced).clip(min=0) + assert "year" not in _unmet_demand.dims + return _unmet_demand def new_consumption( - capacity: xr.DataArray, - market: xr.Dataset, + future_capacity: xr.DataArray, + current_market: xr.Dataset, + future_market: xr.Dataset, technologies: xr.Dataset, - current_year: int, - forecast: int, timeslice_level: Optional[str] = None, ) -> xr.DataArray: r"""Computes share of the demand attributed to new agents. @@ -596,28 +609,24 @@ def new_consumption( """ from numpy import minimum - # Interpolate capacity to forecast year - capa = capacity.interp(year=current_year + forecast) - assert isinstance(capa, xr.DataArray) - - # Interpolate market to forecast year - market = market.interp(year=[current_year, current_year + forecast]) - current = market.sel(year=current_year, drop=True) - forecasted = market.sel(year=current_year + forecast, drop=True) - - # Calculate the increase in consumption over the forecast period - delta = (forecasted.consumption - current.consumption).clip(min=0) - missing = unmet_demand(current, capa, technologies, timeslice_level=timeslice_level) + # Calculate the increase in consumption over the time period + delta = (future_market.consumption - current_market.consumption).clip(min=0) + missing = unmet_demand( + market=future_market, + capacity=future_capacity, + technologies=technologies, + timeslice_level=timeslice_level, + ) consumption = minimum(delta, missing) + assert "year" not in consumption.dims return consumption def new_and_retro_demands( - capacity: xr.DataArray, - market: xr.Dataset, + future_capacity: xr.DataArray, + current_market: xr.Dataset, + future_market: xr.Dataset, technologies: xr.Dataset, - current_year: int, - forecast: int, timeslice_level: Optional[str] = None, ) -> xr.Dataset: """Splits demand into *new* and *retrofit* demand. @@ -634,33 +643,27 @@ def new_and_retro_demands( from muse.quantities import maximum_production - # Interpolate market to forecast year - smarket: xr.Dataset = market.interp(year=[current_year, current_year + forecast]) - - # Interpolate capacity to forecast year - capa = capacity.interp(year=[current_year, current_year + forecast]) - assert isinstance(capa, xr.DataArray) - - if hasattr(capa, "region") and capa.region.dims == (): - capa["region"] = "asset", [str(capa.region.values)] * len(capa.asset) + # TODO + if hasattr(future_capacity, "region") and future_capacity.region.dims == (): + future_capacity["region"] = ( + "asset", + [str(future_capacity.region.values)] * len(future_capacity.asset), + ) # Calculate demand to allocate to "new" agents new_demand = new_consumption( - capa, - smarket, - technologies, - current_year=current_year, - forecast=forecast, + future_capacity=future_capacity, + current_market=current_market, + future_market=future_market, + technologies=technologies, timeslice_level=timeslice_level, ) - if "year" in new_demand.dims: - new_demand = new_demand.squeeze("year") # Maximum production in the forecast year by existing assets service = ( maximum_production( - technologies, - capa.sel(year=current_year + forecast), + technologies=technologies, + capacity=future_capacity, timeslice_level=timeslice_level, ) .groupby("region") @@ -668,17 +671,12 @@ def new_and_retro_demands( ) # Existing asset should not execute beyond demand - service = minimum( - service, smarket.consumption.sel(year=current_year + forecast, drop=True) - ) + service = minimum(service, future_market.consumption) # Leftover demand that cannot be serviced by existing assets or "new" agents - retro_demand = ( - smarket.consumption.sel(year=current_year + forecast, drop=True) - - new_demand - - service - ).clip(min=0) - if "year" in retro_demand.dims: - retro_demand = retro_demand.squeeze("year") - - return xr.Dataset({"new": new_demand, "retrofit": retro_demand}) + retro_demand = (future_market.consumption - new_demand - service).clip(min=0) + + # Return new and retrofit demands + result = xr.Dataset({"new": new_demand, "retrofit": retro_demand}) + assert "year" not in result.new.dims + return result From 84d5dfcda7a41b9188a4b918c72457dd5e87b45f Mon Sep 17 00:00:00 2001 From: Tom Bland Date: Thu, 19 Dec 2024 11:09:46 +0000 Subject: [PATCH 5/6] Move decommissioning_demand function --- src/muse/demand_share.py | 56 +++++++++++++++++++++++++++++++++++--- src/muse/quantities.py | 51 ---------------------------------- tests/test_demand_share.py | 21 ++++++++++++-- tests/test_quantities.py | 17 ------------ 4 files changed, 71 insertions(+), 74 deletions(-) diff --git a/src/muse/demand_share.py b/src/muse/demand_share.py index d7402a5f0..7a2bc2cec 100644 --- a/src/muse/demand_share.py +++ b/src/muse/demand_share.py @@ -251,8 +251,6 @@ def new_and_retro( from muse.utilities import agent_concatenation, reduce_assets def decommissioning(capacity): - from muse.quantities import decommissioning_demand - return decommissioning_demand( technologies, capacity, @@ -384,8 +382,6 @@ def standard_demand( from muse.utilities import agent_concatenation, reduce_assets def decommissioning(capacity): - from muse.quantities import decommissioning_demand - return decommissioning_demand( technologies, capacity, @@ -680,3 +676,55 @@ def new_and_retro_demands( result = xr.Dataset({"new": new_demand, "retrofit": retro_demand}) assert "year" not in result.new.dims return result + + +def decommissioning_demand( + technologies: xr.Dataset, + capacity: xr.DataArray, + year: Optional[Sequence[int]] = None, + timeslice_level: Optional[str] = None, +) -> xr.DataArray: + r"""Computes demand from process decommissioning. + + If `year` is not given, it defaults to all years in capacity. If there are more than + two years, then decommissioning is with respect to first (or minimum) year. + + Let :math:`M_t^r(y)` be the retrofit demand, :math:`^{(s)}\mathcal{D}_t^r(y)` be the + decommissioning demand at the level of the sector, and :math:`A^r_{t, \iota}(y)` be + the assets owned by the agent. Then, the decommissioning demand for agent :math:`i` + is : + + .. math:: + + \mathcal{D}^{r, i}_{t, c}(y) = + \sum_\iota \alpha_{t, \iota}^r \beta_{t, \iota, c}^r + \left(A^{i, r}_{t, \iota}(y) - A^{i, r}_{t, \iota, c}(y + 1) \right) + + given the utilization factor :math:`\alpha_{t, \iota}` and the fixed output factor + :math:`\beta_{t, \iota, c}`. + + Furthermore, decommissioning demand is non-zero only for end-use commodities. + + ncsearch-nohlsearch).. SeeAlso: + :ref:`indices`, :ref:`quantities`, + :py:func:`~muse.quantities.maximum_production` + :py:func:`~muse.commodities.is_enduse` + """ + from muse.quantities import maximum_production + + if year is None: + year = capacity.year.values + year = sorted(year) + capacity = capacity.interp(year=year, kwargs={"fill_value": 0.0}) + baseyear = min(year) + dyears = [u for u in year if u != baseyear] + + # Calculate the decrease in capacity from the current year to future years + capacity_decrease = capacity.sel(year=baseyear) - capacity.sel(year=dyears) + + # Calculate production associated with this capacity + return maximum_production( + technologies, + capacity_decrease, + timeslice_level=timeslice_level, + ).clip(min=0) diff --git a/src/muse/quantities.py b/src/muse/quantities.py index c5626a2c5..0fcddfd0a 100644 --- a/src/muse/quantities.py +++ b/src/muse/quantities.py @@ -7,7 +7,6 @@ Functions for calculating costs (e.g. LCOE, EAC) are in the `costs` module. """ -from collections.abc import Sequence from typing import Optional, Union, cast import numpy as np @@ -234,56 +233,6 @@ def gross_margin( return result -def decommissioning_demand( - technologies: xr.Dataset, - capacity: xr.DataArray, - year: Optional[Sequence[int]] = None, - timeslice_level: Optional[str] = None, -) -> xr.DataArray: - r"""Computes demand from process decommissioning. - - If `year` is not given, it defaults to all years in capacity. If there are more than - two years, then decommissioning is with respect to first (or minimum) year. - - Let :math:`M_t^r(y)` be the retrofit demand, :math:`^{(s)}\mathcal{D}_t^r(y)` be the - decommissioning demand at the level of the sector, and :math:`A^r_{t, \iota}(y)` be - the assets owned by the agent. Then, the decommissioning demand for agent :math:`i` - is : - - .. math:: - - \mathcal{D}^{r, i}_{t, c}(y) = - \sum_\iota \alpha_{t, \iota}^r \beta_{t, \iota, c}^r - \left(A^{i, r}_{t, \iota}(y) - A^{i, r}_{t, \iota, c}(y + 1) \right) - - given the utilization factor :math:`\alpha_{t, \iota}` and the fixed output factor - :math:`\beta_{t, \iota, c}`. - - Furthermore, decommissioning demand is non-zero only for end-use commodities. - - ncsearch-nohlsearch).. SeeAlso: - :ref:`indices`, :ref:`quantities`, - :py:func:`~muse.quantities.maximum_production` - :py:func:`~muse.commodities.is_enduse` - """ - if year is None: - year = capacity.year.values - year = sorted(year) - capacity = capacity.interp(year=year, kwargs={"fill_value": 0.0}) - baseyear = min(year) - dyears = [u for u in year if u != baseyear] - - # Calculate the decrease in capacity from the current year to future years - capacity_decrease = capacity.sel(year=baseyear) - capacity.sel(year=dyears) - - # Calculate production associated with this capacity - return maximum_production( - technologies, - capacity_decrease, - timeslice_level=timeslice_level, - ).clip(min=0) - - def consumption( technologies: xr.Dataset, production: xr.DataArray, diff --git a/tests/test_demand_share.py b/tests/test_demand_share.py index fc1b14602..d59b24743 100644 --- a/tests/test_demand_share.py +++ b/tests/test_demand_share.py @@ -153,7 +153,7 @@ def test_demand_split(technologies, stock, matching_market): from muse.demand_share import _inner_split as inner_split def method(capacity): - from muse.quantities import decommissioning_demand + from muse.demand_share import decommissioning_demand return decommissioning_demand( technologies.sel(region="USA"), @@ -189,7 +189,7 @@ def test_demand_split_zero_share(technologies, stock, matching_market): from muse.demand_share import _inner_split as inner_split def method(capacity): - from muse.quantities import decommissioning_demand + from muse.demand_share import decommissioning_demand return 0 * decommissioning_demand( technologies.sel(region="USA"), @@ -387,3 +387,20 @@ class Agent: assert result.sel(commodity=enduse).values == approx( 0.5 * market.consumption.sel(commodity=enduse).interp(year=2015).values ) + + +def test_decommissioning_demand(technologies, capacity, timeslice): + from muse.commodities import is_enduse + from muse.demand_share import decommissioning_demand + + years = [2010, 2015] + capacity = capacity.interp(year=years) + capacity.loc[{"year": 2010}] = current = 1.3 + capacity.loc[{"year": 2015}] = forecast = 1.0 + technologies.fixed_outputs[:] = fouts = 0.5 + technologies.utilization_factor[:] = ufac = 0.4 + decom = decommissioning_demand(technologies, capacity, years) + assert set(decom.dims) == {"asset", "commodity", "year", "timeslice"} + assert decom.sel(commodity=is_enduse(technologies.comm_usage)).sum( + "timeslice" + ).values == approx(ufac * fouts * (current - forecast)) diff --git a/tests/test_quantities.py b/tests/test_quantities.py index ed5e09525..bdf75362b 100644 --- a/tests/test_quantities.py +++ b/tests/test_quantities.py @@ -96,23 +96,6 @@ def test_gross_margin(technologies, capacity, market, timeslice): assert actual.values == approx(expected.values) -def test_decommissioning_demand(technologies, capacity, timeslice): - from muse.commodities import is_enduse - from muse.quantities import decommissioning_demand - - years = [2010, 2015] - capacity = capacity.interp(year=years) - capacity.loc[{"year": 2010}] = current = 1.3 - capacity.loc[{"year": 2015}] = forecast = 1.0 - technologies.fixed_outputs[:] = fouts = 0.5 - technologies.utilization_factor[:] = ufac = 0.4 - decom = decommissioning_demand(technologies, capacity, years) - assert set(decom.dims) == {"asset", "commodity", "year", "timeslice"} - assert decom.sel(commodity=is_enduse(technologies.comm_usage)).sum( - "timeslice" - ).values == approx(ufac * fouts * (current - forecast)) - - def test_consumption(technologies, production, market): from muse.quantities import consumption From 2737761c6ce1356a8269829bf721234a4237b1d2 Mon Sep 17 00:00:00 2001 From: Tom Bland Date: Thu, 19 Dec 2024 11:13:52 +0000 Subject: [PATCH 6/6] Simplify decommissioning_demand function --- src/muse/demand_share.py | 35 +++++++++++++++++------------------ 1 file changed, 17 insertions(+), 18 deletions(-) diff --git a/src/muse/demand_share.py b/src/muse/demand_share.py index 7a2bc2cec..0194af777 100644 --- a/src/muse/demand_share.py +++ b/src/muse/demand_share.py @@ -251,12 +251,14 @@ def new_and_retro( from muse.utilities import agent_concatenation, reduce_assets def decommissioning(capacity): + current_capacity = capacity.interp(year=current_year) # TODO + future_capacity = capacity.interp(year=current_year + forecast) # TODO return decommissioning_demand( technologies, - capacity, - year=[current_year, current_year + forecast], + current_capacity, + future_capacity, timeslice_level=timeslice_level, - ).squeeze("year") + ) # Select market and capacity data current_market = market.isel(year=0, drop=True) @@ -382,12 +384,14 @@ def standard_demand( from muse.utilities import agent_concatenation, reduce_assets def decommissioning(capacity): + current_capacity = capacity.interp(year=current_year) # TODO + future_capacity = capacity.interp(year=current_year + forecast) # TODO return decommissioning_demand( technologies, - capacity, - year=[current_year, current_year + forecast], + current_capacity, + future_capacity, timeslice_level=timeslice_level, - ).squeeze("year") + ) # Make sure there are no retrofit agents for agent in agents: @@ -680,8 +684,8 @@ def new_and_retro_demands( def decommissioning_demand( technologies: xr.Dataset, - capacity: xr.DataArray, - year: Optional[Sequence[int]] = None, + current_capacity: xr.DataArray, + future_capacity: xr.DataArray, timeslice_level: Optional[str] = None, ) -> xr.DataArray: r"""Computes demand from process decommissioning. @@ -712,19 +716,14 @@ def decommissioning_demand( """ from muse.quantities import maximum_production - if year is None: - year = capacity.year.values - year = sorted(year) - capacity = capacity.interp(year=year, kwargs={"fill_value": 0.0}) - baseyear = min(year) - dyears = [u for u in year if u != baseyear] - - # Calculate the decrease in capacity from the current year to future years - capacity_decrease = capacity.sel(year=baseyear) - capacity.sel(year=dyears) + # Calculate the decrease in capacity from the current year to future year + capacity_decrease = current_capacity - future_capacity # Calculate production associated with this capacity - return maximum_production( + result = maximum_production( technologies, capacity_decrease, timeslice_level=timeslice_level, ).clip(min=0) + assert "year" not in result.dims + return result