diff --git a/docs/application-flow.rst b/docs/application-flow.rst index 6e20a7429..8f8ecda87 100644 --- a/docs/application-flow.rst +++ b/docs/application-flow.rst @@ -503,11 +503,9 @@ The dispatch stage when running a sector can be described by the following graph After the investment stage is completed, then the new capacity of the sector is obtained by aggregating the assets of all agents of the sector. Then, the supply of commodities is calculated as requested by the ``dispatch_production`` argument defined for each sector in the ``settings.toml`` file. -The typical choice used in most examples in MUSE is ``share``, where the utilization across similar assets is the same in percentage. However, there are other options available, like - -- ``costed``: assets are ranked by their levelised costs and the cheaper ones are allowed to service the demand first up to their maximum production. Minimum service can be imposed if present. +There are two possible options for ``dispatch_production`` built into MUSE: +- ``share``: assets each supply a proportion of demand based on their share of total capacity. - ``maximum``: all the assets dispatch their maximum production, regardless of the demand. -- ``match``: supply matches the demand within the constrains on how much an asset can produce while minimizing the overall associated costs. ``match`` allows the choice between different metrics to rank assets, such as levelised costs and gross margin. See :py:mod:`muse.demand_matching` for the mathematical details. Once the supply is obtained, the consumed commodities required to achieve that production level are calculated. The cheapest fuel for flexible technologies is used. diff --git a/docs/inputs/toml.rst b/docs/inputs/toml.rst index 6ebaf6dad..efeccb15d 100644 --- a/docs/inputs/toml.rst +++ b/docs/inputs/toml.rst @@ -327,41 +327,21 @@ A sector accepts these attributes: .. _scipy method's kind attribute: https://docs.scipy.org/doc/scipy/reference/generated/scipy.interpolate.interp1d.html -*investment_production* - In its simplest form, this is the name of a method to compute the production from a - sector, as used when splitting the demand across agents. In other words, this is the - computation of the production which affects future investments. In it's more general - form, *production* can be a subsection of its own, with a "name" attribute. For - instance: - - .. code-block:: TOML - - [sectors.residential.production] - name = "match" - costing = "prices" +*dispatch_production* + The method used to calculate supply of commodities after investments have been made. MUSE provides two methods in :py:mod:`muse.production`: - - share: the production is the maximum production for the existing capacity and + - share: assets each supply a proportion of demand based on their share of total + capacity + - maximum: the production is the maximum production for the existing capacity and the technology's utilization factor. See :py:func:`muse.production.maximum_production`. - - match: production and demand are matched according to a given cost metric. The - cost metric defaults to "prices". It can be modified by using the general form - given above, with a "costing" attribute. The latter can be "prices", - "gross_margin", or "lcoe". - See :py:func:`muse.production.demand_matched_production`. - - *production* can also refer to any custom production method registered with MUSE via - :py:func:`muse.production.register_production`. Defaults to "share". -*dispatch_production* - The name of the production method used to compute the sector's output, as returned - to the muse market clearing algorithm. In other words, this is computation of the - production method which will affect other sectors. - - It has the same format and options as the *production* attribute above. + Additional methods can be registered with + :py:func:`muse.production.register_production` Sectors contain a number of subsections: *interactions* diff --git a/src/muse/demand_share.py b/src/muse/demand_share.py index 36a3c6488..9f9567553 100644 --- a/src/muse/demand_share.py +++ b/src/muse/demand_share.py @@ -114,7 +114,6 @@ def new_and_retro( technologies: xr.Dataset, current_year: int, forecast: int, - production: Union[str, Mapping, Callable] = "maximum_production", ) -> xr.DataArray: r"""Splits demand across new and retro agents. @@ -131,7 +130,6 @@ def new_and_retro( to the production method. The ``consumption`` reflects the demand for the commodities produced by the current sector. technologies: quantities describing the technologies. - production: Production method current_year: Current year of simulation forecast: How many years to forecast ahead @@ -161,8 +159,8 @@ def new_and_retro( simplicity. The resulting expression has the same indices as the consumption :math:`\mathcal{C}_{c, s}^r`. - :math:`P` is any function registered with - :py:func:`@register_production`. + :math:`P` is the maximum production, given by + `. #. the *new* demand :math:`N` is defined as: @@ -245,7 +243,6 @@ def decommissioning(capacity): capacity, market, technologies, - production=production, current_year=current_year, forecast=forecast, ) @@ -330,7 +327,6 @@ def standard_demand( technologies: xr.Dataset, current_year: int, forecast: int, - production: Union[str, Mapping, Callable] = "maximum_production", ) -> xr.DataArray: r"""Splits demand across new agents. @@ -347,7 +343,6 @@ def standard_demand( to the production method. The ``consumption`` reflects the demand for the commodities produced by the current sector. technologies: quantities describing the technologies. - production: Production method current_year: Current year of simulation forecast: How many years to forecast ahead @@ -381,7 +376,6 @@ def decommissioning(capacity): capacity, market, technologies, - production=production, current_year=current_year, forecast=forecast, ) @@ -441,7 +435,6 @@ def unmet_forecasted_demand( technologies: xr.Dataset, current_year: int, forecast: int, - production: Union[str, Mapping, Callable] = "maximum_production", ) -> xr.DataArray: """Forecast demand that cannot be serviced by non-decommissioned current assets.""" from muse.commodities import is_enduse @@ -452,7 +445,7 @@ def unmet_forecasted_demand( smarket: xr.Dataset = market.where(is_enduse(comm_usage), 0).interp(year=year) capacity = reduce_assets([u.assets.capacity.interp(year=year) for u in agents]) capacity = cast(xr.DataArray, capacity) - result = unmet_demand(smarket, capacity, technologies, production) + result = unmet_demand(smarket, capacity, technologies) if "year" in result.dims: result = result.squeeze("year") return result @@ -514,7 +507,6 @@ def unmet_demand( market: xr.Dataset, capacity: xr.DataArray, technologies: xr.Dataset, - production: Union[str, Mapping, Callable] = "maximum_production", ): r"""Share of the demand that cannot be serviced by the existing assets. @@ -526,16 +518,14 @@ def unmet_demand( The resulting expression has the same indices as the consumption :math:`\mathcal{C}_{c, s}^r`. - :math:`P` is any function registered with - :py:func:`@register_production`. + :math:`P` is the maximum production, given by . """ - from muse.production import factory as prod_factory - - prod_method = production if callable(production) else prod_factory(production) - assert callable(prod_method) + from muse.quantities import maximum_production - # Calculate production by existing assets - produced = prod_method(market=market, capacity=capacity, technologies=technologies) + # Calculate maximum production by existing assets + produced = maximum_production( + capacity=capacity, technologies=technologies, timeslices=market.timeslice + ) # Total commodity production by summing over assets if "dst_region" in produced.dims: @@ -569,7 +559,8 @@ def new_consumption( - P[\mathcal{M}(y + \Delta y), \mathcal{A}_{a, s}^r(y)] \right) - Where :math:`P` is a production function taking the market and assets as arguments. + Where :math:`P` the maximum production by existing assets, given by + . """ from numpy import minimum @@ -595,7 +586,6 @@ def new_and_retro_demands( technologies: xr.Dataset, current_year: int, forecast: int, - production: Union[str, Mapping, Callable] = "maximum_production", ) -> xr.Dataset: """Splits demand into *new* and *retrofit* demand. @@ -609,10 +599,7 @@ def new_and_retro_demands( """ from numpy import minimum - from muse.production import factory as prod_factory - - production_method = production if callable(production) else prod_factory(production) - assert callable(production_method) + from muse.quantities import maximum_production # Interpolate market to forecast year smarket: xr.Dataset = market.interp(year=[current_year, current_year + forecast]) @@ -628,12 +615,12 @@ def new_and_retro_demands( if "year" in new_demand.dims: new_demand = new_demand.squeeze("year") - # Total production in the forecast year by existing assets + # Maximum production in the forecast year by existing assets service = ( - production_method( - smarket.sel(year=current_year + forecast), - capa.sel(year=current_year + forecast), + maximum_production( technologies, + capa.sel(year=current_year + forecast), + timeslices=smarket.timeslice, ) .groupby("region") .sum("asset") diff --git a/src/muse/quantities.py b/src/muse/quantities.py index f2ea8f887..339024c63 100644 --- a/src/muse/quantities.py +++ b/src/muse/quantities.py @@ -8,7 +8,7 @@ """ from collections.abc import Sequence -from typing import Callable, Optional, Union, cast +from typing import Optional, Union, cast import numpy as np import xarray as xr @@ -18,8 +18,6 @@ def supply( capacity: xr.DataArray, demand: xr.DataArray, technologies: Union[xr.Dataset, xr.DataArray], - interpolation: str = "linear", - production_method: Optional[Callable] = None, ) -> xr.DataArray: """Production and emission for a given capacity servicing a given demand. @@ -36,8 +34,6 @@ def supply( exceed its share of the demand. technologies: factors bindings the capacity of an asset with its production of commodities and environmental pollutants. - interpolation: Interpolation type - production_method: Production for a given capacity Return: A data array where the commodity dimension only contains actual outputs (i.e. no @@ -45,10 +41,7 @@ def supply( """ from muse.commodities import CommodityUsage, check_usage, is_pollutant - if production_method is None: - production_method = maximum_production - - maxprod = production_method(technologies, capacity, timeslices=demand) + maxprod = maximum_production(technologies, capacity, timeslices=demand) minprod = minimum_production(technologies, capacity, timeslices=demand) size = np.array(maxprod.region).size # in presence of trade demand needs to map maxprod dst_region diff --git a/tests/test_demand_share.py b/tests/test_demand_share.py index cc5ba52f0..9df92cf35 100644 --- a/tests/test_demand_share.py +++ b/tests/test_demand_share.py @@ -120,17 +120,18 @@ def test_new_retro_split_zero_new_unmet(technologies, stock, matching_market): def test_new_retro_accounting_identity(technologies, stock, market): from muse.demand_share import new_and_retro_demands - from muse.production import factory + from muse.quantities import maximum_production share = new_and_retro_demands( stock.capacity, market, technologies, current_year=2010, forecast=5 ) assert (share >= 0).all() - production_method = factory() serviced = ( - production_method( - market.interp(year=2015), stock.capacity.interp(year=2015), technologies + maximum_production( + capacity=stock.capacity.interp(year=2015), + technologies=technologies, + timeslices=market.timeslice, ) .groupby("region") .sum("asset")