diff --git a/src/muse/demand_share.py b/src/muse/demand_share.py index 9171a816f..0194af777 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"], ) @@ -251,23 +251,27 @@ def new_and_retro( from muse.utilities import agent_concatenation, reduce_assets def decommissioning(capacity): - from muse.quantities import decommissioning_demand - + 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) + 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, ) @@ -380,30 +384,33 @@ def standard_demand( from muse.utilities import agent_concatenation, reduce_assets def decommissioning(capacity): - from muse.quantities import decommissioning_demand - + 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: 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,59 @@ 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") + 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 + + +def decommissioning_demand( + technologies: xr.Dataset, + current_capacity: xr.DataArray, + future_capacity: xr.DataArray, + 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:: - return xr.Dataset({"new": new_demand, "retrofit": retro_demand}) + \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 + + # Calculate the decrease in capacity from the current year to future year + capacity_decrease = current_capacity - future_capacity + + # Calculate production associated with this capacity + result = maximum_production( + technologies, + capacity_decrease, + timeslice_level=timeslice_level, + ).clip(min=0) + assert "year" not in result.dims + return result diff --git a/src/muse/quantities.py b/src/muse/quantities.py index 4d639413f..4e2a52741 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/src/muse/sectors/sector.py b/src/muse/sectors/sector.py index 79f15b1ad..e189811a3 100644 --- a/src/muse/sectors/sector.py +++ b/src/muse/sectors/sector.py @@ -213,10 +213,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, 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