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
119 changes: 57 additions & 62 deletions docs/inputs/toml.rst
Original file line number Diff line number Diff line change
Expand Up @@ -270,9 +270,10 @@ A sector accepts these attributes:
.. _sector-type:

*type*
Defines the kind of sector this is. *Standard* sectors are those with type
"default". This value corresponds to the name with which a sector class is registered
with MUSE, via :py:meth:`~muse.sectors.register_sector`. [INSERT OTHER OPTIONS HERE]
Defines the kind of sector this is. There are two options:

* "default": defines a standard sector
* "presets": defines a preset sector (see below)

.. _sector-priority:

Expand All @@ -291,25 +292,20 @@ A sector accepts these attributes:

Defaults to "last".

*interpolation*
Interpolation method used to fill missing years in the *technodata* (defaults to "linear").
*interpolation* (optional, default = "linear")
Interpolation method used to fill missing years in the *technodata*.
Available interpolation methods depend on the underlying `scipy method's kind attribute`_.
Years outside the data range will always be back/forward filled with the closest available data.

.. _scipy method's kind attribute: https://docs.scipy.org/doc/scipy/reference/generated/scipy.interpolate.interp1d.html

*dispatch_production*
*dispatch_production* (optional, default = "share")
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".
* 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`.

Additional methods can be registered with
:py:func:`muse.production.register_production`
Expand All @@ -318,8 +314,8 @@ A sector accepts these attributes:
Path to a csv file containing the characterization of the technologies involved in
the sector, e.g. lifetime, capital costs, etc... See :ref:`inputs-technodata`.

*technodata_timeslices*
Optional. Path to a csv file describing the utilization factor and minimum service
*technodata_timeslices* (optional)
Path to a csv file describing the utilization factor and minimum service
factor of each technology in each timeslice.
See :ref:`user_guide/inputs/technodata_timeslices`.

Expand All @@ -331,8 +327,8 @@ A sector accepts these attributes:
Path to a csv file describing the outputs of each technology involved in the sector.
See :ref:`inputs-iocomms`.

*timeslice_level*
Optional. This represents the level of timeslice granularity over which commodity
*timeslice_level* (optional)
This represents the level of timeslice granularity over which commodity
flows out of the sector are balanced (e.g. if "day", the sector will aim to meet
commodity demands on a daily basis, rather than an hourly basis).
If not given, defaults to the finest level defined in the global `timeslices` section.
Expand All @@ -341,7 +337,8 @@ A sector accepts these attributes:
the timeslice level, then *technodata_timeslices* must have columns "month" and "day", but not "hour")

Sectors contain a number of subsections:
*interactions*

*interactions* (optional)
Defines interactions between agents. These interactions take place right before new
investments are computed. The interactions can be anything. They are expected to
modify the agents and their assets. MUSE provides a default set of interactions that
Expand Down Expand Up @@ -393,11 +390,9 @@ Sectors contain a number of subsections:
"new_to_retro" type of network has been defined but no retro agents are included in
the sector.


*subsectors*

Subsectors group together agents into separate groups servicing the demand for
different commodities. There should be at least one subsector. And there can be as
different commodities. There must be at least one subsector, and there can be as
many as required. For instance, a one-subsector setup would look like:

.. code-block:: toml
Expand Down Expand Up @@ -432,7 +427,7 @@ Sectors contain a number of subsections:
Path to a csv file describing the initial capacity of the sector.
See :ref:`user_guide/inputs/existing_capacity:existing sectoral capacity`.

*lpsolver*
*lpsolver* (optional, default = "scipy")
The solver for linear problems to use when figuring out investments. The solvers
are registered via :py:func:`~muse.investments.register_investment`. At time of
writing, three are available:
Expand All @@ -443,11 +438,7 @@ Sectors contain a number of subsections:
- an "adhoc" solver: Simple in-house solver that ranks the technologies
according to cost and service the demand incrementally.

- "cvxopt" solver: Formulates investment as a true LP problem and solves it
using the python package `cvxopt`_. `cvxopt`_ is *not* installed by default.
Users can install it with ``pip install cvxopt`` or ``conda install cvxopt``.

*demand_share*
*demand_share* (optional, default = "standard_demand")
A method used to split the MCA demand into separate parts to be serviced by
specific agents. The appropriate choice depends on the type of agents being used
in the simulation. There are currently two options:
Expand All @@ -461,7 +452,7 @@ Sectors contain a number of subsections:
demand over the investment period, whereas *retrofit* agents are assigned a share
of the demand that occurs from decommissioned assets.

*constraints*
*constraints* (optional, defaults to full list)
The list of constraints to apply to the LP problem solved by the sector. By
default all of the following are included:

Expand Down Expand Up @@ -498,21 +489,22 @@ Sectors contain a number of subsections:

The following attributes are available:

- *quantity*: Name of the quantity to save. Currently, `capacity` exists,
referring to :py:func:`muse.outputs.capacity`. However, users can
customize and create further output quantities by registering with MUSE via
:py:func:`muse.outputs.register_output_quantity`. See
:py:mod:`muse.outputs` for more details.
* *quantity*:
Name of the quantity to save.
The options are capacity, consumption, supply and costs.
Users can also customize and create further output quantities by registering with MUSE via
:py:func:`muse.outputs.register_output_quantity`. See :py:mod:`muse.outputs` for more details.

- *sink*: the sink is the place (disk, cloud, database, etc...) and format with which
* *sink*:
the sink is the place (disk, cloud, database, etc...) and format with which
the computed quantity is saved. Currently only sinks that save to files are
implemented. The filename can specified via `filename`, as given below. The
following sinks are available: "csv", "netcfd", "excel". However, more sinks can
be added by interested users, and registered with MUSE via
:py:func:`muse.outputs.register_output_sink`. See
:py:mod:`muse.outputs` for more details.
implemented.
The following sinks are available: "csv", "netcfd", "excel" and "aggregate".
Additional sinks can be added by interested users, and registered with MUSE via
:py:func:`muse.outputs.register_output_sink`. See :py:mod:`muse.outputs` for more details.

- *filename*: defines the format of the file where to save the data. There are several
* *filename*:
defines the format of the file where to save the data. There are several
standard values that are automatically substituted:

- cwd: current working directory, where MUSE was started
Expand All @@ -526,25 +518,29 @@ Sectors contain a number of subsections:

Defaults to `{cwd}/{default_output_dir}/{Sector}/{Quantity}/{year}{suffix}`.

- *overwrite*: If `False` MUSE will issue an error and abort, instead of
* *overwrite*:
If `False` MUSE will issue an error and abort, instead of
overwriting an existing file. Defaults to `False`. This prevents important output files from being overwritten.
There is a special output sink for aggregating over years. It can be invoked as
follows:

For example, the following would save supply data for the commercial sector as a separate file for each year:

.. code-block:: TOML

[[sectors.commercial.outputs]]
quantity = "capacity"
sink.aggregate = 'csv'
quantity = "supply"
sink = "csv"
filename = "{cwd}/{default_output_dir}/{Sector}/{Quantity}/{year}{suffix}"
overwrite = true

Or, if specifying additional output, where ... can be any parameter for the final
sink:
There is a special output sink for aggregating over years (i.e. a single output file for all years). It can be invoked as
follows:

.. code-block:: TOML

[[sectors.commercial.outputs]]
quantity = "capacity"
sink.aggregate.name = { ... }
quantity = "supply"
sink = "aggregate"
filename = "{cwd}/{default_output_dir}/{Sector}/{Quantity}.csv"

Note that the aggregate sink always overwrites the final file, since it will
overwrite itself.
Expand All @@ -559,40 +555,39 @@ simulation.

Preset sectors are defined in :py:class:`~muse.sectors.PresetSector`.

The three components, production, consumption, and prices, can be set independently and
not all three need to be set. Production and consumption default to zero, and prices
default to leaving things unchanged.
A common example would be the following, where commodity consumption is defined exogeneously:

.. code-block:: TOML

The following defines a standard preset sector where consumption is defined as a
function of macro-economic data, i.e. population and gdp.
[sectors.commercial_presets]
type = 'presets'
priority = 0
consumption_path = "{path}/technodata/preset/*Consumption.csv"

Alternatively, you may define consumption as a function of macro-economic data, i.e. population and GDP:

.. code-block:: TOML

[sectors.commercial_presets]
type = 'presets'
priority = 'presets'
priority = 0
timeslice_shares_path = '{path}/technodata/TimesliceShareCommercial.csv'
macrodrivers_path = '{path}/technodata/Macrodrivers.csv'
regression_path = '{path}/technodata/regressionparameters.csv'
timeslices_levels = {'day': ['all-day']}

The following attributes are accepted:

*type*
*type* (required)
See the attribute in the standard mode, :ref:`type<sector-type>`. *Preset* sectors
are those with type "presets".

*priority*
*priority* (required)
See the attribute in the standard mode, :ref:`priority<sector-priority>`.

*timeslices_levels*
See the attribute in the standard mode, `Timeslices`_.

.. _preset-consumption:

*consumption_path*
CSV output files, one per year. This attribute can include wild cards, i.e. '*',
CSV files, one per year. This attribute can include wild cards, i.e. '*',
which can match anything. For instance: `consumption_path = "{cwd}/Consumption*.csv"` will match any csv file starting with "Consumption" in the
current working directory. The file names must include the year for which it defines
the consumption, e.g. `Consumption2015.csv`.
Expand Down
44 changes: 9 additions & 35 deletions src/muse/production.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,22 +40,24 @@ def production(
"supply",
]

from collections.abc import Mapping, MutableMapping
from typing import Any, Callable, cast
from collections.abc import MutableMapping
from typing import Callable

import xarray as xr

from muse.registration import registrator

PRODUCTION_SIGNATURE = Callable[[xr.DataArray, xr.DataArray, xr.Dataset], xr.DataArray]
"""Production signature."""
PRODUCTION_SIGNATURE = Callable[
[xr.Dataset, xr.DataArray, xr.Dataset, str], xr.DataArray
]

PRODUCTION_METHODS: MutableMapping[str, PRODUCTION_SIGNATURE] = {}
"""Dictionary of production methods. """
PRODUCTION_METHODS: MutableMapping[str, PRODUCTION_SIGNATURE] = {}


@registrator(registry=PRODUCTION_METHODS, loglevel="info")
def register_production(function: PRODUCTION_SIGNATURE = None):
def register_production(function: PRODUCTION_SIGNATURE):
"""Decorator to register a function as a production method.

.. seealso::
Expand All @@ -65,38 +67,10 @@ def register_production(function: PRODUCTION_SIGNATURE = None):
return function


def factory(
settings: 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

def factory(name) -> PRODUCTION_SIGNATURE:
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)
)
return PRODUCTION_METHODS[name]


@register_production(name=("max", "maximum"))
Expand Down
19 changes: 6 additions & 13 deletions src/muse/sectors/sector.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ def factory(cls, name: str, settings: Any) -> Sector:
from muse.outputs.sector import factory as ofactory
from muse.production import factory as pfactory
from muse.readers.toml import read_technodata
from muse.utilities import nametuple_to_dict

# Read sector settings
sector_settings = getattr(settings.sectors, name)._asdict()
Expand Down Expand Up @@ -72,14 +71,9 @@ 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 production method
dispatch_production = sector_settings.pop("dispatch_production", "share")
production = pfactory(dispatch_production)

# Create interactions
interactions = interaction_factory(sector_settings.pop("interactions", None))
Expand All @@ -95,8 +89,8 @@ def factory(cls, name: str, settings: Any) -> Sector:
return cls(
name,
technologies,
supply_prod=production,
subsectors=subsectors,
supply_prod=supply,
outputs=outputs,
interactions=interactions,
**sector_settings,
Expand All @@ -106,15 +100,14 @@ def __init__(
self,
name: str,
technologies: xr.Dataset,
supply_prod: PRODUCTION_SIGNATURE,
subsectors: Sequence[Subsector] = [],
interactions: Callable[[Sequence[AbstractAgent]], None] | None = None,
outputs: Callable | None = None,
supply_prod: PRODUCTION_SIGNATURE | None = None,
timeslice_level: str | None = None,
):
from muse.interactions import factory as interaction_factory
from muse.outputs.sector import factory as ofactory
from muse.production import maximum_production
from muse.timeslices import TIMESLICE

"""Name of the sector."""
Expand Down Expand Up @@ -164,7 +157,7 @@ def __init__(
It can be anything registered with
:py:func:`@register_production<muse.production.register_production>`.
"""
self.supply_prod = supply_prod or maximum_production
self.supply_prod = supply_prod

"""Full supply, consumption and costs data for the most recent year."""
self.output_data: xr.Dataset
Expand Down