diff --git a/docs/inputs/toml.rst b/docs/inputs/toml.rst index 249050bc..661de28d 100644 --- a/docs/inputs/toml.rst +++ b/docs/inputs/toml.rst @@ -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: @@ -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` @@ -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`. @@ -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. @@ -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 @@ -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 @@ -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: @@ -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: @@ -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: @@ -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 @@ -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. @@ -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`. *Preset* sectors are those with type "presets". -*priority* +*priority* (required) See the attribute in the standard mode, :ref:`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`. diff --git a/src/muse/production.py b/src/muse/production.py index b19dbdbd..c6eaea4a 100644 --- a/src/muse/production.py +++ b/src/muse/production.py @@ -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:: @@ -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")) diff --git a/src/muse/sectors/sector.py b/src/muse/sectors/sector.py index 00d2532a..e0347b0f 100644 --- a/src/muse/sectors/sector.py +++ b/src/muse/sectors/sector.py @@ -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() @@ -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)) @@ -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, @@ -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.""" @@ -164,7 +157,7 @@ def __init__( It can be anything registered with :py:func:`@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