Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 2 additions & 4 deletions docs/application-flow.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
34 changes: 7 additions & 27 deletions docs/inputs/toml.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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*
Expand Down
45 changes: 16 additions & 29 deletions src/muse/demand_share.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand All @@ -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

Expand Down Expand Up @@ -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<muse.production.register_production>`.
:math:`P` is the maximum production, given by
<muse.quantities.maximum_production>`.


#. the *new* demand :math:`N` is defined as:
Expand Down Expand Up @@ -245,7 +243,6 @@ def decommissioning(capacity):
capacity,
market,
technologies,
production=production,
current_year=current_year,
forecast=forecast,
)
Expand Down Expand Up @@ -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.

Expand All @@ -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

Expand Down Expand Up @@ -381,7 +376,6 @@ def decommissioning(capacity):
capacity,
market,
technologies,
production=production,
current_year=current_year,
forecast=forecast,
)
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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.

Expand All @@ -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<muse.production.register_production>`.
:math:`P` is the maximum production, given by <muse.quantities.maximum_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 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:
Expand Down Expand Up @@ -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
<muse.quantities.maximum_production>.
"""
from numpy import minimum

Expand All @@ -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.

Expand All @@ -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])
Expand All @@ -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")
Expand Down
11 changes: 2 additions & 9 deletions src/muse/quantities.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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.

Expand All @@ -36,19 +34,14 @@ 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
input commodities).
"""
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
Expand Down
9 changes: 5 additions & 4 deletions tests/test_demand_share.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down