From a5c7a8d38333d259cd0166186c87319fa5bb8554 Mon Sep 17 00:00:00 2001 From: Tom Bland Date: Mon, 11 Nov 2024 13:56:30 +0000 Subject: [PATCH 1/6] Delete production module --- docs/api.rst | 7 --- docs/inputs/toml.rst | 36 ----------- src/muse/demand_share.py | 29 +++------ src/muse/production.py | 124 ------------------------------------- src/muse/sectors/sector.py | 44 +++---------- 5 files changed, 19 insertions(+), 221 deletions(-) delete mode 100644 src/muse/production.py diff --git a/docs/api.rst b/docs/api.rst index 0d47458d6..00e5990e3 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -56,13 +56,6 @@ PresetSector :members: -Production -~~~~~~~~~~ - -.. automodule:: muse.production - :members: - - Agent Interactions ~~~~~~~~~~~~~~~~~~ diff --git a/docs/inputs/toml.rst b/docs/inputs/toml.rst index 6ebaf6dad..ad454a193 100644 --- a/docs/inputs/toml.rst +++ b/docs/inputs/toml.rst @@ -327,42 +327,6 @@ 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" - - MUSE provides two methods in :py:mod:`muse.production`: - - - share: 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. - Sectors contain a number of subsections: *interactions* Defines interactions between agents. These interactions take place right before new diff --git a/src/muse/demand_share.py b/src/muse/demand_share.py index 36a3c6488..208463a1f 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. @@ -245,7 +244,6 @@ def decommissioning(capacity): capacity, market, technologies, - production=production, current_year=current_year, forecast=forecast, ) @@ -330,7 +328,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. @@ -381,7 +378,6 @@ def decommissioning(capacity): capacity, market, technologies, - production=production, current_year=current_year, forecast=forecast, ) @@ -441,7 +437,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 +447,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 +509,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. @@ -529,13 +523,12 @@ def unmet_demand( :math:`P` is any function registered with :py:func:`@register_production`. """ - 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 supply # Calculate production by existing assets - produced = prod_method(market=market, capacity=capacity, technologies=technologies) + produced = supply( + capacity=capacity, demand=market.consumption, technologies=technologies + ) # Total commodity production by summing over assets if "dst_region" in produced.dims: @@ -595,7 +588,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 +601,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]) @@ -630,10 +619,10 @@ def new_and_retro_demands( # Total 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/production.py b/src/muse/production.py deleted file mode 100644 index 53494c06f..000000000 --- a/src/muse/production.py +++ /dev/null @@ -1,124 +0,0 @@ -"""Various ways and means to compute production. - -Production is the amount of commodities produced by an asset. However, depending on the -context, it could be computed several ways. For instance, it can be obtained straight -from the capacity of the asset. Or it can be obtained by matching for the same -commodities with a set of assets. - -Production methods can be registered via the :py:func:`@register_production -` production decorator. Registering a function makes the function -accessible from MUSE's input file. Production methods are not expected to modify their -arguments. Furthermore they should conform the -following signatures: - -.. code-block:: python - - @register_production - def production( - market: xr.Dataset, capacity: xr.DataArray, technologies: xr.Dataset, **kwargs - ) -> xr.DataArray: - pass - - -Arguments: - market: Market, including demand and prices. - capacity: The capacity of each asset within a market. - technologies: A dataset characterising the technologies of the same assets. - **kwargs: Any number of keyword arguments - -Returns: - A `xr.DataArray` with the amount produced for each good from each asset. -""" - -__all__ = [ - "factory", - "maximum_production", - "register_production", - "supply", - "PRODUCTION_SIGNATURE", -] -from collections.abc import Mapping, MutableMapping -from typing import Any, Callable, Union, cast - -import xarray as xr - -from muse.registration import registrator - -PRODUCTION_SIGNATURE = Callable[[xr.DataArray, xr.DataArray, xr.Dataset], xr.DataArray] -"""Production signature.""" - -PRODUCTION_METHODS: MutableMapping[str, PRODUCTION_SIGNATURE] = {} -"""Dictionary of production methods. """ - - -@registrator(registry=PRODUCTION_METHODS, loglevel="info") -def register_production(function: PRODUCTION_SIGNATURE = None): - """Decorator to register a function as a production method. - - .. seealso:: - - :py:mod:`muse.production` - """ - return function - - -def factory( - settings: Union[str, Mapping] = "maximum_production", **kwargs -) -> PRODUCTION_SIGNATURE: - """Creates a production functor. - - This function's raison d'ĂȘtre is to convert the input from a TOML file into an - actual functor usable within the model, i.e. it converts data into logic. - - Arguments: - settings: Registered production method to create. The name is resolved when the - function returned by the factory is called. Hence, it could refer to a - function yet to be registered when this factory method is called. - **kwargs: any keyword argument the production method accepts. - """ - from functools import partial - - from muse.production import PRODUCTION_METHODS - - if isinstance(settings, str): - name = settings - keywords: MutableMapping[str, Any] = dict() - else: - keywords = dict(**settings) - name = keywords.pop("name") - - keywords.update(**kwargs) - name = keywords.pop("name", name) - - method = PRODUCTION_METHODS[name] - return cast( - PRODUCTION_SIGNATURE, method if not keywords else partial(method, **keywords) - ) - - -@register_production(name=("max", "maximum")) -def maximum_production( - market: xr.Dataset, capacity: xr.DataArray, technologies: xr.Dataset -) -> xr.DataArray: - """Production when running at full capacity. - - *Full capacity* is limited by the utilization factor. For more details, see - :py:func:`muse.quantities.maximum_production`. - """ - from muse.quantities import maximum_production - - return maximum_production(technologies, capacity, timeslices=market.timeslice) - - -@register_production(name=("share", "shares")) -def supply( - market: xr.Dataset, capacity: xr.DataArray, technologies: xr.Dataset -) -> xr.DataArray: - """Service current demand equally from all assets. - - "Equally" means that equivalent technologies are used to the same percentage of - their respective capacity. - """ - from muse.quantities import supply - - return supply(capacity, market.consumption, technologies) diff --git a/src/muse/sectors/sector.py b/src/muse/sectors/sector.py index 8d0f082ff..2b90dc808 100644 --- a/src/muse/sectors/sector.py +++ b/src/muse/sectors/sector.py @@ -11,7 +11,6 @@ import xarray as xr from muse.agents import AbstractAgent -from muse.production import PRODUCTION_SIGNATURE from muse.sectors.abstract import AbstractSector from muse.sectors.register import register_sector from muse.sectors.subsector import Subsector @@ -25,10 +24,8 @@ class Sector(AbstractSector): # type: ignore def factory(cls, name: str, settings: Any) -> Sector: from muse.interactions import factory as interaction_factory from muse.outputs.sector import factory as ofactory - from muse.production import factory as pfactory from muse.readers import read_timeslices from muse.readers.toml import read_technodata - from muse.utilities import nametuple_to_dict # Read sector settings sector_settings = getattr(settings.sectors, name)._asdict() @@ -71,15 +68,6 @@ def factory(cls, name: str, settings: Any) -> Sector: # Create outputs outputs = ofactory(*sector_settings.pop("outputs", []), sector_name=name) - supply_args = sector_settings.pop( - "supply", sector_settings.pop("dispatch_production", {}) - ) - if isinstance(supply_args, str): - supply_args = {"name": supply_args} - else: - supply_args = nametuple_to_dict(supply_args) - supply = pfactory(**supply_args) - # Create interactions interactions = interaction_factory(sector_settings.pop("interactions", None)) @@ -89,6 +77,7 @@ def factory(cls, name: str, settings: Any) -> Sector: "commodities_out", "commodities_in", "technodata_timeslices", + "dispatch_production", # old parameter which may still exist in the file ): sector_settings.pop(attr, None) return cls( @@ -96,7 +85,6 @@ def factory(cls, name: str, settings: Any) -> Sector: technologies, subsectors=subsectors, timeslices=timeslices, - supply_prod=supply, outputs=outputs, interactions=interactions, **sector_settings, @@ -111,11 +99,9 @@ def __init__( interactions: Callable[[Sequence[AbstractAgent]], None] | None = None, interpolation: str = "linear", outputs: Callable | None = None, - supply_prod: PRODUCTION_SIGNATURE | None = None, ): from muse.interactions import factory as interaction_factory from muse.outputs.sector import factory as ofactory - from muse.production import maximum_production self.name: str = name """Name of the sector.""" @@ -155,14 +141,6 @@ def __init__( cast(Callable, ofactory()) if outputs is None else outputs ) """A function for outputting data for post-mortem analysis.""" - self.supply_prod = ( - supply_prod if supply_prod is not None else maximum_production - ) - """ Computes production as used to return the supply to the MCA. - - It can be anything registered with - :py:func:`@register_production`. - """ self.output_data: xr.Dataset """Full supply, consumption and costs data for the most recent year.""" @@ -282,7 +260,7 @@ def market_variables(self, market: xr.Dataset, technologies: xr.Dataset) -> Any: """Computes resulting market: production, consumption, and costs.""" from muse.commodities import is_pollutant from muse.costs import annual_levelized_cost_of_energy, supply_cost - from muse.quantities import consumption + from muse.quantities import consumption, supply from muse.timeslices import QuantityType, convert_timeslice from muse.utilities import broadcast_techs @@ -290,26 +268,24 @@ def market_variables(self, market: xr.Dataset, technologies: xr.Dataset) -> Any: capacity = self.capacity.interp(year=years, **self.interpolation) # Calculate supply - supply = self.supply_prod( - market=market, capacity=capacity, technologies=technologies - ) - if "timeslice" in market.prices.dims and "timeslice" not in supply.dims: - supply = convert_timeslice(supply, market.timeslice, QuantityType.EXTENSIVE) + sup = supply(capacity, market.consumption, technologies) + if "timeslice" in market.prices.dims and "timeslice" not in sup.dims: + sup = convert_timeslice(sup, market.timeslice, QuantityType.EXTENSIVE) # Calculate consumption - consume = consumption(technologies, supply, market.prices) + consume = consumption(technologies, sup, market.prices) # Calculate commodity prices - technodata = cast(xr.Dataset, broadcast_techs(technologies, supply)) + technodata = cast(xr.Dataset, broadcast_techs(technologies, sup)) costs = supply_cost( - supply.where(~is_pollutant(supply.comm_usage), 0), + sup.where(~is_pollutant(sup.comm_usage), 0), annual_levelized_cost_of_energy( - prices=market.prices.sel(region=supply.region), technologies=technodata + prices=market.prices.sel(region=sup.region), technologies=technodata ), asset_dim="asset", ) - return supply, consume, costs + return sup, consume, costs @property def capacity(self) -> xr.DataArray: From 19c0c23b35d3cb01eb8ce4e19d8f19a50e1e2e8d Mon Sep 17 00:00:00 2001 From: Tom Bland Date: Mon, 11 Nov 2024 14:19:16 +0000 Subject: [PATCH 2/6] Switch one case of supply to maximum_production --- src/muse/demand_share.py | 6 +++--- src/muse/quantities.py | 11 ++--------- 2 files changed, 5 insertions(+), 12 deletions(-) diff --git a/src/muse/demand_share.py b/src/muse/demand_share.py index 208463a1f..ae90fa122 100644 --- a/src/muse/demand_share.py +++ b/src/muse/demand_share.py @@ -523,11 +523,11 @@ def unmet_demand( :math:`P` is any function registered with :py:func:`@register_production`. """ - from muse.quantities import supply + from muse.quantities import maximum_production # Calculate production by existing assets - produced = supply( - capacity=capacity, demand=market.consumption, technologies=technologies + produced = maximum_production( + capacity=capacity, technologies=technologies, timeslices=market.timeslice ) # Total commodity production by summing over assets 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 From 234d42ad8dcb4153dde849342ab7cc36a91429b7 Mon Sep 17 00:00:00 2001 From: Tom Bland Date: Mon, 11 Nov 2024 14:25:44 +0000 Subject: [PATCH 3/6] Fix test --- tests/test_demand_share.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) 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") From 78579c8a7ba387d0534d6e5037589894b6a84218 Mon Sep 17 00:00:00 2001 From: Tom Bland Date: Mon, 11 Nov 2024 15:17:38 +0000 Subject: [PATCH 4/6] Update comments and docstrings --- src/muse/demand_share.py | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/src/muse/demand_share.py b/src/muse/demand_share.py index ae90fa122..9f9567553 100644 --- a/src/muse/demand_share.py +++ b/src/muse/demand_share.py @@ -130,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 @@ -160,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: @@ -344,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 @@ -520,12 +518,11 @@ 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.quantities import maximum_production - # Calculate production by existing assets + # Calculate maximum production by existing assets produced = maximum_production( capacity=capacity, technologies=technologies, timeslices=market.timeslice ) @@ -562,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 @@ -617,7 +615,7 @@ 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 = ( maximum_production( technologies, From 486db2d3b0f6e2184757ec616d43641eba105503 Mon Sep 17 00:00:00 2001 From: Tom Bland Date: Mon, 11 Nov 2024 16:13:10 +0000 Subject: [PATCH 5/6] Restore production module, only for dispatch --- docs/api.rst | 7 +++ docs/application-flow.rst | 6 +- docs/inputs/toml.rst | 16 +++++ src/muse/production.py | 124 +++++++++++++++++++++++++++++++++++++ src/muse/sectors/sector.py | 44 ++++++++++--- 5 files changed, 183 insertions(+), 14 deletions(-) create mode 100644 src/muse/production.py diff --git a/docs/api.rst b/docs/api.rst index 00e5990e3..0d47458d6 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -56,6 +56,13 @@ PresetSector :members: +Production +~~~~~~~~~~ + +.. automodule:: muse.production + :members: + + Agent Interactions ~~~~~~~~~~~~~~~~~~ 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 ad454a193..198381bc8 100644 --- a/docs/inputs/toml.rst +++ b/docs/inputs/toml.rst @@ -327,6 +327,22 @@ A sector accepts these attributes: .. _scipy method's kind attribute: https://docs.scipy.org/doc/scipy/reference/generated/scipy.interpolate.interp1d.html +*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: 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`. + + Defaults to "share". + + Additional methods can be registered with + :py:func:`muse.production.register_production` + Sectors contain a number of subsections: *interactions* Defines interactions between agents. These interactions take place right before new diff --git a/src/muse/production.py b/src/muse/production.py new file mode 100644 index 000000000..53494c06f --- /dev/null +++ b/src/muse/production.py @@ -0,0 +1,124 @@ +"""Various ways and means to compute production. + +Production is the amount of commodities produced by an asset. However, depending on the +context, it could be computed several ways. For instance, it can be obtained straight +from the capacity of the asset. Or it can be obtained by matching for the same +commodities with a set of assets. + +Production methods can be registered via the :py:func:`@register_production +` production decorator. Registering a function makes the function +accessible from MUSE's input file. Production methods are not expected to modify their +arguments. Furthermore they should conform the +following signatures: + +.. code-block:: python + + @register_production + def production( + market: xr.Dataset, capacity: xr.DataArray, technologies: xr.Dataset, **kwargs + ) -> xr.DataArray: + pass + + +Arguments: + market: Market, including demand and prices. + capacity: The capacity of each asset within a market. + technologies: A dataset characterising the technologies of the same assets. + **kwargs: Any number of keyword arguments + +Returns: + A `xr.DataArray` with the amount produced for each good from each asset. +""" + +__all__ = [ + "factory", + "maximum_production", + "register_production", + "supply", + "PRODUCTION_SIGNATURE", +] +from collections.abc import Mapping, MutableMapping +from typing import Any, Callable, Union, cast + +import xarray as xr + +from muse.registration import registrator + +PRODUCTION_SIGNATURE = Callable[[xr.DataArray, xr.DataArray, xr.Dataset], xr.DataArray] +"""Production signature.""" + +PRODUCTION_METHODS: MutableMapping[str, PRODUCTION_SIGNATURE] = {} +"""Dictionary of production methods. """ + + +@registrator(registry=PRODUCTION_METHODS, loglevel="info") +def register_production(function: PRODUCTION_SIGNATURE = None): + """Decorator to register a function as a production method. + + .. seealso:: + + :py:mod:`muse.production` + """ + return function + + +def factory( + settings: Union[str, Mapping] = "maximum_production", **kwargs +) -> PRODUCTION_SIGNATURE: + """Creates a production functor. + + This function's raison d'ĂȘtre is to convert the input from a TOML file into an + actual functor usable within the model, i.e. it converts data into logic. + + Arguments: + settings: Registered production method to create. The name is resolved when the + function returned by the factory is called. Hence, it could refer to a + function yet to be registered when this factory method is called. + **kwargs: any keyword argument the production method accepts. + """ + from functools import partial + + from muse.production import PRODUCTION_METHODS + + if isinstance(settings, str): + name = settings + keywords: MutableMapping[str, Any] = dict() + else: + keywords = dict(**settings) + name = keywords.pop("name") + + keywords.update(**kwargs) + name = keywords.pop("name", name) + + method = PRODUCTION_METHODS[name] + return cast( + PRODUCTION_SIGNATURE, method if not keywords else partial(method, **keywords) + ) + + +@register_production(name=("max", "maximum")) +def maximum_production( + market: xr.Dataset, capacity: xr.DataArray, technologies: xr.Dataset +) -> xr.DataArray: + """Production when running at full capacity. + + *Full capacity* is limited by the utilization factor. For more details, see + :py:func:`muse.quantities.maximum_production`. + """ + from muse.quantities import maximum_production + + return maximum_production(technologies, capacity, timeslices=market.timeslice) + + +@register_production(name=("share", "shares")) +def supply( + market: xr.Dataset, capacity: xr.DataArray, technologies: xr.Dataset +) -> xr.DataArray: + """Service current demand equally from all assets. + + "Equally" means that equivalent technologies are used to the same percentage of + their respective capacity. + """ + from muse.quantities import supply + + return supply(capacity, market.consumption, technologies) diff --git a/src/muse/sectors/sector.py b/src/muse/sectors/sector.py index 2b90dc808..8d0f082ff 100644 --- a/src/muse/sectors/sector.py +++ b/src/muse/sectors/sector.py @@ -11,6 +11,7 @@ import xarray as xr from muse.agents import AbstractAgent +from muse.production import PRODUCTION_SIGNATURE from muse.sectors.abstract import AbstractSector from muse.sectors.register import register_sector from muse.sectors.subsector import Subsector @@ -24,8 +25,10 @@ class Sector(AbstractSector): # type: ignore def factory(cls, name: str, settings: Any) -> Sector: from muse.interactions import factory as interaction_factory from muse.outputs.sector import factory as ofactory + from muse.production import factory as pfactory from muse.readers import read_timeslices from muse.readers.toml import read_technodata + from muse.utilities import nametuple_to_dict # Read sector settings sector_settings = getattr(settings.sectors, name)._asdict() @@ -68,6 +71,15 @@ def factory(cls, name: str, settings: Any) -> Sector: # Create outputs outputs = ofactory(*sector_settings.pop("outputs", []), sector_name=name) + supply_args = sector_settings.pop( + "supply", sector_settings.pop("dispatch_production", {}) + ) + if isinstance(supply_args, str): + supply_args = {"name": supply_args} + else: + supply_args = nametuple_to_dict(supply_args) + supply = pfactory(**supply_args) + # Create interactions interactions = interaction_factory(sector_settings.pop("interactions", None)) @@ -77,7 +89,6 @@ def factory(cls, name: str, settings: Any) -> Sector: "commodities_out", "commodities_in", "technodata_timeslices", - "dispatch_production", # old parameter which may still exist in the file ): sector_settings.pop(attr, None) return cls( @@ -85,6 +96,7 @@ def factory(cls, name: str, settings: Any) -> Sector: technologies, subsectors=subsectors, timeslices=timeslices, + supply_prod=supply, outputs=outputs, interactions=interactions, **sector_settings, @@ -99,9 +111,11 @@ def __init__( interactions: Callable[[Sequence[AbstractAgent]], None] | None = None, interpolation: str = "linear", outputs: Callable | None = None, + supply_prod: PRODUCTION_SIGNATURE | None = None, ): from muse.interactions import factory as interaction_factory from muse.outputs.sector import factory as ofactory + from muse.production import maximum_production self.name: str = name """Name of the sector.""" @@ -141,6 +155,14 @@ def __init__( cast(Callable, ofactory()) if outputs is None else outputs ) """A function for outputting data for post-mortem analysis.""" + self.supply_prod = ( + supply_prod if supply_prod is not None else maximum_production + ) + """ Computes production as used to return the supply to the MCA. + + It can be anything registered with + :py:func:`@register_production`. + """ self.output_data: xr.Dataset """Full supply, consumption and costs data for the most recent year.""" @@ -260,7 +282,7 @@ def market_variables(self, market: xr.Dataset, technologies: xr.Dataset) -> Any: """Computes resulting market: production, consumption, and costs.""" from muse.commodities import is_pollutant from muse.costs import annual_levelized_cost_of_energy, supply_cost - from muse.quantities import consumption, supply + from muse.quantities import consumption from muse.timeslices import QuantityType, convert_timeslice from muse.utilities import broadcast_techs @@ -268,24 +290,26 @@ def market_variables(self, market: xr.Dataset, technologies: xr.Dataset) -> Any: capacity = self.capacity.interp(year=years, **self.interpolation) # Calculate supply - sup = supply(capacity, market.consumption, technologies) - if "timeslice" in market.prices.dims and "timeslice" not in sup.dims: - sup = convert_timeslice(sup, market.timeslice, QuantityType.EXTENSIVE) + supply = self.supply_prod( + market=market, capacity=capacity, technologies=technologies + ) + if "timeslice" in market.prices.dims and "timeslice" not in supply.dims: + supply = convert_timeslice(supply, market.timeslice, QuantityType.EXTENSIVE) # Calculate consumption - consume = consumption(technologies, sup, market.prices) + consume = consumption(technologies, supply, market.prices) # Calculate commodity prices - technodata = cast(xr.Dataset, broadcast_techs(technologies, sup)) + technodata = cast(xr.Dataset, broadcast_techs(technologies, supply)) costs = supply_cost( - sup.where(~is_pollutant(sup.comm_usage), 0), + supply.where(~is_pollutant(supply.comm_usage), 0), annual_levelized_cost_of_energy( - prices=market.prices.sel(region=sup.region), technologies=technodata + prices=market.prices.sel(region=supply.region), technologies=technodata ), asset_dim="asset", ) - return sup, consume, costs + return supply, consume, costs @property def capacity(self) -> xr.DataArray: From a620ffb656132e8cf932cead570d4166c3b34fb1 Mon Sep 17 00:00:00 2001 From: Tom Bland Date: Mon, 11 Nov 2024 16:23:37 +0000 Subject: [PATCH 6/6] Fix alignment in documentatioh --- docs/inputs/toml.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/inputs/toml.rst b/docs/inputs/toml.rst index 198381bc8..efeccb15d 100644 --- a/docs/inputs/toml.rst +++ b/docs/inputs/toml.rst @@ -338,10 +338,10 @@ A sector accepts these attributes: the technology's utilization factor. See :py:func:`muse.production.maximum_production`. - Defaults to "share". + Defaults to "share". - Additional methods can be registered with - :py:func:`muse.production.register_production` + Additional methods can be registered with + :py:func:`muse.production.register_production` Sectors contain a number of subsections: *interactions*