diff --git a/.bumpversion.cfg b/.bumpversion.cfg index 443e9b36a..1d8064b96 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 1.2.2 +current_version = 1.3.0 commit = True tag = True diff --git a/CITATION.cff b/CITATION.cff index 549e0878a..0305dba21 100644 --- a/CITATION.cff +++ b/CITATION.cff @@ -9,5 +9,5 @@ authors: given-names: Adam title: MUSE_OS -version: v1.2.2 +version: v1.3.0 date-released: 2024-08-13 diff --git a/docs/conf.py b/docs/conf.py index 661fa8904..73b87e6bf 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -8,7 +8,7 @@ project = "MUSE" copyright = "2024, Imperial College London" author = "Imperial College London" -release = "1.2.2" +release = "1.3.0" version = ".".join(release.split(".")[:2]) # -- General configuration --------------------------------------------------- diff --git a/docs/inputs/toml.rst b/docs/inputs/toml.rst index efeccb15d..6cadb7b8f 100644 --- a/docs/inputs/toml.rst +++ b/docs/inputs/toml.rst @@ -236,30 +236,6 @@ levels. For instance, there no ``peak`` periods during weekends. All that matter that the relative weights (i.e. the number of hours) are consistent and sum up to a year. -The input above defines the finest times slice in the code. In order to define rougher -timeslices we can introduce items in each levels that represent aggregates at that -level. By default, we have the following: - -.. code-block:: TOML - - [timeslices.aggregates] - all-day = ["night", "morning", "afternoon", "early-peak", "late-peak", "evening"] - all-week = ["weekday", "weekend"] - all-year = ["winter", "summer", "spring-autumn"] - -Here, ``all-day`` aggregates the full day. However, one could potentially create -aggregates such as: - -.. code-block:: TOML - - [timeslices.aggregates] - daylight = ["morning", "afternoon", "early-peak", "late-peak"] - nightlife = ["evening", "night"] - - It is possible to specify a timeslice level for the mca by adding an -`mca.timeslice_levels` section, using an inline table format. -See section on `Timeslices_`. - *outputs_cache* This option behaves exactly like `outputs` for sectors and accepts the same options but controls the output of cached quantities instead. This option is NOT available for @@ -423,30 +399,6 @@ Sectors contain a number of subsections: Path to a csv file describing the outputs of each technology involved in the sector. See :ref:`inputs-iocomms`. - Once the finest timeslice and its aggregates are given, it is possible for each sector -to define the timeslice simply by referring to the slices it will use at each level. - -.. _sector-timeslices: - -*timeslice_levels* - Optional. These define the timeslices of a sector. If not specified, the finest timeslice levels will be used - (See `Timeslices`_). - It can be implemented with the following rows: - -.. code-block:: TOML - - [sectors.some_sector.timeslice_levels] - day = ["daylight", "nightlife"] - month = ["all-year"] - - Above, ``sectors.some_sector.timeslice_levels.week`` defaults its value in the finest - timeslice. Indeed, if the subsection ``sectors.some_sector.timeslice_levels`` is not - given, then the sector will default to using the finest timeslices. - - If the MCA uses a rougher - timeslice framework, the market will be expressed within it. Hence information from - sectors with a finer timeslice framework will be lost. - *subsectors* Subsectors group together agents into separate groups servicing the demand for @@ -711,58 +663,3 @@ The following attributes are accepted: filters.region = ["USA", "ASEA"] filters.commodity = ["algae", "fluorescent light"] - - --------------- -Legacy Sectors --------------- - -Legacy sectors wrap sectors developed for a previous version of MUSE to the open-source -version. - -Preset sectors are defined in :py:class:`~muse.sectors.PresetSector`. - -The can be defined in the TOML file as follows: - -.. code-block:: TOML - - [global_input_files] - macrodrivers = '{path}/input/Macrodrivers.csv' - regions = '{path}/input/Regions.csv' - global_commodities = '{path}/input/MUSEGlobalCommodities.csv' - - [sectors.Industry] - type = 'legacy' - priority = 'demand' - agregation_level = 'month' - excess = 0 - - userdata_path = '{muse_sectors}/Industry' - technodata_path = '{muse_sectors}/Industry' - timeslices_path = '{muse_sectors}/Industry/TimeslicesIndustry.csv' - output_path = '{path}/output' - -For historical reasons, the three `global_input_files` above are required. The sector -itself can use the following attributes. - -*type* - See the attribute in the standard mode, :ref:`type`. *Legacy* sectors - are those with type "legacy". - -*priority* - See the attribute in the standard mode, :ref:`priority`. - -*agregation_level* - Information relevant to the sector's timeslice. - -*excess* - Excess factor used to model early obsolescence. - -*userdata_path* - Path to a directory with sector-specific data files. - -*technodata_path* - Path to a technodata CSV file. See. :ref:`inputs-technodata`. - -*output_path* - Path to a directory where the sector will write output files. diff --git a/src/muse/__init__.py b/src/muse/__init__.py index b57e66ac1..c53ec7402 100644 --- a/src/muse/__init__.py +++ b/src/muse/__init__.py @@ -2,7 +2,7 @@ import os -VERSION = "1.2.2" +VERSION = "1.3.0" def _create_logger(color: bool = True): @@ -89,7 +89,6 @@ def add_file_logger() -> None: "read_technodictionary", "read_technologies", "read_timeslice_shares", - "read_csv_timeslices", "read_settings", "read_macro_drivers", "read_csv_agent_parameters", diff --git a/src/muse/__main__.py b/src/muse/__main__.py index 6108b0b8a..b7386dc06 100644 --- a/src/muse/__main__.py +++ b/src/muse/__main__.py @@ -62,5 +62,51 @@ def run(): muse_main(args.settings, args.model, args.copy) +def patched_broadcast_compat_data(self, other): + """Patch for xarray.core.variable._broadcast_compat_data. + + This has been introduced to disallow automatic broadcasting along the 'timeslice' + dimension. + + If `self` and `other` differ in whether they have a 'timeslice' dimension (in which + case automatic broadcasting would normally be performed), an error is raised. + + In this case, developers must explicitly handle broadcasting by calling either + `broadcast_timeslice` or `distribute_timeslice` (see `muse.timeslices`). The + appropriate choice of operation will depend on the context and the quantity in + question. + """ + from xarray.core.variable import Variable, _broadcast_compat_variables + + if (isinstance(other, Variable)) and ("timeslice" in self.dims) != ( + "timeslice" in getattr(other, "dims", []) + ): + raise ValueError( + "Broadcasting along the 'timeslice' dimension is required, but automatic " + "broadcasting is disabled. Please handle it explicitly using " + "`broadcast_timeslice` or `distribute_timeslice` (see `muse.timeslices`)." + ) + + # The rest of the function is copied directly from + # xarray.core.variable._broadcast_compat_data + if all(hasattr(other, attr) for attr in ["dims", "data", "shape", "encoding"]): + # `other` satisfies the necessary Variable API for broadcast_variables + new_self, new_other = _broadcast_compat_variables(self, other) + self_data = new_self.data + other_data = new_other.data + dims = new_self.dims + else: + # rely on numpy broadcasting rules + self_data = self.data + other_data = other + dims = self.dims + return self_data, other_data, dims + + if "__main__" == __name__: - run() + from unittest.mock import patch + + with patch( + "xarray.core.variable._broadcast_compat_data", patched_broadcast_compat_data + ): + run() diff --git a/src/muse/constraints.py b/src/muse/constraints.py index 40026e4a2..bb261a0ae 100644 --- a/src/muse/constraints.py +++ b/src/muse/constraints.py @@ -446,7 +446,7 @@ def max_production( from xarray import ones_like, zeros_like from muse.commodities import is_enduse - from muse.timeslices import QuantityType, convert_timeslice + from muse.timeslices import broadcast_timeslice, distribute_timeslice if year is None: year = int(market.year.min()) @@ -465,15 +465,9 @@ def max_production( .sel(**kwargs) .drop_vars("technology") ) - capacity = ( - convert_timeslice( - techs.fixed_outputs, - market.timeslice, - QuantityType.EXTENSIVE, - ) - * techs.utilization_factor + capacity = distribute_timeslice(techs.fixed_outputs) * broadcast_timeslice( + techs.utilization_factor ) - if "asset" not in capacity.dims and "asset" in search_space.dims: capacity = capacity.expand_dims(asset=search_space.asset) production = ones_like(capacity) @@ -490,8 +484,8 @@ def max_production( maxadd = maxadd.rename(technology="replacement") maxadd = maxadd.where(maxadd == 0, 0.0) maxadd = maxadd.where(maxadd > 0, -1.0) - capacity = capacity * maxadd - production = production * maxadd + capacity = capacity * broadcast_timeslice(maxadd) + production = production * broadcast_timeslice(maxadd) b = b.rename(region="src_region") return xr.Dataset( dict(capacity=-cast(np.ndarray, capacity), production=production, b=b), @@ -542,21 +536,9 @@ def demand_limiting_capacity( # utilization factor. if "timeslice" in b.dims or "timeslice" in capacity.dims: ratio = b / capacity - ts = ratio.timeslice.isel( - timeslice=ratio.min("replacement").argmax("timeslice") - ) - # We select this timeslice for each array - don't trust the indices: - # search for the right timeslice in the array and select it. - b = ( - b.isel(timeslice=(b.timeslice == ts).argmax("timeslice")) - if "timeslice" in b.dims - else b - ) - capacity = ( - capacity.isel(timeslice=(capacity.timeslice == ts).argmax("timeslice")) - if "timeslice" in capacity.dims - else capacity - ) + ts_index = ratio.min("replacement").argmax("timeslice") + b = b.isel(timeslice=ts_index) + capacity = capacity.isel(timeslice=ts_index) # An adjustment is required to account for technologies that have multiple output # commodities @@ -732,7 +714,7 @@ def minimum_service( from xarray import ones_like, zeros_like from muse.commodities import is_enduse - from muse.timeslices import QuantityType, convert_timeslice + from muse.timeslices import broadcast_timeslice, distribute_timeslice if "minimum_service_factor" not in technologies.data_vars: return None @@ -751,17 +733,12 @@ def minimum_service( if "region" in search_space.coords and "region" in technologies.dims: kwargs["region"] = assets.region techs = ( - technologies[["fixed_outputs", "utilization_factor", "minimum_service_factor"]] + technologies[["fixed_outputs", "minimum_service_factor"]] .sel(**kwargs) .drop_vars("technology") ) - capacity = ( - convert_timeslice( - techs.fixed_outputs, - market.timeslice, - QuantityType.EXTENSIVE, - ) - * techs.minimum_service_factor + capacity = distribute_timeslice(techs.fixed_outputs) * broadcast_timeslice( + techs.minimum_service_factor ) if "asset" not in capacity.dims: capacity = capacity.expand_dims(asset=search_space.asset) @@ -773,9 +750,7 @@ def minimum_service( ) -def lp_costs( - technologies: xr.Dataset, costs: xr.DataArray, timeslices: xr.DataArray -) -> xr.Dataset: +def lp_costs(technologies: xr.Dataset, costs: xr.DataArray) -> xr.Dataset: """Creates costs for solving with scipy's LP solver. Example: @@ -785,7 +760,6 @@ def lp_costs( >>> from muse import examples >>> technologies = examples.technodata("residential", model="medium") >>> search_space = examples.search_space("residential", model="medium") - >>> timeslices = examples.sector("residential", model="medium").timeslices >>> costs = ( ... search_space ... * np.arange(np.prod(search_space.shape)).reshape(search_space.shape) @@ -796,7 +770,7 @@ def lp_costs( >>> from muse.constraints import lp_costs >>> lpcosts = lp_costs( - ... technologies.sel(year=2020, region="R1"), costs, timeslices + ... technologies.sel(year=2020, region="R1"), costs ... ) >>> assert "capacity" in lpcosts.data_vars >>> assert "production" in lpcosts.data_vars @@ -826,29 +800,20 @@ def lp_costs( from xarray import zeros_like from muse.commodities import is_enduse - from muse.timeslices import convert_timeslice + from muse.timeslices import broadcast_timeslice, distribute_timeslice assert "year" not in technologies.dims - ts_costs = convert_timeslice(costs, timeslices) selection = dict( commodity=is_enduse(technologies.comm_usage), technology=technologies.technology.isin(costs.replacement), ) - if "region" in technologies.fixed_outputs.dims and "region" in ts_costs.coords: - selection["region"] = ts_costs.region + if "region" in technologies.fixed_outputs.dims and "region" in costs.coords: + selection["region"] = costs.region fouts = technologies.fixed_outputs.sel(selection).rename(technology="replacement") - # lpcosts.dims = Frozen({'asset': 2, - # 'replacement': 2, - # 'timeslice': 3, - # 'commodity': 1}) - # muse38: lpcosts.dims = Frozen({'asset': 2, , - # 'commodity': 1 - # 'replacement': 2, - # 'timeslice': 3}) - production = zeros_like(ts_costs * fouts) + production = zeros_like(broadcast_timeslice(costs) * distribute_timeslice(fouts)) for dim in production.dims: if isinstance(production.get_index(dim), pd.MultiIndex): production = drop_timeslice(production) @@ -978,7 +943,6 @@ def lp_constraint_matrix( ... .sel(region=assets.region) ... ), ... costs=search * np.arange(np.prod(search.shape)).reshape(search.shape), - ... timeslices=market.timeslice, ... ) For a simple example, we can first check the case where b is scalar. The result @@ -1098,7 +1062,6 @@ class ScipyAdapter: >>> from muse import examples >>> from muse.quantities import maximum_production - >>> from muse.timeslices import convert_timeslice >>> from muse import constraints as cs >>> res = examples.sector("residential", model="medium") >>> market = examples.residential_market("medium") @@ -1107,7 +1070,6 @@ class ScipyAdapter: >>> market_demand = 0.8 * maximum_production( ... res.technologies.interp(year=2025), ... assets.capacity.sel(year=2025).groupby("technology").sum("asset"), - ... timeslices=market.timeslice, ... ).rename(technology="asset") >>> costs = search * np.arange(np.prod(search.shape)).reshape(search.shape) >>> constraint = cs.max_capacity_expansion( @@ -1143,7 +1105,7 @@ class ScipyAdapter: >>> technologies = res.technologies.interp(year=market.year.min() + 5) >>> inputs = cs.ScipyAdapter.factory( - ... technologies, costs, market.timeslice, constraint + ... technologies, costs, constraint ... ) The decision variables are always constrained between zero and infinity: @@ -1168,7 +1130,7 @@ class ScipyAdapter: In practice, :py:func:`~muse.constraints.lp_costs` helps us define the decision variables (and ``c``). We can verify that the sizes are consistent: - >>> lpcosts = cs.lp_costs(technologies, costs, market.timeslice) + >>> lpcosts = cs.lp_costs(technologies, costs) >>> capsize = lpcosts.capacity.size >>> prodsize = lpcosts.production.size >>> assert inputs.c.size == capsize + prodsize @@ -1203,10 +1165,9 @@ def factory( cls, technologies: xr.Dataset, costs: xr.DataArray, - timeslices: pd.Index, *constraints: Constraint, ) -> ScipyAdapter: - lpcosts = lp_costs(technologies, costs, timeslices) + lpcosts = lp_costs(technologies, costs) data = cls._unified_dataset(technologies, lpcosts, *constraints) diff --git a/src/muse/costs.py b/src/muse/costs.py index 18c78bd83..5350547ba 100644 --- a/src/muse/costs.py +++ b/src/muse/costs.py @@ -13,7 +13,7 @@ from muse.commodities import is_enduse, is_fuel, is_material, is_pollutant from muse.quantities import consumption -from muse.timeslices import QuantityType, convert_timeslice +from muse.timeslices import broadcast_timeslice, distribute_timeslice from muse.utilities import filter_input @@ -79,10 +79,12 @@ def net_present_value( years = xr.DataArray(iyears, coords={"year": iyears}, dims="year") # Evolution of rates with time - rates = discount_factor( - years - year + 1, - interest_rate=techs.interest_rate, - mask=years <= year + life, + rates = broadcast_timeslice( + discount_factor( + years - year + 1, + interest_rate=techs.interest_rate, + mask=years <= year + life, + ) ) # Filters @@ -99,10 +101,8 @@ def net_present_value( raw_revenues = (production * prices_non_env * rates).sum(("commodity", "year")) # Cost of installed capacity - installed_capacity_costs = convert_timeslice( + installed_capacity_costs = distribute_timeslice( techs.cap_par * (capacity**techs.cap_exp), - prices.timeslice, - QuantityType.EXTENSIVE, ) # Cost related to environmental products @@ -123,19 +123,20 @@ def net_present_value( # Fixed costs fixed_costs = ( - convert_timeslice( - techs.fix_par * (capacity**techs.fix_exp), - prices.timeslice, - QuantityType.EXTENSIVE, - ) - * rates + distribute_timeslice(techs.fix_par * (capacity**techs.fix_exp)) * rates ).sum("year") # Variable costs - tech_activity = (production / techs.fixed_outputs).max("commodity") - variable_costs = ((techs.var_par * tech_activity**techs.var_exp) * rates).sum( - "year" + tech_activity = (production / broadcast_timeslice(techs.fixed_outputs)).max( + "commodity" ) + variable_costs = ( + ( + broadcast_timeslice(techs.var_par) + * tech_activity ** broadcast_timeslice(techs.var_exp) + ) + * rates + ).sum("year") # Net present value result = raw_revenues - ( @@ -208,7 +209,7 @@ def equivalent_annual_cost( """ npc = net_present_cost(technologies, prices, capacity, production, year) crf = capital_recovery_factor(technologies) - return npc * crf + return npc * broadcast_timeslice(crf) def lifetime_levelized_cost_of_energy( @@ -232,6 +233,8 @@ def lifetime_levelized_cost_of_energy( Return: xr.DataArray with the LCOE calculated for the relevant technologies """ + from muse.timeslices import broadcast_timeslice, distribute_timeslice + techs = technologies[ [ "technical_life", @@ -255,10 +258,12 @@ def lifetime_levelized_cost_of_energy( years = xr.DataArray(iyears, coords={"year": iyears}, dims="year") # Evolution of rates with time - rates = discount_factor( - years=years - year + 1, - interest_rate=techs.interest_rate, - mask=years <= year + life, + rates = broadcast_timeslice( + discount_factor( + years=years - year + 1, + interest_rate=techs.interest_rate, + mask=years <= year + life, + ) ) # Filters @@ -271,10 +276,8 @@ def lifetime_levelized_cost_of_energy( cons = consumption(technologies=techs, production=production, prices=prices) # Cost of installed capacity - installed_capacity_costs = convert_timeslice( + installed_capacity_costs = distribute_timeslice( techs.cap_par * (capacity**techs.cap_exp), - prices.timeslice, - QuantityType.EXTENSIVE, ) # Cost related to environmental products @@ -295,19 +298,20 @@ def lifetime_levelized_cost_of_energy( # Fixed costs fixed_costs = ( - convert_timeslice( - techs.fix_par * (capacity**techs.fix_exp), - prices.timeslice, - QuantityType.EXTENSIVE, - ) - * rates + distribute_timeslice(techs.fix_par * (capacity**techs.fix_exp)) * rates ).sum("year") # Variable costs - tech_activity = (production / techs.fixed_outputs).max("commodity") - variable_costs = ((techs.var_par * tech_activity**techs.var_exp) * rates).sum( - "year" + tech_activity = (production / broadcast_timeslice(techs.fixed_outputs)).max( + "commodity" ) + variable_costs = ( + ( + broadcast_timeslice(techs.var_par) + * tech_activity ** broadcast_timeslice(techs.var_exp) + ) + * rates + ).sum("year") # Production prod = ( @@ -397,58 +401,32 @@ def annual_levelized_cost_of_energy( rates = techs.interest_rate / (1 - (1 + techs.interest_rate) ** (-life)) # Capital costs - annualized_capital_costs = ( - convert_timeslice( - techs.cap_par * rates, - prices.timeslice, - QuantityType.EXTENSIVE, - ) - / techs.utilization_factor - ) + annualized_capital_costs = distribute_timeslice( + techs.cap_par * rates + ) / broadcast_timeslice(techs.utilization_factor) # Fixed and variable running costs - o_and_e_costs = ( - convert_timeslice( - (techs.fix_par + techs.var_par), - prices.timeslice, - QuantityType.EXTENSIVE, - ) - / techs.utilization_factor - ) + o_and_e_costs = distribute_timeslice( + techs.fix_par + techs.var_par + ) / broadcast_timeslice(techs.utilization_factor) # Fuel costs from fixed and flexible inputs - fuel_costs = ( - convert_timeslice(techs.fixed_inputs, prices.timeslice, QuantityType.EXTENSIVE) - * prices - ).sum("commodity") - fuel_costs += ( - convert_timeslice( - techs.flexible_inputs, prices.timeslice, QuantityType.EXTENSIVE - ) - * prices - ).sum("commodity") + fuel_costs = (distribute_timeslice(techs.fixed_inputs) * prices).sum("commodity") + fuel_costs += (distribute_timeslice(techs.flexible_inputs) * prices).sum( + "commodity" + ) # Environmental costs if "region" in techs.dims: env_costs = ( - ( - convert_timeslice( - techs.fixed_outputs, prices.timeslice, QuantityType.EXTENSIVE - ) - * prices - ) + (distribute_timeslice(techs.fixed_outputs) * prices) .sel(region=techs.region) .sel(commodity=is_pollutant(techs.comm_usage)) .sum("commodity") ) else: env_costs = ( - ( - convert_timeslice( - techs.fixed_outputs, prices.timeslice, QuantityType.EXTENSIVE - ) - * prices - ) + (distribute_timeslice(techs.fixed_outputs) * prices) .sel(commodity=is_pollutant(techs.comm_usage)) .sum("commodity") ) diff --git a/src/muse/decisions.py b/src/muse/decisions.py index b75b2a446..e24a3a9c9 100644 --- a/src/muse/decisions.py +++ b/src/muse/decisions.py @@ -117,7 +117,9 @@ def mean(objectives: Dataset, *args, **kwargs) -> DataArray: from xarray import concat allobjectives = concat(objectives.data_vars.values(), dim="concat_var") - return allobjectives.mean(set(allobjectives.dims) - {"asset", "replacement"}) + return allobjectives.mean( + set(allobjectives.dims) - {"asset", "replacement", "timeslice"} + ) @register_decision diff --git a/src/muse/demand_share.py b/src/muse/demand_share.py index 9f9567553..f32aab808 100644 --- a/src/muse/demand_share.py +++ b/src/muse/demand_share.py @@ -143,7 +143,7 @@ def new_and_retro( A_{a, s}^r = w_s\sum_i A_a^{r, i} with :math:`w_s` a weight associated with each timeslice and determined via - :py:func:`muse.timeslices.convert_timeslice`. + :py:func:`muse.timeslices.distribute_timeslice`. #. An intermediate quantity, the :py:func:`unmet demand ` :math:`U` is defined from @@ -234,7 +234,6 @@ def decommissioning(capacity): technologies, capacity, year=[current_year, current_year + forecast], - timeslices=market.timeslice, ).squeeze("year") capacity = reduce_assets([u.assets.capacity for u in agents]) @@ -308,7 +307,6 @@ def decommissioning(capacity): partial( maximum_production, technologies=regional_techs, - timeslices=market.timeslice, year=current_year, ), id_to_nquantity, @@ -360,7 +358,6 @@ def decommissioning(capacity): technologies, capacity, year=[current_year, current_year + forecast], - timeslices=market.timeslice, ).squeeze("year") # Make sure there are no retrofit agents @@ -412,7 +409,6 @@ def decommissioning(capacity): partial( maximum_production, technologies=technologies.sel(region=region), - timeslices=market.timeslice, year=current_year, ), id_to_quantity, @@ -523,9 +519,7 @@ def unmet_demand( from muse.quantities import maximum_production # Calculate maximum production by existing assets - produced = maximum_production( - capacity=capacity, technologies=technologies, timeslices=market.timeslice - ) + produced = maximum_production(capacity=capacity, technologies=technologies) # Total commodity production by summing over assets if "dst_region" in produced.dims: @@ -603,8 +597,11 @@ def new_and_retro_demands( # Interpolate market to forecast year smarket: xr.Dataset = market.interp(year=[current_year, current_year + forecast]) + + # Interpolate capacity to forecast year capa = capacity.interp(year=[current_year, current_year + forecast]) assert isinstance(capa, xr.DataArray) + if hasattr(capa, "region") and capa.region.dims == (): capa["region"] = "asset", [str(capa.region.values)] * len(capa.asset) @@ -620,7 +617,6 @@ def new_and_retro_demands( maximum_production( technologies, capa.sel(year=current_year + forecast), - timeslices=smarket.timeslice, ) .groupby("region") .sum("asset") diff --git a/src/muse/examples.py b/src/muse/examples.py index 9aa4fa9b6..df6293d6f 100644 --- a/src/muse/examples.py +++ b/src/muse/examples.py @@ -203,7 +203,6 @@ def mca_market(model: str = "default") -> xr.Dataset: base_year_import=getattr( settings.global_input_files, "base_year_import", None ), - timeslices=settings.timeslices, ) .sel(region=settings.regions) .interp(year=settings.time_framework, method=settings.interpolation_mode) @@ -263,9 +262,7 @@ def matching_market(sector: str, model: str = "default") -> xr.Dataset: market = xr.Dataset() production = cast( xr.DataArray, - maximum_production( - loaded_sector.technologies, assets.capacity, loaded_sector.timeslices - ), + maximum_production(loaded_sector.technologies, assets.capacity), ) market["supply"] = production.sum("asset") if "dst_region" in market.dims: diff --git a/src/muse/investments.py b/src/muse/investments.py index 0e2abb831..50b3cb23d 100644 --- a/src/muse/investments.py +++ b/src/muse/investments.py @@ -121,25 +121,6 @@ def factory(settings: Optional[Union[str, Mapping]] = None) -> Callable: name = settings["name"] params = {k: v for k, v in settings.items() if k != "name"} - top = params.get("timeslice_op", "max") - if isinstance(top, str): - if top.lower() == "max": - - def timeslice_op(x: xr.DataArray) -> xr.DataArray: - from muse.timeslices import convert_timeslice - - return (x / convert_timeslice(xr.DataArray(1), x)).max("timeslice") - - elif top.lower() == "sum": - - def timeslice_op(x: xr.DataArray) -> xr.DataArray: - return x.sum("timeslice") - - else: - raise ValueError(f"Unknown timeslice transform {top}") - - params["timeslice_op"] = timeslice_op - investment = INVESTMENTS[name] def compute_investment( @@ -243,7 +224,6 @@ def adhoc_match_demand( technologies: xr.Dataset, constraints: list[Constraint], year: int, - timeslice_op: Optional[Callable[[xr.DataArray], xr.DataArray]] = None, ) -> xr.DataArray: from muse.demand_matching import demand_matching from muse.quantities import capacity_in_use, maximum_production @@ -254,7 +234,6 @@ def adhoc_match_demand( max_prod = maximum_production( technologies, max_capacity, - timeslices=demand, year=year, technology=costs.replacement, commodity=demand.commodity, @@ -262,10 +241,6 @@ def adhoc_match_demand( # Push disabled techs to last rank. # Any production assigned to them by the demand-matching algorithm will be removed. - - if "timeslice" in costs.dims and timeslice_op is not None: - costs = costs.mean("timeslice").mean("asset") # timeslice_op(costs) - minobj = costs.min() maxobj = costs.where(search_space, minobj).max("replacement") + 1 @@ -280,8 +255,8 @@ def adhoc_match_demand( capacity = capacity_in_use( production, technologies, year=year, technology=production.replacement ).drop_vars("technology") - if "timeslice" in capacity.dims and timeslice_op is not None: - capacity = timeslice_op(capacity) + if "timeslice" in capacity.dims: + capacity = timeslice_max(capacity) result = xr.Dataset({"capacity": capacity, "production": production}) return result @@ -294,7 +269,6 @@ def scipy_match_demand( technologies: xr.Dataset, constraints: list[Constraint], year: Optional[int] = None, - timeslice_op: Optional[Callable[[xr.DataArray], xr.DataArray]] = None, **options, ) -> xr.DataArray: from logging import getLogger @@ -303,10 +277,8 @@ def scipy_match_demand( from muse.constraints import ScipyAdapter - if "timeslice" in costs.dims and timeslice_op is not None: - costs = timeslice_op(costs) - - timeslice = next(cs.timeslice for cs in constraints if "timeslice" in cs.dims) + if "timeslice" in costs.dims: + costs = timeslice_max(costs) # Select technodata for the current year if "year" in technologies.dims and year is None: @@ -317,9 +289,7 @@ def scipy_match_demand( techs = technologies # Run scipy optimization with highs solver - adapter = ScipyAdapter.factory( - techs, cast(np.ndarray, costs), timeslice, *constraints - ) + adapter = ScipyAdapter.factory(techs, cast(np.ndarray, costs), *constraints) res = linprog(**adapter.kwargs, method="highs") # Backup: try with highs-ipm @@ -356,7 +326,6 @@ def cvxopt_match_demand( technologies: xr.Dataset, constraints: list[Constraint], year: Optional[int] = None, - timeslice_op: Optional[Callable[[xr.DataArray], xr.DataArray]] = None, **options, ) -> xr.DataArray: from importlib import import_module @@ -372,9 +341,7 @@ def cvxopt_match_demand( techs = technologies def default_to_scipy(): - return scipy_match_demand( - costs, search_space, techs, constraints, timeslice_op=timeslice_op - ) + return scipy_match_demand(costs, search_space, techs, constraints) try: cvxopt = import_module("cvxopt") @@ -387,8 +354,8 @@ def default_to_scipy(): getLogger(__name__).critical(msg) return default_to_scipy() - if "timeslice" in costs.dims and timeslice_op is not None: - costs = timeslice_op(costs) + if "timeslice" in costs.dims: + costs = timeslice_max(costs) timeslice = next(cs.timeslice for cs in constraints if "timeslice" in cs.dims) adapter = ScipyAdapter.factory( techs, -cast(np.ndarray, costs), timeslice, *constraints @@ -414,3 +381,14 @@ def default_to_scipy(): solution = cast(Callable[[np.ndarray], xr.Dataset], adapter.to_muse)(list(res["x"])) return solution + + +def timeslice_max(x: xr.DataArray) -> xr.DataArray: + """Find the max value over the timeslice dimension, normlaized for timeslice length. + + This first annualizes the value in each timeslice by dividing by the fraction of the + year that the timeslice occupies, then takes the maximum value + """ + from muse.timeslices import TIMESLICE, broadcast_timeslice + + return (x / (TIMESLICE / broadcast_timeslice(TIMESLICE.sum()))).max("timeslice") diff --git a/src/muse/mca.py b/src/muse/mca.py index 1440a7d76..850e7673e 100644 --- a/src/muse/mca.py +++ b/src/muse/mca.py @@ -41,6 +41,7 @@ def factory(cls, settings: str | Path | Mapping | Any) -> MCA: from muse.outputs.mca import factory as ofactory from muse.readers import read_settings from muse.readers.toml import convert + from muse.timeslices import drop_timeslice if isinstance(settings, (str, Path)): settings = read_settings(settings) # type: ignore @@ -57,7 +58,6 @@ def factory(cls, settings: str | Path | Mapping | Any) -> MCA: base_year_import=getattr( settings.global_input_files, "base_year_import", None ), - timeslices=settings.timeslices, ).sel(region=settings.regions) ).interp(year=settings.time_framework, method=settings.interpolation_mode) @@ -276,6 +276,8 @@ def run(self) -> None: from xarray import DataArray + from muse.timeslices import broadcast_timeslice + nyear = len(self.time_framework) - 1 check_carbon_budget = len(self.carbon_budget) and len(self.carbon_commodities) shoots = self.control_undershoot or self.control_overshoot @@ -296,7 +298,7 @@ def run(self) -> None: new_market.prices.loc[dict(commodity=self.carbon_commodities)] = ( future_propagation( new_market.prices.sel(commodity=self.carbon_commodities), - future_price, + broadcast_timeslice(future_price), ) ) self.carbon_price = future_propagation(self.carbon_price, future_price) @@ -360,6 +362,7 @@ def single_year_iteration( from copy import deepcopy from muse.commodities import is_enduse + from muse.timeslices import drop_timeslice sectors = deepcopy(sectors) market = market.copy(deep=True) diff --git a/src/muse/objectives.py b/src/muse/objectives.py index 2bb3b7ba4..5e51d7ef0 100644 --- a/src/muse/objectives.py +++ b/src/muse/objectives.py @@ -133,12 +133,16 @@ def objectives( *args, **kwargs, ) -> xr.Dataset: + from muse.timeslices import broadcast_timeslice + result = xr.Dataset() for name, objective in functions: obj = objective( technologies=technologies, demand=demand, prices=prices, *args, **kwargs ) - if "timeslice" in obj.dims and "timeslice" in result.dims: + if "timeslice" not in obj.dims: + obj = broadcast_timeslice(obj) + if "timeslice" in result.dims: obj = drop_timeslice(obj) result[name] = obj return result @@ -214,12 +218,8 @@ def capacity_to_service_demand( ) -> xr.DataArray: """Minimum capacity required to fulfill the demand.""" from muse.quantities import capacity_to_service_demand - from muse.timeslices import represent_hours - hours = represent_hours(demand.timeslice) - return capacity_to_service_demand( - demand=demand, technologies=technologies, hours=hours - ) + return capacity_to_service_demand(demand=demand, technologies=technologies) @register_objective @@ -230,13 +230,12 @@ def capacity_in_use( **kwargs, ): from muse.commodities import is_enduse - from muse.timeslices import represent_hours + from muse.timeslices import TIMESLICE - hours = represent_hours(demand.timeslice) enduses = is_enduse(technologies.comm_usage.sel(commodity=demand.commodity)) return ( - (demand.sel(commodity=enduses).sum("commodity") / hours).sum("timeslice") - * hours.sum() + (demand.sel(commodity=enduses).sum("commodity") / TIMESLICE).sum("timeslice") + * TIMESLICE.sum() / technologies.utilization_factor ) @@ -279,7 +278,9 @@ def fixed_costs( :math:`\alpha` and :math:`\beta` are "fix_par" and "fix_exp" in :ref:`inputs-technodata`, respectively. """ - capacity = capacity_to_service_demand(technologies, demand) + from muse.quantities import capacity_to_service_demand + + capacity = capacity_to_service_demand(technologies=technologies, demand=demand) result = technologies.fix_par * (capacity**technologies.fix_exp) return result @@ -322,18 +323,15 @@ def emission_cost( with :math:`s` the timeslices and :math:`c` the commodity. """ from muse.commodities import is_enduse, is_pollutant - from muse.timeslices import QuantityType, convert_timeslice + from muse.timeslices import distribute_timeslice enduses = is_enduse(technologies.comm_usage.sel(commodity=demand.commodity)) total = demand.sel(commodity=enduses).sum("commodity") envs = is_pollutant(technologies.comm_usage) prices = filter_input(prices, year=demand.year.item(), commodity=envs) - return total * ( - convert_timeslice( - technologies.fixed_outputs, prices.timeslice, QuantityType.EXTENSIVE - ) - * prices - ).sum("commodity") + return total * (distribute_timeslice(technologies.fixed_outputs) * prices).sum( + "commodity" + ) @register_objective @@ -394,15 +392,14 @@ def lifetime_levelized_cost_of_energy( due to a zero utilisation factor. """ from muse.costs import lifetime_levelized_cost_of_energy as LCOE - from muse.timeslices import QuantityType, convert_timeslice + from muse.quantities import capacity_to_service_demand + from muse.timeslices import broadcast_timeslice, distribute_timeslice - capacity = capacity_to_service_demand(technologies, demand) + capacity = capacity_to_service_demand(technologies=technologies, demand=demand) production = ( - capacity - * convert_timeslice( - technologies.fixed_outputs, demand.timeslice, QuantityType.EXTENSIVE - ) - * technologies.utilization_factor + broadcast_timeslice(capacity) + * distribute_timeslice(technologies.fixed_outputs) + * broadcast_timeslice(technologies.utilization_factor) ) results = LCOE( @@ -429,15 +426,14 @@ def net_present_value( See :py:func:`muse.costs.net_present_value` for more details. """ from muse.costs import net_present_value as NPV - from muse.timeslices import QuantityType, convert_timeslice + from muse.quantities import capacity_to_service_demand + from muse.timeslices import broadcast_timeslice, distribute_timeslice - capacity = capacity_to_service_demand(technologies, demand) + capacity = capacity_to_service_demand(technologies=technologies, demand=demand) production = ( - capacity - * convert_timeslice( - technologies.fixed_outputs, demand.timeslice, QuantityType.EXTENSIVE - ) - * technologies.utilization_factor + broadcast_timeslice(capacity) + * distribute_timeslice(technologies.fixed_outputs) + * broadcast_timeslice(technologies.utilization_factor) ) results = NPV( @@ -463,15 +459,14 @@ def net_present_cost( See :py:func:`muse.costs.net_present_cost` for more details. """ from muse.costs import net_present_cost as NPC - from muse.timeslices import QuantityType, convert_timeslice + from muse.quantities import capacity_to_service_demand + from muse.timeslices import broadcast_timeslice, distribute_timeslice - capacity = capacity_to_service_demand(technologies, demand) + capacity = capacity_to_service_demand(technologies=technologies, demand=demand) production = ( - capacity - * convert_timeslice( - technologies.fixed_outputs, demand.timeslice, QuantityType.EXTENSIVE - ) - * technologies.utilization_factor + broadcast_timeslice(capacity) + * distribute_timeslice(technologies.fixed_outputs) + * broadcast_timeslice(technologies.utilization_factor) ) results = NPC( @@ -497,15 +492,14 @@ def equivalent_annual_cost( See :py:func:`muse.costs.equivalent_annual_cost` for more details. """ from muse.costs import equivalent_annual_cost as EAC - from muse.timeslices import QuantityType, convert_timeslice + from muse.quantities import capacity_to_service_demand + from muse.timeslices import broadcast_timeslice, distribute_timeslice - capacity = capacity_to_service_demand(technologies, demand) + capacity = capacity_to_service_demand(technologies=technologies, demand=demand) production = ( - capacity - * convert_timeslice( - technologies.fixed_outputs, demand.timeslice, QuantityType.EXTENSIVE - ) - * technologies.utilization_factor + broadcast_timeslice(capacity) + * distribute_timeslice(technologies.fixed_outputs) + * broadcast_timeslice(technologies.utilization_factor) ) results = EAC( diff --git a/src/muse/outputs/mca.py b/src/muse/outputs/mca.py index 262aaadde..7d9284daf 100644 --- a/src/muse/outputs/mca.py +++ b/src/muse/outputs/mca.py @@ -35,7 +35,7 @@ def quantity( from muse.outputs.sector import market_quantity from muse.registration import registrator from muse.sectors import AbstractSector -from muse.timeslices import QuantityType, convert_timeslice +from muse.timeslices import distribute_timeslice from muse.utilities import multiindex_to_coords OUTPUT_QUANTITY_SIGNATURE = Callable[ @@ -267,14 +267,10 @@ def sector_fuel_costs( year=output_year, ).fillna(0.0) - production = convert_timeslice( - supply( - agent_market, - capacity, - technologies, - ), - agent_market["consumption"].timeslice, - QuantityType.EXTENSIVE, + production = supply( + agent_market, + capacity, + technologies, ) prices = a.filter_input(market.prices, year=output_year) @@ -318,7 +314,6 @@ def sector_capital_costs( if len(technologies) > 0: for a in agents: - demand = market.consumption * a.quantity output_year = a.year - a.forecast capacity = a.filter_input(a.assets.capacity, year=output_year).fillna(0.0) data = a.filter_input( @@ -326,12 +321,7 @@ def sector_capital_costs( year=output_year, technology=capacity.technology, ) - result = data.cap_par * (capacity**data.cap_exp) - data_agent = convert_timeslice( - result, - demand.timeslice, - QuantityType.EXTENSIVE, - ) + data_agent = distribute_timeslice(data.cap_par * (capacity**data.cap_exp)) data_agent["agent"] = a.name data_agent["category"] = a.category data_agent["sector"] = getattr(sector, "name", "unnamed") @@ -388,15 +378,12 @@ def sector_emission_costs( i = (np.where(envs))[0][0] red_envs = envs[i].commodity.values prices = a.filter_input(market.prices, year=output_year, commodity=red_envs) - production = convert_timeslice( - supply( - agent_market, - capacity, - technologies, - ), - agent_market["consumption"].timeslice, - QuantityType.EXTENSIVE, + production = supply( + agent_market, + capacity, + technologies, ) + total = production.sel(commodity=enduses).sum("commodity") data_agent = total * (allemissions * prices).sum("commodity") data_agent["agent"] = a.name @@ -463,11 +450,7 @@ def sector_lcoe(sector: AbstractSector, market: xr.Dataset, **kwargs) -> pd.Data capacity = agent.filter_input(capacity_to_service_demand(demand, techs)) production = ( capacity - * convert_timeslice( - techs.fixed_outputs, - demand.timeslice, - QuantityType.EXTENSIVE, - ) + * distribute_timeslice(techs.fixed_outputs) * techs.utilization_factor ) @@ -544,11 +527,7 @@ def sector_eac(sector: AbstractSector, market: xr.Dataset, **kwargs) -> pd.DataF capacity = agent.filter_input(capacity_to_service_demand(demand, techs)) production = ( capacity - * convert_timeslice( - techs.fixed_outputs, - demand.timeslice, - QuantityType.EXTENSIVE, - ) + * distribute_timeslice(techs.fixed_outputs) * techs.utilization_factor ) diff --git a/src/muse/production.py b/src/muse/production.py index 53494c06f..39f8dbb84 100644 --- a/src/muse/production.py +++ b/src/muse/production.py @@ -107,7 +107,7 @@ def maximum_production( """ from muse.quantities import maximum_production - return maximum_production(technologies, capacity, timeslices=market.timeslice) + return maximum_production(technologies, capacity) @register_production(name=("share", "shares")) diff --git a/src/muse/quantities.py b/src/muse/quantities.py index 339024c63..fa4cec2cc 100644 --- a/src/muse/quantities.py +++ b/src/muse/quantities.py @@ -40,9 +40,10 @@ def supply( input commodities). """ from muse.commodities import CommodityUsage, check_usage, is_pollutant + from muse.timeslices import broadcast_timeslice - maxprod = maximum_production(technologies, capacity, timeslices=demand) - minprod = minimum_production(technologies, capacity, timeslices=demand) + maxprod = maximum_production(technologies, capacity) + minprod = minimum_production(technologies, capacity) size = np.array(maxprod.region).size # in presence of trade demand needs to map maxprod dst_region if ( @@ -81,8 +82,12 @@ def supply( demsum = set(maxprod.dims).difference(demand.dims) expanded_demand = (demand * maxprod / maxprod.sum(demsum)).fillna(0) - expanded_maxprod = (maxprod * demand / demand.sum(prodsum)).fillna(0) - expanded_minprod = (minprod * demand / demand.sum(prodsum)).fillna(0) + expanded_maxprod = ( + maxprod * demand / broadcast_timeslice(demand.sum(prodsum)) + ).fillna(0) + expanded_minprod = ( + minprod * demand / broadcast_timeslice(demand.sum(prodsum)) + ).fillna(0) expanded_demand = expanded_demand.reindex_like(maxprod) expanded_minprod = expanded_minprod.reindex_like(maxprod) @@ -118,6 +123,7 @@ def emission(production: xr.DataArray, fixed_outputs: xr.DataArray): A data array containing emissions (and only emissions). """ from muse.commodities import is_enduse, is_pollutant + from muse.timeslices import broadcast_timeslice from muse.utilities import broadcast_techs # just in case we are passed a technologies dataset, like in other functions @@ -126,8 +132,8 @@ def emission(production: xr.DataArray, fixed_outputs: xr.DataArray): ) envs = is_pollutant(fouts.comm_usage) enduses = is_enduse(fouts.comm_usage) - return production.sel(commodity=enduses).sum("commodity") * fouts.sel( - commodity=envs + return production.sel(commodity=enduses).sum("commodity") * broadcast_timeslice( + fouts.sel(commodity=envs) ) @@ -146,7 +152,7 @@ def gross_margin( - non-environmental commodities OUTPUTS are related to revenues. """ from muse.commodities import is_enduse, is_pollutant - from muse.timeslices import QuantityType, convert_timeslice + from muse.timeslices import distribute_timeslice from muse.utilities import broadcast_techs tech = broadcast_techs( # type: ignore @@ -183,22 +189,15 @@ def gross_margin( enduses = is_enduse(technologies.comm_usage) # Variable costs depend on factors such as labour - variable_costs = convert_timeslice( + variable_costs = distribute_timeslice( var_par * ((fixed_outputs.sel(commodity=enduses)).sum("commodity")) ** var_exp, - prices.timeslice, - QuantityType.EXTENSIVE, ) # The individual prices are selected # costs due to consumables, direct inputs - consumption_costs = ( - prices - * convert_timeslice(fixed_inputs, prices.timeslice, QuantityType.EXTENSIVE) - ).sum("commodity") + consumption_costs = (prices * distribute_timeslice(fixed_inputs)).sum("commodity") # costs due to pollutants - production_costs = prices * convert_timeslice( - fixed_outputs, prices.timeslice, QuantityType.EXTENSIVE - ) + production_costs = prices * distribute_timeslice(fixed_outputs) environmental_costs = (production_costs.sel(commodity=environmentals)).sum( "commodity" ) @@ -216,7 +215,6 @@ def gross_margin( def decommissioning_demand( technologies: xr.Dataset, capacity: xr.DataArray, - timeslices: xr.DataArray, year: Optional[Sequence[int]] = None, ) -> xr.DataArray: r"""Computes demand from process decommissioning. @@ -259,7 +257,6 @@ def decommissioning_demand( return maximum_production( technologies, capacity_decrease, - timeslices=timeslices, ).clip(min=0) @@ -275,7 +272,7 @@ def consumption( are not given, then flexible consumption is *not* considered. """ from muse.commodities import is_enduse, is_fuel - from muse.timeslices import QuantityType, convert_timeslice + from muse.timeslices import broadcast_timeslice from muse.utilities import filter_with_template params = filter_with_template( @@ -287,14 +284,10 @@ def consumption( comm_usage = technologies.comm_usage.sel(commodity=production.commodity) production = production.sel(commodity=is_enduse(comm_usage)).sum("commodity") - - if prices is not None and "timeslice" in prices.dims: - production = convert_timeslice( # type: ignore - production, prices, QuantityType.EXTENSIVE - ) - params_fuels = is_fuel(params.comm_usage) - consumption = production * params.fixed_inputs.where(params_fuels, 0) + consumption = production * broadcast_timeslice( + params.fixed_inputs.where(params_fuels, 0) + ) if prices is None: return consumption @@ -315,7 +308,7 @@ def consumption( ] # add consumption from cheapest fuel assert all(flexs.commodity.values == consumption.commodity.values) - flex = flexs.where(minprices == flexs.commodity, 0) + flex = flexs.where(minprices == broadcast_timeslice(flexs.commodity), 0) flex = flex / (flex > 0).sum("commodity").clip(min=1) return consumption + flex * production @@ -323,7 +316,6 @@ def consumption( def maximum_production( technologies: xr.Dataset, capacity: xr.DataArray, - timeslices: xr.DataArray, **filters, ): r"""Production for a given capacity. @@ -359,7 +351,7 @@ def maximum_production( filters and the set of technologies in `capacity`. """ from muse.commodities import is_enduse - from muse.timeslices import QuantityType, convert_timeslice + from muse.timeslices import broadcast_timeslice, distribute_timeslice from muse.utilities import broadcast_techs, filter_input capa = filter_input( @@ -372,9 +364,9 @@ def maximum_production( btechs, **{k: v for k, v in filters.items() if k in btechs.dims} ) result = ( - capa - * convert_timeslice(ftechs.fixed_outputs, timeslices, QuantityType.EXTENSIVE) - * ftechs.utilization_factor + broadcast_timeslice(capa) + * distribute_timeslice(ftechs.fixed_outputs) + * broadcast_timeslice(ftechs.utilization_factor) ) return result.where(is_enduse(result.comm_usage), 0) @@ -405,6 +397,7 @@ def capacity_in_use( Capacity-in-use for each technology, whittled down by the filters. """ from muse.commodities import is_enduse + from muse.timeslices import broadcast_timeslice from muse.utilities import broadcast_techs, filter_input prod = filter_input( @@ -419,7 +412,7 @@ def capacity_in_use( ) factor = 1 / (ftechs.fixed_outputs * ftechs.utilization_factor) - capa_in_use = (prod * factor).where(~np.isinf(factor), 0) + capa_in_use = (prod * broadcast_timeslice(factor)).where(~np.isinf(factor), 0) capa_in_use = capa_in_use.where( is_enduse(technologies.comm_usage.sel(commodity=capa_in_use.commodity)), 0 @@ -433,7 +426,6 @@ def capacity_in_use( def minimum_production( technologies: xr.Dataset, capacity: xr.DataArray, - timeslices: xr.DataArray, **filters, ): r"""Minimum production for a given capacity. @@ -469,7 +461,7 @@ def minimum_production( the filters and the set of technologies in `capacity`. """ from muse.commodities import is_enduse - from muse.timeslices import QuantityType, convert_timeslice + from muse.timeslices import broadcast_timeslice, distribute_timeslice from muse.utilities import broadcast_techs, filter_input capa = filter_input( @@ -477,7 +469,7 @@ def minimum_production( ) if "minimum_service_factor" not in technologies: - return xr.zeros_like(capa) + return broadcast_timeslice(xr.zeros_like(capa)) btechs = broadcast_techs( # type: ignore cast( @@ -490,9 +482,9 @@ def minimum_production( btechs, **{k: v for k, v in filters.items() if k in btechs.dims} ) result = ( - capa - * convert_timeslice(ftechs.fixed_outputs, timeslices, QuantityType.EXTENSIVE) - * ftechs.minimum_service_factor + broadcast_timeslice(capa) + * distribute_timeslice(ftechs.fixed_outputs) + * broadcast_timeslice(ftechs.minimum_service_factor) ) return result.where(is_enduse(result.comm_usage), 0) @@ -500,17 +492,12 @@ def minimum_production( def capacity_to_service_demand( demand: xr.DataArray, technologies: xr.Dataset, - hours=None, ) -> xr.DataArray: """Minimum capacity required to fulfill the demand.""" - from muse.timeslices import represent_hours - - if hours is None: - hours = represent_hours(demand.timeslice) - max_hours = hours.max() / hours.sum() - commodity_output = technologies.fixed_outputs.sel(commodity=demand.commodity) - max_demand = ( - demand.where(commodity_output > 0, 0) - / commodity_output.where(commodity_output > 0, 1) - ).max(("commodity", "timeslice")) - return max_demand / technologies.utilization_factor / max_hours + from muse.timeslices import broadcast_timeslice, distribute_timeslice + + timeslice_outputs = distribute_timeslice( + technologies.fixed_outputs.sel(commodity=demand.commodity) + ) * broadcast_timeslice(technologies.utilization_factor) + capa_to_service_demand = demand / timeslice_outputs + return capa_to_service_demand.max(("commodity", "timeslice")) diff --git a/src/muse/readers/__init__.py b/src/muse/readers/__init__.py index 930dd43ef..01e06dca3 100644 --- a/src/muse/readers/__init__.py +++ b/src/muse/readers/__init__.py @@ -2,7 +2,7 @@ from muse.defaults import DATA_DIRECTORY from muse.readers.csv import * # noqa: F403 -from muse.readers.toml import read_settings, read_timeslices # noqa: F401 +from muse.readers.toml import read_settings # noqa: F401 DEFAULT_SETTINGS_PATH = DATA_DIRECTORY / "default_settings.toml" """Default settings path.""" diff --git a/src/muse/readers/csv.py b/src/muse/readers/csv.py index 5f96b318f..4eea66108 100644 --- a/src/muse/readers/csv.py +++ b/src/muse/readers/csv.py @@ -5,7 +5,6 @@ "read_io_technodata", "read_initial_assets", "read_technologies", - "read_csv_timeslices", "read_global_commodities", "read_timeslice_shares", "read_csv_agent_parameters", @@ -137,7 +136,7 @@ def to_agent_share(name): def read_technodata_timeslices(filename: Union[str, Path]) -> xr.Dataset: from muse.readers import camel_to_snake - from muse.timeslices import TIMESLICE, convert_timeslice + from muse.timeslices import TIMESLICE csv = pd.read_csv(filename, float_precision="high", low_memory=False) csv = csv.rename(columns=camel_to_snake) @@ -171,7 +170,7 @@ def read_technodata_timeslices(filename: Union[str, Path]) -> xr.Dataset: if item not in ["technology", "region", "year"] ] result = result.stack(timeslice=timeslice_levels) - result = convert_timeslice(result, TIMESLICE) + result = result.sel(timeslice=TIMESLICE.timeslice) # sorts timeslices into the correct order return result @@ -416,35 +415,6 @@ def read_technologies( return result -def read_csv_timeslices(path: Union[str, Path], **kwargs) -> xr.DataArray: - """Reads timeslice information from input.""" - from logging import getLogger - - getLogger(__name__).info(f"Reading timeslices from {path}") - data = pd.read_csv(path, float_precision="high", **kwargs) - - def snake_case(string): - from re import sub - - result = sub(r"((?<=[a-z])[A-Z]|(? xr.Dataset: """Reads commodities information from input.""" from logging import getLogger @@ -479,7 +449,6 @@ def read_global_commodities(path: Union[str, Path]) -> xr.Dataset: def read_timeslice_shares( path: Union[str, Path] = DEFAULT_SECTORS_DIRECTORY, sector: Optional[str] = None, - timeslice: Union[str, Path, xr.DataArray] = "Timeslices{sector}.csv", ) -> xr.Dataset: """Reads sliceshare information into a xr.Dataset. @@ -498,12 +467,6 @@ def read_timeslice_shares( path, filename = path.parent, path.name re = match(r"TimesliceShare(.*)\.csv", filename) sector = path.name if re is None else re.group(1) - if isinstance(timeslice, str) and "{sector}" in timeslice: - timeslice = timeslice.format(sector=sector) - if isinstance(timeslice, (str, Path)) and not Path(timeslice).is_file(): - timeslice = find_sectors_file(timeslice, sector, path) - if isinstance(timeslice, (str, Path)): - timeslice = read_csv_timeslices(timeslice, low_memory=False) share_path = find_sectors_file(f"TimesliceShare{sector}.csv", sector, path) getLogger(__name__).info(f"Reading timeslice shares from {share_path}") @@ -516,13 +479,6 @@ def read_timeslice_shares( data.columns.name = "commodity" result = xr.DataArray(data).unstack("rt").to_dataset(name="shares") - - if timeslice is None: - result = result.drop_vars("timeslice") - elif isinstance(timeslice, xr.DataArray) and hasattr(timeslice, "timeslice"): - result["timeslice"] = timeslice.timeslice - else: - result["timeslice"] = timeslice return result.shares @@ -634,19 +590,16 @@ def read_initial_market( projections: Union[xr.DataArray, Path, str], base_year_import: Optional[Union[str, Path, xr.DataArray]] = None, base_year_export: Optional[Union[str, Path, xr.DataArray]] = None, - timeslices: Optional[xr.DataArray] = None, ) -> xr.Dataset: """Read projections, import and export csv files.""" from logging import getLogger - from muse.timeslices import QuantityType, convert_timeslice + from muse.timeslices import TIMESLICE, distribute_timeslice # Projections must always be present if isinstance(projections, (str, Path)): getLogger(__name__).info(f"Reading projections from {projections}") projections = read_attribute_table(projections) - if timeslices is not None: - projections = convert_timeslice(projections, timeslices, QuantityType.INTENSIVE) # Base year export is optional. If it is not there, it's set to zero if isinstance(base_year_export, (str, Path)): @@ -664,13 +617,8 @@ def read_initial_market( getLogger(__name__).info("Base year import not provided. Set to zero.") base_year_import = xr.zeros_like(projections) - if timeslices is not None: - base_year_export = convert_timeslice( - base_year_export, timeslices, QuantityType.EXTENSIVE - ) - base_year_import = convert_timeslice( - base_year_import, timeslices, QuantityType.EXTENSIVE - ) + base_year_export = distribute_timeslice(base_year_export) + base_year_import = distribute_timeslice(base_year_import) base_year_export.name = "exports" base_year_import.name = "imports" @@ -690,7 +638,7 @@ def read_initial_market( commodity_price="prices", units_commodity_price="units_prices" ) result["prices"] = ( - result["prices"].expand_dims({"timeslice": timeslices}).drop_vars("timeslice") + result["prices"].expand_dims({"timeslice": TIMESLICE}).drop_vars("timeslice") ) return result diff --git a/src/muse/readers/toml.py b/src/muse/readers/toml.py index 5b357d636..55e97692c 100644 --- a/src/muse/readers/toml.py +++ b/src/muse/readers/toml.py @@ -16,7 +16,6 @@ ) import numpy as np -import pandas as pd import xarray as xr from muse.decorators import SETTINGS_CHECKS, register_settings_check @@ -360,6 +359,8 @@ def read_settings( Returns: A dictionary with the settings """ + from muse.timeslices import setup_module + getLogger(__name__).info("Reading MUSE settings") # The user data @@ -389,181 +390,16 @@ def read_settings( settings = add_known_parameters(default_settings, user_settings) settings = add_unknown_parameters(settings, user_settings) + # Set up timeslices + setup_module(settings) + settings.pop("timeslices", None) + # Finally, we run some checks to make sure all makes sense and files exist. validate_settings(settings) return convert(settings) -def read_ts_multiindex( - settings: Optional[Union[Mapping, str]] = None, - timeslice: Optional[xr.DataArray] = None, - transforms: Optional[dict[tuple, np.ndarray]] = None, -) -> pd.MultiIndex: - '''Read multiindex for a timeslice from TOML. - - Example: - The timeslices are read from ``timeslice_levels``. The levels (keyword) and - slice (list of values) correspond to the level, slices and slice aggregates - defined in the the ``timeslices`` section. - - >>> toml = """ - ... ["timeslices"] - ... winter.weekday.day = 5 - ... winter.weekday.night = 5 - ... winter.weekend.day = 2 - ... winter.weekend.night = 2 - ... winter.weekend.dusk = 1 - ... summer.weekday.day = 5 - ... summer.weekday.night = 5 - ... summer.weekend.day = 2 - ... summer.weekend.night = 2 - ... summer.weekend.dusk = 1 - ... level_names = ["semester", "week", "day"] - ... aggregates.allday = ["day", "night"] - ... [timeslice_levels] - ... day = ["dusk", "allday"] - ... """ - >>> from muse.timeslices import ( - ... reference_timeslice, aggregate_transforms - ... ) - >>> from muse.readers.toml import read_ts_multiindex - >>> ref = reference_timeslice(toml) - >>> transforms = aggregate_transforms(toml, ref) - >>> read_ts_multiindex(toml, ref, transforms) - MultiIndex([('summer', 'weekday', 'allday'), - ('summer', 'weekend', 'dusk'), - ('summer', 'weekend', 'allday'), - ('winter', 'weekday', 'allday'), - ('winter', 'weekend', 'dusk'), - ('winter', 'weekend', 'allday')], - names=['semester', 'week', 'day']) - - It is an error to refer to a level or a slice that does not exist: - - >>> read_ts_multiindex(dict(days=["dusk", "allday"]), ref, transforms) - Traceback (most recent call last): - ... - muse.readers.toml.IncorrectSettings: Unexpected level name(s): ... - >>> read_ts_multiindex(dict(day=["usk", "allday"]), ref, transforms) - Traceback (most recent call last): - ... - muse.readers.toml.IncorrectSettings: Unexpected slice(s): ... - ''' - from itertools import product - - from toml import loads - - from muse.timeslices import TIMESLICE, TRANSFORMS - - indices = (TIMESLICE if timeslice is None else timeslice).get_index("timeslice") - if transforms is None: - transforms = TRANSFORMS - if isinstance(settings, str): - settings = loads(settings) - elif settings is None: - return indices - elif not isinstance(settings, Mapping): - settings = undo_damage(settings) - settings = settings.get("timeslice_levels", settings) - assert isinstance(settings, Mapping) - if not set(settings).issubset(indices.names): - msg = "Unexpected level name(s): " + ", ".join( - set(settings).difference(indices.names) - ) - raise IncorrectSettings(msg) - levels = [ - settings.get(name, level) for name, level in zip(indices.names, indices.levels) - ] - levels = [[level] if isinstance(level, str) else level for level in levels] - for i, level in enumerate(levels): - known = [index[i] for index in transforms if len(index) > i] - unexpected = set(level).difference(known) - if unexpected: - raise IncorrectSettings("Unexpected slice(s): " + ", ".join(unexpected)) - return pd.MultiIndex.from_tuples( - [index for index in product(*levels) if index in transforms], - names=indices.names, - ) - - -def read_timeslices( - settings: Optional[Union[str, Mapping]] = None, - timeslice: Optional[xr.DataArray] = None, - transforms: Optional[dict[tuple, np.ndarray]] = None, -) -> xr.Dataset: - '''Reads timeslice levels and create resulting timeslice coordinate. - - Args: - settings: TOML dictionary. It should contain a ``timeslice_levels`` section. - Otherwise, the timeslices will default to the global (finest) timeslices. - timeslice: Finest timeslices. Defaults to the global in - :py:mod:`~muse.timeslices`. If using the default, then this function - should be called *after* the timeslice module has been setup with a call to - :py:func:`~muse.timeslice.setup_module`. - transforms: Transforms from desired timeslices to the finest timeslice. Defaults - to the global in :py:mod:`~muse.timeslices`. If using the default, - then this function should be called *after* the timeslice module has been - setup with a call to :py:func:`~muse.timeslice.setup_module`. - - Returns: - A xr.Dataset with the timeslice coordinates. - - Example: - >>> toml = """ - ... ["timeslices"] - ... winter.weekday.day = 5 - ... winter.weekday.night = 5 - ... winter.weekend.day = 2 - ... winter.weekend.night = 2 - ... winter.weekend.dusk = 1 - ... summer.weekday.day = 5 - ... summer.weekday.night = 5 - ... summer.weekend.day = 2 - ... summer.weekend.night = 2 - ... summer.weekend.dusk = 1 - ... level_names = ["semester", "week", "day"] - ... aggregates.allday = ["day", "night"] - ... [timeslice_levels] - ... day = ["dusk", "allday"] - ... """ - >>> from muse.timeslices import ( - ... reference_timeslice, aggregate_transforms - ... ) - >>> from muse.readers.toml import read_timeslices - >>> ref = reference_timeslice(toml) - >>> transforms = aggregate_transforms(toml, ref) - >>> ts = read_timeslices(toml, ref, transforms) - >>> assert "semester" in ts.coords - >>> assert "week" in ts.coords - >>> assert "day" in ts.coords - >>> assert "represent_hours" in ts.coords - >>> assert set(ts.coords["day"].data) == {"dusk", "allday"} - >>> assert set(ts.coords["week"].data) == {"weekday", "weekend"} - >>> assert set(ts.coords["semester"].data) == {"summer", "winter"} - ''' - from muse.timeslices import TIMESLICE, timeslice_projector - - if timeslice is None: - timeslice = TIMESLICE - if settings is None: - return xr.Dataset({"represent_hours": timeslice}).set_coords("represent_hours") - indices = read_ts_multiindex(settings, timeslice=timeslice, transforms=transforms) - units = xr.DataArray( - np.ones(len(indices)), coords={"timeslice": indices}, dims="timeslice" - ) - proj = timeslice_projector(units, finest=timeslice, transforms=transforms) - proj *= xr.DataArray( - timeslice.values, - coords={"finest_timeslice": proj.finest_timeslice}, - dims="finest_timeslice", - ) - - return xr.Dataset({"represent_hours": proj.sum("finest_timeslice")}).set_coords( - "represent_hours" - ) - - def add_known_parameters(dd, u, parent=None): """Function for updating the settings dictionary recursively. @@ -761,20 +597,6 @@ def check_iteration_control(settings: dict) -> None: assert settings["tolerance"] > 0, msg -@register_settings_check(vary_name=False) -def check_time_slices(settings: dict) -> None: - """Check the time slices. - - If there is no error, they are transformed into a xr.DataArray - """ - from muse.timeslices import setup_module - - setup_module(settings) - settings["timeslices"] = read_timeslices( - settings.get("mca", settings).get("timeslice_levels", None) - ).timeslice - - @register_settings_check(vary_name=False) def check_global_data_files(settings: dict) -> None: """Checks that the global user files exist.""" diff --git a/src/muse/sectors/preset_sector.py b/src/muse/sectors/preset_sector.py index 7a35bc2a3..03bf20080 100644 --- a/src/muse/sectors/preset_sector.py +++ b/src/muse/sectors/preset_sector.py @@ -28,17 +28,14 @@ def factory(cls, name: str, settings: Any) -> PresetSector: read_presets, read_regression_parameters, read_timeslice_shares, - read_timeslices, ) from muse.regressions import endogenous_demand - from muse.timeslices import QuantityType, convert_timeslice + from muse.timeslices import TIMESLICE, broadcast_timeslice, distribute_timeslice sector_conf = getattr(settings.sectors, name) presets = Dataset() - timeslice = read_timeslices( - getattr(sector_conf, "timeslice_levels", None) - ).timeslice + timeslice = TIMESLICE.timeslice if getattr(sector_conf, "consumption_path", None) is not None: consumption = read_presets(sector_conf.consumption_path) presets["consumption"] = consumption.assign_coords(timeslice=timeslice) @@ -71,22 +68,14 @@ def factory(cls, name: str, settings: Any) -> PresetSector: if getattr(sector_conf, "timeslice_shares_path", None) is not None: assert isinstance(timeslice, DataArray) - shares = read_timeslice_shares( - sector_conf.timeslice_shares_path, timeslice=timeslice - ) + shares = read_timeslice_shares(sector_conf.timeslice_shares_path) + shares = shares.assign_coords(timeslice=timeslice) assert consumption.commodity.isin(shares.commodity).all() assert consumption.region.isin(shares.region).all() - if "timeslice" in shares.dims: - ts = shares.timeslice - shares = drop_timeslice(shares) - consumption = (shares * consumption).assign_coords(timeslice=ts) - else: - consumption = consumption * shares.sel( - region=consumption.region, commodity=consumption.commodity - ) - presets["consumption"] = drop_timeslice(consumption).assign_coords( - timeslice=timeslice - ) + consumption = broadcast_timeslice(consumption) * shares.sel( + region=consumption.region, commodity=consumption.commodity + ) + presets["consumption"] = consumption if getattr(sector_conf, "supply_path", None) is not None: supply = read_presets(sector_conf.supply_path) @@ -121,9 +110,7 @@ def factory(cls, name: str, settings: Any) -> PresetSector: # add timeslice, if missing for component in {"supply", "consumption"}: if "timeslice" not in presets[component].dims: - presets[component] = convert_timeslice( - presets[component], timeslice, QuantityType.EXTENSIVE - ) + presets[component] = distribute_timeslice(presets[component]) comm_usage = (presets.costs > 0).any(set(presets.costs.dims) - {"commodity"}) presets["comm_usage"] = ( @@ -151,21 +138,13 @@ def __init__( def next(self, mca_market: Dataset) -> Dataset: """Advance sector by one time period.""" - from muse.timeslices import QuantityType, convert_timeslice - presets = self.presets.sel(region=mca_market.region) supply = self._interpolate(presets.supply, mca_market.year) consumption = self._interpolate(presets.consumption, mca_market.year) costs = self._interpolate(presets.costs, mca_market.year) - result = convert_timeslice( - Dataset({"supply": supply, "consumption": consumption}), - mca_market.timeslice, - QuantityType.EXTENSIVE, - ) - result["costs"] = drop_timeslice( - convert_timeslice(costs, mca_market.timeslice, QuantityType.INTENSIVE) - ) + result = Dataset({"supply": supply, "consumption": consumption}) + result["costs"] = drop_timeslice(costs) assert isinstance(result, Dataset) return result diff --git a/src/muse/sectors/sector.py b/src/muse/sectors/sector.py index 8d0f082ff..7037eeca4 100644 --- a/src/muse/sectors/sector.py +++ b/src/muse/sectors/sector.py @@ -7,7 +7,6 @@ cast, ) -import pandas as pd import xarray as xr from muse.agents import AbstractAgent @@ -26,7 +25,6 @@ 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 @@ -39,11 +37,6 @@ def factory(cls, name: str, settings: Any) -> Sector: if len(sector_settings["subsectors"]._asdict()) == 0: raise RuntimeError(f"Empty 'subsectors' section in sector {name}") - # Timeslices - timeslices = read_timeslices( - sector_settings.pop("timeslice_levels", None) - ).get_index("timeslice") - # Read technologies technologies = read_technodata(settings, name, settings.time_framework) @@ -95,7 +88,6 @@ def factory(cls, name: str, settings: Any) -> Sector: name, technologies, subsectors=subsectors, - timeslices=timeslices, supply_prod=supply, outputs=outputs, interactions=interactions, @@ -107,7 +99,6 @@ def __init__( name: str, technologies: xr.Dataset, subsectors: Sequence[Subsector] = [], - timeslices: pd.MultiIndex | None = None, interactions: Callable[[Sequence[AbstractAgent]], None] | None = None, interpolation: str = "linear", outputs: Callable | None = None, @@ -123,10 +114,6 @@ def __init__( """Subsectors controlled by this object.""" self.technologies: xr.Dataset = technologies """Parameters describing the sector's technologies.""" - self.timeslices: pd.MultiIndex | None = timeslices - """Timeslice at which this sector operates. - If None, it will operate using the timeslice of the input market. - """ self.interpolation: Mapping[str, Any] = { "method": interpolation, "kwargs": {"fill_value": "extrapolate"}, @@ -267,7 +254,6 @@ def group_assets(x: xr.DataArray) -> xr.DataArray: result = xr.Dataset( dict(supply=supply, consumption=consumption, costs=costs) ) - result = self.convert_market_timeslice(result, mca_market.timeslice) result["comm_usage"] = self.technologies.comm_usage.sel( commodity=result.commodity ) @@ -283,7 +269,6 @@ def market_variables(self, market: xr.Dataset, technologies: xr.Dataset) -> Any: from muse.commodities import is_pollutant from muse.costs import annual_levelized_cost_of_energy, supply_cost from muse.quantities import consumption - from muse.timeslices import QuantityType, convert_timeslice from muse.utilities import broadcast_techs years = market.year.values @@ -293,8 +278,6 @@ def market_variables(self, market: xr.Dataset, technologies: xr.Dataset) -> Any: 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, supply, market.prices) @@ -381,29 +364,3 @@ def agents(self) -> Iterator[AbstractAgent]: """Iterator over all agents in the sector.""" for subsector in self.subsectors: yield from subsector.agents - - @staticmethod - def convert_market_timeslice( - market: xr.Dataset, - timeslice: pd.MultiIndex, - intensive: str | tuple[str] = "prices", - ) -> xr.Dataset: - """Converts market from one to another timeslice.""" - from muse.timeslices import QuantityType, convert_timeslice - - if isinstance(intensive, str): - intensive = (intensive,) - - timesliced = {d for d in market.data_vars if "timeslice" in market[d].dims} - intensives = convert_timeslice( - market[list(timesliced.intersection(intensive))], - timeslice, - QuantityType.INTENSIVE, - ) - extensives = convert_timeslice( - market[list(timesliced.difference(intensives.data_vars))], - timeslice, - QuantityType.EXTENSIVE, - ) - others = market[list(set(market.data_vars).difference(timesliced))] - return xr.merge([intensives, extensives, others]) diff --git a/src/muse/timeslices.py b/src/muse/timeslices.py index e2a0c3e66..d22ab791f 100644 --- a/src/muse/timeslices.py +++ b/src/muse/timeslices.py @@ -1,72 +1,27 @@ """Timeslice utility functions.""" __all__ = [ - "reference_timeslice", - "aggregate_transforms", - "convert_timeslice", - "timeslice_projector", + "read_timeslices", + "broadcast_timeslice", + "distribute_timeslice", + "drop_timeslice", "setup_module", - "represent_hours", ] from collections.abc import Mapping, Sequence -from enum import Enum, unique -from typing import Optional, Union +from typing import Union -import xarray as xr from numpy import ndarray from pandas import MultiIndex -from xarray import DataArray, Dataset +from xarray import DataArray TIMESLICE: DataArray = None # type: ignore """Array with the finest timeslice.""" TRANSFORMS: dict[tuple, ndarray] = None # type: ignore """Transforms from each aggregate to the finest timeslice.""" -DEFAULT_TIMESLICE_DESCRIPTION = """ - [timeslices] - winter.weekday.night = 396 - winter.weekday.morning = 396 - winter.weekday.afternoon = 264 - winter.weekday.early-peak = 66 - winter.weekday.late-peak = 66 - winter.weekday.evening = 396 - winter.weekend.night = 156 - winter.weekend.morning = 156 - winter.weekend.afternoon = 156 - winter.weekend.evening = 156 - spring-autumn.weekday.night = 792 - spring-autumn.weekday.morning = 792 - spring-autumn.weekday.afternoon = 528 - spring-autumn.weekday.early-peak = 132 - spring-autumn.weekday.late-peak = 132 - spring-autumn.weekday.evening = 792 - spring-autumn.weekend.night = 300 - spring-autumn.weekend.morning = 300 - spring-autumn.weekend.afternoon = 300 - spring-autumn.weekend.evening = 300 - summer.weekday.night = 396 - summer.weekday.morning = 396 - summer.weekday.afternoon = 264 - summer.weekday.early-peak = 66 - summer.weekday.late-peak = 66 - summer.weekday.evening = 396 - summer.weekend.night = 150 - summer.weekend.morning = 150 - summer.weekend.afternoon = 150 - summer.weekend.evening = 150 - level_names = ["month", "day", "hour"] - [timeslices.aggregates] - all-day = [ - "night", "morning", "afternoon", "early-peak", "late-peak", "evening", "night" - ] - all-week = ["weekday", "weekend"] - all-year = ["winter", "summer", "spring-autumn"] - """ - - -def reference_timeslice( +def read_timeslices( settings: Union[Mapping, str], level_names: Sequence[str] = ("month", "day", "hour"), name: str = "timeslice", @@ -89,8 +44,8 @@ def reference_timeslice( weight of each timeslice. Example: - >>> from muse.timeslices import reference_timeslice - >>> reference_timeslice( + >>> from muse.timeslices import read_timeslices + >>> read_timeslices( ... """ ... [timeslices] ... spring.weekday = 5 @@ -144,436 +99,35 @@ def reference_timeslice( return DataArray(ts, coords={"timeslice": indices}, dims=name) -def aggregate_transforms( - settings: Optional[Union[Mapping, str]] = None, - timeslice: Optional[DataArray] = None, -) -> dict[tuple, ndarray]: - '''Creates dictionary of transforms for aggregate levels. - - The transforms are used to create the projectors towards the finest timeslice. - - Arguments: - timeslice: a ``DataArray`` with the timeslice dimension. - settings: A dictionary mapping the name of an aggregate with the values it - aggregates, or a string that toml will parse as such. If not given, only the - unit transforms are returned. - - Return: - A dictionary of transforms for each possible slice to it's corresponding finest - timeslices. - - Example: - >>> toml = """ - ... [timeslices] - ... spring.weekday = 5 - ... spring.weekend = 2 - ... autumn.weekday = 5 - ... autumn.weekend = 2 - ... winter.weekday = 5 - ... winter.weekend = 2 - ... summer.weekday = 5 - ... summer.weekend = 2 - ... - ... [timeslices.aggregates] - ... spautumn = ["spring", "autumn"] - ... week = ["weekday", "weekend"] - ... """ - >>> from muse.timeslices import reference_timeslice, aggregate_transforms - >>> ref = reference_timeslice(toml) - >>> transforms = aggregate_transforms(toml, ref) - >>> transforms[("spring", "weekend")] - array([0, 1, 0, 0, 0, 0, 0, 0]) - >>> transforms[("spautumn", "weekday")] - array([1, 0, 1, 0, 0, 0, 0, 0]) - >>> transforms[("autumn", "week")].T - array([0, 0, 1, 1, 0, 0, 0, 0]) - >>> transforms[("spautumn", "week")].T - array([1, 1, 1, 1, 0, 0, 0, 0]) - ''' - from itertools import product - - from numpy import identity, sum - from toml import loads - - if timeslice is None: - timeslice = TIMESLICE - if settings is None: - settings = {} - elif isinstance(settings, str): - settings = loads(settings) - - # get timeslice dimension - Id = identity(len(timeslice), dtype=int) - indices = timeslice.get_index("timeslice") - unitvecs: dict[tuple, ndarray] = {index: Id[i] for (i, index) in enumerate(indices)} - if "timeslices" in settings or "aggregates" in settings: - settings = settings.get("timeslices", settings).get("aggregates", {}) - assert isinstance(settings, Mapping) - - assert set(settings).intersection(unitvecs) == set() - levels = [list(level) for level in indices.levels] - for name, equivalent in settings.items(): - matching_levels = [ - set(level).issuperset(equivalent) for level in indices.levels - ] - if sum(matching_levels) == 0: - raise ValueError(f"Could not find matching level for {equivalent}") - elif sum(matching_levels) > 1: - raise ValueError(f"Found more than one matching level for {equivalent}") - level = matching_levels.index(True) - levels[level].append(name) - - result: dict[tuple, ndarray] = {} - for index in set(product(*levels)).difference(unitvecs): - if not any(level in settings for level in index): - continue - agglevels = set(product(*(settings.get(level, [level]) for level in index))) - result[index] = sum( - [unitvecs[agg] for agg in unitvecs if agg in agglevels], axis=0 - ) - result.update(unitvecs) - return result - - def setup_module(settings: Union[str, Mapping]): """Sets up module singletons.""" global TIMESLICE - global TRANSFORMS - TIMESLICE = reference_timeslice(settings) - TRANSFORMS = aggregate_transforms(settings, TIMESLICE) - - -def timeslice_projector( - x: Union[DataArray, MultiIndex], - finest: Optional[DataArray] = None, - transforms: Optional[dict[tuple, ndarray]] = None, -) -> DataArray: - '''Project time-slice to standardized finest time-slices. - - Returns a matrix from the input timeslice ``x`` to the ``finest`` timeslice, using - the input ``transforms``. The latter are a set of transforms that map indices from - one timeslice to indices in another. - - Example: - Lets define the following timeslices and aggregates: - - >>> toml = """ - ... ["timeslices"] - ... winter.weekday.day = 5 - ... winter.weekday.night = 5 - ... winter.weekend.day = 2 - ... winter.weekend.night = 2 - ... winter.weekend.dusk = 1 - ... summer.weekday.day = 5 - ... summer.weekday.night = 5 - ... summer.weekend.day = 2 - ... summer.weekend.night = 2 - ... summer.weekend.dusk = 1 - ... level_names = ["semester", "week", "day"] - ... aggregates.allday = ["day", "night"] - ... """ - >>> from muse.timeslices import ( - ... reference_timeslice, aggregate_transforms - ... ) - >>> ref = reference_timeslice(toml) - >>> transforms = aggregate_transforms(toml, ref) - >>> from pandas import MultiIndex - >>> input_ts = DataArray( - ... [1, 2, 3], - ... coords={ - ... "timeslice": MultiIndex.from_tuples( - ... [ - ... ("winter", "weekday", "allday"), - ... ("winter", "weekend", "dusk"), - ... ("summer", "weekend", "night"), - ... ], - ... names=ref.get_index("timeslice").names, - ... ), - ... }, - ... dims="timeslice" - ... ) - >>> input_ts # doctest: +SKIP - Size: 12B - array([1, 2, 3]) - Coordinates: - * timeslice (timeslice) object 24B MultiIndex - * semester (timeslice) object 24B 'winter' 'winter' 'summer' - * week (timeslice) object 24B 'weekday' 'weekend' 'weekend' - * day (timeslice) object 24B 'allday' 'dusk' 'night' - - The input timeslice does not have to be complete. In any case, we can now - compute a transform, i.e. a matrix that will take this timeslice and transform - it to the equivalent times in the finest timeslice: - - >>> from muse.timeslices import timeslice_projector - >>> timeslice_projector(input_ts, ref, transforms) # doctest: +SKIP - Size: 120B - array([[1, 0, 0], - [1, 0, 0], - [0, 0, 0], - [0, 0, 0], - [0, 1, 0], - [0, 0, 0], - [0, 0, 0], - [0, 0, 0], - [0, 0, 1], - [0, 0, 0]]) - Coordinates: - * finest_timeslice (finest_timeslice) object 80B MultiIndex - * finest_semester (finest_timeslice) object 80B 'winter' ... 'summer' - * finest_week (finest_timeslice) object 80B 'weekday' ... 'weekend' - * finest_day (finest_timeslice) object 80B 'day' 'night' ... 'dusk' - * timeslice (timeslice) object 24B MultiIndex - * semester (timeslice) object 24B 'winter' 'winter' 'summer' - * week (timeslice) object 24B 'weekday' 'weekend' 'weekend' - * day (timeslice) object 24B 'allday' 'dusk' 'night' + TIMESLICE = read_timeslices(settings) - It is possible to give as input an array which does not have a timeslice of its - own: - >>> nots = DataArray([5.0, 1.0, 2.0], dims="a", coords={'a': [1, 2, 3]}) - >>> timeslice_projector(nots, ref, transforms).T # doctest: +SKIP - Size: 40B - array([[1, 1, 1, 1, 1, 1, 1, 1, 1, 1]]) - Coordinates: - * finest_timeslice (finest_timeslice) object 80B MultiIndex - * finest_semester (finest_timeslice) object 80B 'winter' ... 'summer' - * finest_week (finest_timeslice) object 80B 'weekday' ... 'weekend' - * finest_day (finest_timeslice) object 80B 'day' 'night' ... 'dusk' - Dimensions without coordinates: timeslice - ''' - from numpy import concatenate, ones_like - from xarray import DataArray - - if finest is None: - global TIMESLICE - finest = TIMESLICE - if transforms is None: - global TRANSFORMS - transforms = TRANSFORMS - - index = finest.get_index("timeslice") - index = index.set_names(f"finest_{u}" for u in index.names) - - if isinstance(x, MultiIndex): - timeslices = x - elif "timeslice" in x.dims: - timeslices = x.get_index("timeslice") - else: - return DataArray( - ones_like(finest, dtype=int)[:, None], - coords={"finest_timeslice": index}, - dims=("finest_timeslice", "timeslice"), - ) - - return DataArray( - concatenate([transforms[index][:, None] for index in timeslices], axis=1), - coords={"finest_timeslice": index, "timeslice": timeslices}, - dims=("finest_timeslice", "timeslice"), - name="projector", - ) - - -@unique -class QuantityType(Enum): - """Underlying transformation when performing time-slice conversion. - - The meaning of a quantity vs the time-slice can be different: - - - intensive: when extending the period of interest, quantities should be - added together. For instance the number of hours should be summed across - months. - - extensive: when extending the period of interest, quantities should be - broadcasted. For instance when extending a price from a one week period to - a two week period, the price should remain the same. Going in the opposite - direction (reducing the length of the time period), quantities should be - averaged. - """ - - INTENSIVE = "intensive" - EXTENSIVE = "extensive" - - -def convert_timeslice( - x: Union[DataArray, Dataset], - ts: Union[DataArray, Dataset, MultiIndex], - quantity: Union[QuantityType, str] = QuantityType.EXTENSIVE, - finest: Optional[DataArray] = None, - transforms: Optional[dict[tuple, ndarray]] = None, -) -> Union[DataArray, Dataset]: - '''Adjusts the timeslice of x to match that of ts. - - The conversion can be done in on of two ways, depending on whether the - quantity is extensive or intensive. See `QuantityType`. - - Example: - Lets define three timeslices from finest, to fine, to rough: - - >>> toml = """ - ... ["timeslices"] - ... winter.weekday.day = 5 - ... winter.weekday.night = 5 - ... winter.weekend.day = 2 - ... winter.weekend.night = 2 - ... summer.weekday.day = 5 - ... summer.weekday.night = 5 - ... summer.weekend.day = 2 - ... summer.weekend.night = 2 - ... level_names = ["semester", "week", "day"] - ... aggregates.allday = ["day", "night"] - ... aggregates.allweek = ["weekend", "weekday"] - ... aggregates.allyear = ["winter", "summer"] - ... """ - >>> from muse.timeslices import setup_module - >>> from muse.readers import read_timeslices - >>> setup_module(toml) - >>> finest_ts = read_timeslices() - >>> fine_ts = read_timeslices(dict(week=["allweek"])) - >>> rough_ts = read_timeslices(dict(semester=["allyear"], day=["allday"])) - - Lets also define to other data-arrays to demonstrate how we can play with - dimensions: +def broadcast_timeslice(x, ts=None): + from xarray import Coordinates - >>> from numpy import array - >>> x = DataArray( - ... [5, 2, 3], - ... coords={'a': array([1, 2, 3], dtype="int64")}, - ... dims='a' - ... ) - >>> y = DataArray([1, 1, 2], coords={'b': ["d", "e", "f"]}, dims='b') + if ts is None: + ts = TIMESLICE - We can now easily convert arrays with different dimensions. First, lets check - conversion from an array with no timeslices: + # If x already has timeslices, check that it is matches the reference timeslice. + if "timeslice" in x.dims: + if x.timeslice.reset_coords(drop=True).equals(ts.timeslice): + return x + raise ValueError("x has incompatible timeslicing.") - >>> from xarray import ones_like - >>> from muse.timeslices import convert_timeslice, QuantityType - >>> z = convert_timeslice(x, finest_ts, QuantityType.EXTENSIVE) - >>> z.round(6) - Size: 192B - array([[0.892857, 0.357143, 0.535714], - [0.892857, 0.357143, 0.535714], - [0.357143, 0.142857, 0.214286], - [0.357143, 0.142857, 0.214286], - [0.892857, 0.357143, 0.535714], - [0.892857, 0.357143, 0.535714], - [0.357143, 0.142857, 0.214286], - [0.357143, 0.142857, 0.214286]]) - Coordinates: - * timeslice (timeslice) object 64B MultiIndex - * semester (timeslice) object 64B 'winter' 'winter' ... 'summer' 'summer' - * week (timeslice) object 64B 'weekday' 'weekday' ... 'weekend' - * day (timeslice) object 64B 'day' 'night' 'day' ... 'day' 'night' - * a (a) int64 24B 1 2 3 - >>> z.sum("timeslice") - Size: 24B - array([5., 2., 3.]) - Coordinates: - * a (a) int64 24B 1 2 3 - - As expected, the sum over timeslices recovers the original array. - - In the case of an intensive quantity without a timeslice dimension, the - operation does not do anything: - - >>> convert_timeslice([1, 2], rough_ts, QuantityType.INTENSIVE) - [1, 2] - - More interesting is the conversion between different timeslices: - - >>> from xarray import zeros_like - >>> zfine = x + y + zeros_like(fine_ts.timeslice, dtype=int) - >>> zrough = convert_timeslice(zfine, rough_ts) - >>> zrough.round(6) - Size: 144B - array([[[17.142857, 17.142857, 20. ], - [ 8.571429, 8.571429, 11.428571], - [11.428571, 11.428571, 14.285714]], - - [[ 6.857143, 6.857143, 8. ], - [ 3.428571, 3.428571, 4.571429], - [ 4.571429, 4.571429, 5.714286]]]) - Coordinates: - * timeslice (timeslice) object 16B MultiIndex - * semester (timeslice) object 16B 'allyear' 'allyear' - * week (timeslice) object 16B 'weekday' 'weekend' - * day (timeslice) object 16B 'allday' 'allday' - * a (a) int64 24B 1 2 3 - * b (b) >> from numpy import all - >>> all(zfine.sum("timeslice").round(6) == zrough.sum("timeslice").round(6)) - Size: 1B - array(True) - Or that the ratio of weekdays to weekends makes sense: - >>> weekdays = ( - ... zrough - ... .unstack("timeslice") - ... .sel(week="weekday") - ... .stack(timeslice=["semester", "day"]) - ... .squeeze() - ... ) - >>> weekend = ( - ... zrough - ... .unstack("timeslice") - ... .sel(week="weekend") - ... .stack(timeslice=["semester", "day"]) - ... .squeeze() - ... ) - >>> bool(all((weekend * 5).round(6) == (weekdays * 2).round(6))) - True - ''' - if finest is None: - global TIMESLICE - finest = TIMESLICE - if transforms is None: - global TRANSFORMS - transforms = TRANSFORMS - if hasattr(ts, "timeslice"): - ts = ts.timeslice - has_ts = "timeslice" in getattr(x, "dims", ()) - same_ts = has_ts and len(ts) == len(x.timeslice) and x.timeslice.equals(ts) - if same_ts or ((not has_ts) and quantity == QuantityType.INTENSIVE): - return x - quantity = QuantityType(quantity) - proj0 = timeslice_projector(x, finest=finest, transforms=transforms) - proj1 = timeslice_projector(ts, finest=finest, transforms=transforms) - if quantity is QuantityType.EXTENSIVE: - finest = finest.rename(timeslice="finest_timeslice") - index = finest.get_index("finest_timeslice") - index = index.set_names(f"finest_{u}" for u in index.names) - mindex_coords = xr.Coordinates.from_pandas_multiindex(index, "finest_timeslice") - finest = finest.drop_vars(list(finest.coords)).assign_coords(mindex_coords) - proj0 = proj0 * finest - proj0 = proj0 / proj0.sum("finest_timeslice") - elif quantity is QuantityType.INTENSIVE: - proj1 = proj1 / proj1.sum("finest_timeslice") - - new_names = {"timeslice": "final_ts"} | { - c: f"{c}_ts" for c in proj1.timeslice.coords if c != "timeslice" - } - P = (proj1.rename(**new_names) * proj0).sum("finest_timeslice") - - final_names = {"final_ts": "timeslice"} | { - c: c.replace("_ts", "") for c in P.final_ts.coords if c != "final_ts" - } - return (P * x).sum("timeslice").rename(**final_names) - - -def represent_hours( - timeslices: DataArray, nhours: Union[int, float] = 8765.82 -) -> DataArray: - """Number of hours per timeslice. +def distribute_timeslice(x, ts=None): + if ts is None: + ts = TIMESLICE - Arguments: - timeslices: The timeslice for which to compute the number of hours - nhours: The total number of hours represented in the timeslice. Defaults to the - average number of hours in year. - """ - return convert_timeslice(DataArray([nhours]), timeslices).squeeze() + extensive = broadcast_timeslice(x, ts) + return extensive * (ts / broadcast_timeslice(ts.sum())) def drop_timeslice(data: DataArray) -> DataArray: @@ -585,6 +139,3 @@ def drop_timeslice(data: DataArray) -> DataArray: return data return data.drop_vars(data.timeslice.indexes) - - -setup_module(DEFAULT_TIMESLICE_DESCRIPTION) diff --git a/tests/conftest.py b/tests/conftest.py index 0a178d03c..f7d52adb8 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -110,20 +110,53 @@ def compare_dirs(actual_dir, expected_dir, **kwargs): return compare_dirs -@fixture -def save_timeslice_globals(): - from muse import timeslices - - old = timeslices.TIMESLICE, timeslices.TRANSFORMS - yield - timeslices.TIMESLICE, timeslices.TRANSFORMS = old - - @fixture def default_timeslice_globals(): - from muse.timeslices import DEFAULT_TIMESLICE_DESCRIPTION, setup_module + from muse.timeslices import setup_module + + default_timeslices = """ + [timeslices] + winter.weekday.night = 396 + winter.weekday.morning = 396 + winter.weekday.afternoon = 264 + winter.weekday.early-peak = 66 + winter.weekday.late-peak = 66 + winter.weekday.evening = 396 + winter.weekend.night = 156 + winter.weekend.morning = 156 + winter.weekend.afternoon = 156 + winter.weekend.evening = 156 + spring-autumn.weekday.night = 792 + spring-autumn.weekday.morning = 792 + spring-autumn.weekday.afternoon = 528 + spring-autumn.weekday.early-peak = 132 + spring-autumn.weekday.late-peak = 132 + spring-autumn.weekday.evening = 792 + spring-autumn.weekend.night = 300 + spring-autumn.weekend.morning = 300 + spring-autumn.weekend.afternoon = 300 + spring-autumn.weekend.evening = 300 + summer.weekday.night = 396 + summer.weekday.morning = 396 + summer.weekday.afternoon = 264 + summer.weekday.early-peak = 66 + summer.weekday.late-peak = 66 + summer.weekday.evening = 396 + summer.weekend.night = 150 + summer.weekend.morning = 150 + summer.weekend.afternoon = 150 + summer.weekend.evening = 150 + level_names = ["month", "day", "hour"] + + [timeslices.aggregates] + all-day = [ + "night", "morning", "afternoon", "early-peak", "late-peak", "evening", "night" + ] + all-week = ["weekday", "weekend"] + all-year = ["winter", "summer", "spring-autumn"] + """ - setup_module(DEFAULT_TIMESLICE_DESCRIPTION) + setup_module(default_timeslices) @fixture @@ -133,22 +166,6 @@ def timeslice(default_timeslice_globals) -> Dataset: return TIMESLICE -@fixture -def other_timeslice() -> Dataset: - from pandas import MultiIndex - - months = ["winter", "spring-autumn", "summer"] - days = ["all-week", "all-week", "all-week"] - hour = ["all-day", "all-day", "all-day"] - coordinates = MultiIndex.from_arrays( - [months, days, hour], names=("month", "day", "hour") - ) - result = Dataset(coords={"timeslice": coordinates}) - result["represent_hours"] = ("timeslice", [2920, 2920, 2920]) - result = result.set_coords("represent_hours") - return result - - @fixture def coords() -> Mapping: """Technoeconomics coordinates.""" diff --git a/tests/test_constraints.py b/tests/test_constraints.py index 20a433808..3681821cf 100644 --- a/tests/test_constraints.py +++ b/tests/test_constraints.py @@ -20,14 +20,6 @@ def residential(model): return examples.sector("residential", model=model) -@fixture(params=["timeslice_as_list", "timeslice_as_multindex"]) -def timeslices(market, request): - timeslice = market.timeslice - if request.param == "timeslice_as_multindex": - timeslice = _as_list(timeslice) - return timeslice - - @fixture def technologies(residential): return residential.technologies.squeeze("region") @@ -61,7 +53,6 @@ def lpcosts(technologies, market, costs): return lp_costs( technologies.interp(year=market.year.min() + 5).drop_vars("year"), costs=costs, - timeslices=market.timeslice, ) @@ -71,13 +62,12 @@ def assets(residential): @fixture -def market_demand(assets, technologies, market): +def market_demand(assets, technologies): from muse.quantities import maximum_production return 0.8 * maximum_production( technologies.interp(year=2025), assets.capacity.sel(year=2025).groupby("technology").sum("asset"), - timeslices=market.timeslice, ).rename(technology="asset") @@ -208,12 +198,12 @@ def test_lp_constraint(constraint, lpcosts): assert result.b.values == approx(0) -def test_to_scipy_adapter_maxprod(technologies, costs, max_production, timeslices): +def test_to_scipy_adapter_maxprod(technologies, costs, max_production): from muse.constraints import ScipyAdapter, lp_costs technologies = technologies.interp(year=2025) - adapter = ScipyAdapter.factory(technologies, costs, timeslices, max_production) + adapter = ScipyAdapter.factory(technologies, costs, max_production) assert set(adapter.kwargs) == {"c", "A_ub", "b_ub", "A_eq", "b_eq", "bounds"} assert adapter.bounds == (0, np.inf) assert adapter.A_eq is None @@ -224,7 +214,7 @@ def test_to_scipy_adapter_maxprod(technologies, costs, max_production, timeslice assert adapter.b_ub.size == adapter.A_ub.shape[0] assert adapter.c.size == adapter.A_ub.shape[1] - lpcosts = lp_costs(technologies, costs, timeslices) + lpcosts = lp_costs(technologies, costs) capsize = lpcosts.capacity.size prodsize = lpcosts.production.size assert adapter.c.size == capsize + prodsize @@ -233,12 +223,12 @@ def test_to_scipy_adapter_maxprod(technologies, costs, max_production, timeslice assert adapter.A_ub[:, capsize:] == approx(np.eye(prodsize)) -def test_to_scipy_adapter_demand(technologies, costs, demand_constraint, timeslices): +def test_to_scipy_adapter_demand(technologies, costs, demand_constraint): from muse.constraints import ScipyAdapter, lp_costs technologies = technologies.interp(year=2025) - adapter = ScipyAdapter.factory(technologies, costs, timeslices, demand_constraint) + adapter = ScipyAdapter.factory(technologies, costs, demand_constraint) assert set(adapter.kwargs) == {"c", "A_ub", "b_ub", "A_eq", "b_eq", "bounds"} assert adapter.bounds == (0, np.inf) assert adapter.A_ub is not None @@ -251,7 +241,7 @@ def test_to_scipy_adapter_demand(technologies, costs, demand_constraint, timesli assert adapter.b_ub.size == adapter.A_ub.shape[0] assert adapter.c.size == adapter.A_ub.shape[1] - lpcosts = lp_costs(technologies, costs, timeslices) + lpcosts = lp_costs(technologies, costs) capsize = lpcosts.capacity.size prodsize = lpcosts.production.size assert adapter.c.size == capsize + prodsize @@ -265,15 +255,13 @@ def test_to_scipy_adapter_demand(technologies, costs, demand_constraint, timesli def test_to_scipy_adapter_max_capacity_expansion( - technologies, costs, max_capacity_expansion, timeslices + technologies, costs, max_capacity_expansion ): from muse.constraints import ScipyAdapter, lp_costs technologies = technologies.interp(year=2025) - adapter = ScipyAdapter.factory( - technologies, costs, timeslices, max_capacity_expansion - ) + adapter = ScipyAdapter.factory(technologies, costs, max_capacity_expansion) assert set(adapter.kwargs) == {"c", "A_ub", "b_ub", "A_eq", "b_eq", "bounds"} assert adapter.bounds == (0, np.inf) assert adapter.A_ub is not None @@ -287,7 +275,7 @@ def test_to_scipy_adapter_max_capacity_expansion( assert adapter.c.size == adapter.A_ub.shape[1] assert adapter.c.ndim == 1 - lpcosts = lp_costs(technologies, costs, timeslices) + lpcosts = lp_costs(technologies, costs) capsize = lpcosts.capacity.size prodsize = lpcosts.production.size assert adapter.c.size == capsize + prodsize @@ -297,12 +285,12 @@ def test_to_scipy_adapter_max_capacity_expansion( assert set(adapter.A_ub[:, :capsize].flatten()) == {0.0, 1.0} -def test_to_scipy_adapter_no_constraint(technologies, costs, timeslices): +def test_to_scipy_adapter_no_constraint(technologies, costs): from muse.constraints import ScipyAdapter, lp_costs technologies = technologies.interp(year=2025) - adapter = ScipyAdapter.factory(technologies, costs, timeslices) + adapter = ScipyAdapter.factory(technologies, costs) assert set(adapter.kwargs) == {"c", "A_ub", "b_ub", "A_eq", "b_eq", "bounds"} assert adapter.bounds == (0, np.inf) assert adapter.A_ub is None @@ -311,18 +299,18 @@ def test_to_scipy_adapter_no_constraint(technologies, costs, timeslices): assert adapter.b_eq is None assert adapter.c.ndim == 1 - lpcosts = lp_costs(technologies, costs, timeslices) + lpcosts = lp_costs(technologies, costs) capsize = lpcosts.capacity.size prodsize = lpcosts.production.size assert adapter.c.size == capsize + prodsize -def test_back_to_muse_capacity(technologies, costs, timeslices): +def test_back_to_muse_capacity(technologies, costs): from muse.constraints import ScipyAdapter, lp_costs technologies = technologies.interp(year=2025) - lpcosts = lp_costs(technologies, costs, timeslices) + lpcosts = lp_costs(technologies, costs) data = ScipyAdapter._unified_dataset(technologies, lpcosts) lpquantity = ScipyAdapter._selected_quantity(data, "capacity") assert set(lpquantity.dims) == {"d(asset)", "d(replacement)"} @@ -332,12 +320,12 @@ def test_back_to_muse_capacity(technologies, costs, timeslices): assert (copy == lpcosts.capacity).all() -def test_back_to_muse_production(technologies, costs, timeslices): +def test_back_to_muse_production(technologies, costs): from muse.constraints import ScipyAdapter, lp_costs technologies = technologies.interp(year=2025) - lpcosts = lp_costs(technologies, costs, timeslices) + lpcosts = lp_costs(technologies, costs) data = ScipyAdapter._unified_dataset(technologies, lpcosts) lpquantity = ScipyAdapter._selected_quantity(data, "production") assert set(lpquantity.dims) == { @@ -352,11 +340,11 @@ def test_back_to_muse_production(technologies, costs, timeslices): assert (copy == lpcosts.production).all() -def test_back_to_muse_all(technologies, costs, timeslices, rng: np.random.Generator): +def test_back_to_muse_all(technologies, costs, rng: np.random.Generator): from muse.constraints import ScipyAdapter, lp_costs technologies = technologies.interp(year=2025) - lpcosts = lp_costs(technologies, costs, timeslices) + lpcosts = lp_costs(technologies, costs) data = ScipyAdapter._unified_dataset(technologies, lpcosts) lpcapacity = ScipyAdapter._selected_quantity(data, "capacity") @@ -383,11 +371,11 @@ def test_back_to_muse_all(technologies, costs, timeslices, rng: np.random.Genera assert (copy.production == lpcosts.production).all() -def test_scipy_adapter_back_to_muse(technologies, costs, timeslices, rng): +def test_scipy_adapter_back_to_muse(technologies, costs, rng): from muse.constraints import ScipyAdapter, lp_costs technologies = technologies.interp(year=2025) - lpcosts = lp_costs(technologies, costs, timeslices) + lpcosts = lp_costs(technologies, costs) data = ScipyAdapter._unified_dataset(technologies, lpcosts) lpcapacity = ScipyAdapter._selected_quantity(data, "capacity") @@ -406,7 +394,7 @@ def test_scipy_adapter_back_to_muse(technologies, costs, timeslices, rng): ) ) - adapter = ScipyAdapter.factory(technologies, costs, timeslices) + adapter = ScipyAdapter.factory(technologies, costs) assert (adapter.to_muse(x).capacity == lpcosts.capacity).all() assert (adapter.to_muse(x).production == lpcosts.production).all() @@ -422,14 +410,12 @@ def _as_list(data: Union[xr.DataArray, xr.Dataset]) -> Union[xr.DataArray, xr.Da return data -def test_scipy_adapter_standard_constraints( - technologies, costs, constraints, timeslices -): +def test_scipy_adapter_standard_constraints(technologies, costs, constraints): from muse.constraints import ScipyAdapter technologies = technologies.interp(year=2025) - adapter = ScipyAdapter.factory(technologies, costs, timeslices, *constraints) + adapter = ScipyAdapter.factory(technologies, costs, *constraints) maxprod = next(cs for cs in constraints if cs.name == "max_production") maxcapa = next(cs for cs in constraints if cs.name == "max capacity expansion") demand = next(cs for cs in constraints if cs.name == "demand") diff --git a/tests/test_costs.py b/tests/test_costs.py index 66c89fa79..1099e2312 100644 --- a/tests/test_costs.py +++ b/tests/test_costs.py @@ -18,15 +18,13 @@ def _capacity(technologies, demand_share): @fixture -def _production(technologies, _capacity, demand_share): - from muse.timeslices import QuantityType, convert_timeslice +def _production(technologies, _capacity): + from muse.timeslices import broadcast_timeslice, distribute_timeslice production = ( - _capacity - * convert_timeslice( - technologies.fixed_outputs, demand_share.timeslice, QuantityType.EXTENSIVE - ) - * technologies.utilization_factor + broadcast_timeslice(_capacity) + * distribute_timeslice(technologies.fixed_outputs) + * broadcast_timeslice(technologies.utilization_factor) ) return production diff --git a/tests/test_demand_share.py b/tests/test_demand_share.py index 9df92cf35..fc1b14602 100644 --- a/tests/test_demand_share.py +++ b/tests/test_demand_share.py @@ -8,20 +8,20 @@ def matching_market(technologies, stock, timeslice): """A market which matches stocks exactly.""" return ( - _matching_market(technologies, stock, timeslice) + _matching_market(technologies, stock) .interp(year=[2010, 2015, 2020, 2025]) .transpose("timeslice", "region", "commodity", "year") ) -def _matching_market(technologies, stock, timeslice): +def _matching_market(technologies, stock): """A market which matches stocks exactly.""" from numpy.random import random from muse.quantities import consumption, maximum_production market = xr.Dataset() - production = maximum_production(technologies, stock.capacity, timeslices=timeslice) + production = maximum_production(technologies, stock.capacity) market["supply"] = production.sum("asset") market["consumption"] = drop_timeslice( consumption(technologies, production).sum("asset") + market.supply @@ -131,7 +131,6 @@ def test_new_retro_accounting_identity(technologies, stock, market): maximum_production( capacity=stock.capacity.interp(year=2015), technologies=technologies, - timeslices=market.timeslice, ) .groupby("region") .sum("asset") @@ -159,7 +158,6 @@ def method(capacity): return decommissioning_demand( technologies.sel(region="USA"), capacity, - matching_market.timeslice, year=[2012, 2017], ) @@ -196,7 +194,6 @@ def method(capacity): return 0 * decommissioning_demand( technologies.sel(region="USA"), capacity, - matching_market.timeslice, year=[2012, 2017], ) @@ -234,8 +231,8 @@ def test_new_retro_demand_share(technologies, coords, market, timeslice, stock_f asia_stock = stock_factory(coords, technologies).expand_dims(region=["ASEAN"]) usa_stock = stock_factory(coords, technologies).expand_dims(region=["USA"]) - asia_market = _matching_market(technologies, asia_stock, timeslice) - usa_market = _matching_market(technologies, usa_stock, timeslice) + asia_market = _matching_market(technologies, asia_stock) + usa_market = _matching_market(technologies, usa_stock) market = xr.concat((asia_market, usa_market), dim="region") market.consumption.loc[{"year": 2031}] *= 2 @@ -288,8 +285,8 @@ def test_standard_demand_share(technologies, coords, market, timeslice, stock_fa asia_stock = stock_factory(coords, technologies).expand_dims(region=["ASEAN"]) usa_stock = stock_factory(coords, technologies).expand_dims(region=["USA"]) - asia_market = _matching_market(technologies, asia_stock, timeslice) - usa_market = _matching_market(technologies, usa_stock, timeslice) + asia_market = _matching_market(technologies, asia_stock) + usa_market = _matching_market(technologies, usa_stock) market = xr.concat((asia_market, usa_market), dim="region") market.consumption.loc[{"year": 2031}] *= 2 @@ -341,8 +338,8 @@ def test_unmet_forecast_demand(technologies, coords, timeslice, stock_factory): asia_stock = stock_factory(coords, technologies).expand_dims(region=["ASEAN"]) usa_stock = stock_factory(coords, technologies).expand_dims(region=["USA"]) - asia_market = _matching_market(technologies, asia_stock, timeslice) - usa_market = _matching_market(technologies, usa_stock, timeslice) + asia_market = _matching_market(technologies, asia_stock) + usa_market = _matching_market(technologies, usa_stock) market = xr.concat((asia_market, usa_market), dim="region") current_year = market.year[0] diff --git a/tests/test_fullsim_regression.py b/tests/test_fullsim_regression.py index 4fe089b83..43609bcf3 100644 --- a/tests/test_fullsim_regression.py +++ b/tests/test_fullsim_regression.py @@ -5,7 +5,6 @@ from muse.examples import available_examples -@mark.usefixtures("save_timeslice_globals") @mark.regression @mark.example @mark.parametrize("model", available_examples()) @@ -40,7 +39,6 @@ def available_tutorials(): return [d.parent for d in base_path.rglob("*/input") if d.is_dir()] -@mark.usefixtures("save_timeslice_globals") @mark.regression @mark.tutorial @mark.parametrize("tutorial_path", available_tutorials()) diff --git a/tests/test_quantities.py b/tests/test_quantities.py index 28fa533cf..8e1238342 100644 --- a/tests/test_quantities.py +++ b/tests/test_quantities.py @@ -29,14 +29,14 @@ def production( ) -> xr.DataArray: from numpy.random import random - from muse.timeslices import QuantityType, convert_timeslice + from muse.timeslices import broadcast_timeslice, distribute_timeslice comms = xr.DataArray( random(len(technologies.commodity)), coords={"commodity": technologies.commodity}, dims="commodity", ) - return capacity * convert_timeslice(comms, timeslice, QuantityType.EXTENSIVE) + return broadcast_timeslice(capacity) * distribute_timeslice(comms) def make_array(array): @@ -48,21 +48,16 @@ def test_supply_enduse(technologies, capacity, timeslice): """End-use part of supply.""" from muse.commodities import is_enduse from muse.quantities import maximum_production, supply - from muse.timeslices import QuantityType, convert_timeslice - production = maximum_production(technologies, capacity, timeslice) - demand = convert_timeslice( - production.sum("asset") + 1, timeslice, QuantityType.EXTENSIVE - ) + production = maximum_production(technologies, capacity) + demand = production.sum("asset") + 1 spl = supply(capacity, demand, technologies).where( is_enduse(technologies.comm_usage), 0 ) assert (abs(spl - production) < 1e-12).all() assert (spl.sum("asset") < demand).all() - demand = convert_timeslice( - production.sum("asset") * 0.7, timeslice, QuantityType.EXTENSIVE - ) + demand = production.sum("asset") * 0.7 spl = supply(capacity, demand, technologies).where( is_enduse(technologies.comm_usage), 0 ) @@ -77,7 +72,7 @@ def test_supply_emissions(technologies, capacity, timeslice): from muse.commodities import is_enduse, is_pollutant from muse.quantities import emission, maximum_production, supply - production = maximum_production(technologies, capacity, timeslices=timeslice) + production = maximum_production(technologies, capacity) spl = supply(capacity, production.sum("asset") + 1, technologies) msn = emission(spl.where(is_enduse(spl.comm_usage), 0), technologies.fixed_outputs) actual, expected = xr.broadcast( @@ -137,7 +132,7 @@ def test_decommissioning_demand(technologies, capacity, timeslice): capacity.loc[{"year": 2015}] = forecast = 1.0 technologies.fixed_outputs[:] = fouts = 0.5 technologies.utilization_factor[:] = ufac = 0.4 - decom = decommissioning_demand(technologies, capacity, timeslice, years) + decom = decommissioning_demand(technologies, capacity, years) assert set(decom.dims) == {"asset", "commodity", "year", "timeslice"} assert decom.sel(commodity=is_enduse(technologies.comm_usage)).sum( "timeslice" @@ -147,6 +142,7 @@ def test_decommissioning_demand(technologies, capacity, timeslice): def test_consumption_no_flex(technologies, production, market): from muse.commodities import is_enduse, is_fuel from muse.quantities import consumption + from muse.timeslices import broadcast_timeslice fins = ( technologies.fixed_inputs.where(is_fuel(technologies.comm_usage), 0) @@ -159,7 +155,7 @@ def test_consumption_no_flex(technologies, production, market): ) services = technologies.commodity.sel(commodity=is_enduse(technologies.comm_usage)) expected = ( - (production.rename(commodity="comm_in") * fins) + (production.rename(commodity="comm_in") * broadcast_timeslice(fins)) .sel(comm_in=production.commodity.isin(services).rename(commodity="comm_in")) .sum("comm_in") ) @@ -178,7 +174,7 @@ def test_consumption_with_flex(technologies, production, market, timeslice): from muse.commodities import is_enduse, is_fuel from muse.quantities import consumption - from muse.timeslices import QuantityType, convert_timeslice + from muse.timeslices import broadcast_timeslice, distribute_timeslice techs = technologies.copy() techs.fixed_inputs[:] = 0 @@ -204,12 +200,14 @@ def one_dim(dimension): timeslice = one_dim(market.timeslice) commodity = one_dim(market.commodity) - prices = timeslice + commodity + year * region + prices = ( + timeslice + + broadcast_timeslice(commodity) + + broadcast_timeslice(year) * broadcast_timeslice(region) + ) assert set(prices.dims) == set(market.prices.dims) noenduse = ~is_enduse(techs.comm_usage) - production = convert_timeslice( - asset * year + commodity, timeslice, QuantityType.EXTENSIVE - ) + production = distribute_timeslice(asset * year + commodity) production.loc[{"commodity": noenduse}] = 0 actual = consumption(technologies, production, prices) @@ -247,7 +245,7 @@ def one_dim(dimension): def test_production_aggregate_asset_view( - capacity: xr.DataArray, technologies: xr.Dataset, timeslice: xr.DataArray + capacity: xr.DataArray, technologies: xr.Dataset ): """Production when capacity has format of agent.sector. @@ -265,7 +263,7 @@ def test_production_aggregate_asset_view( technologies.fixed_outputs[:] = 1 technologies.utilization_factor[:] = 1 - prod = maximum_production(technologies, capacity, timeslices=timeslice) + prod = maximum_production(technologies, capacity) assert set(prod.dims) == set(capacity.dims).union({"commodity", "timeslice"}) assert prod.sel(commodity=~enduses).values == approx(0) prod, expected = xr.broadcast( @@ -275,7 +273,7 @@ def test_production_aggregate_asset_view( technologies.fixed_outputs[:] = fouts = 2 technologies.utilization_factor[:] = ufact = 0.5 - prod = maximum_production(technologies, capacity, timeslices=timeslice) + prod = maximum_production(technologies, capacity) assert prod.sel(commodity=~enduses).values == approx(0) assert set(prod.dims) == set(capacity.dims).union({"commodity", "timeslice"}) prod, expected = xr.broadcast( @@ -285,7 +283,7 @@ def test_production_aggregate_asset_view( technologies.fixed_outputs[:] = fouts = 3 technologies.utilization_factor[:] = ufact = 0.5 - prod = maximum_production(technologies, capacity, timeslices=timeslice) + prod = maximum_production(technologies, capacity) assert prod.sel(commodity=~enduses).values == approx(0) assert set(prod.dims) == set(capacity.dims).union({"commodity", "timeslice"}) prod, expected = xr.broadcast( @@ -301,7 +299,7 @@ def test_production_agent_asset_view( from muse.utilities import coords_to_multiindex, reduce_assets capacity = coords_to_multiindex(reduce_assets(capacity)).unstack("asset").fillna(0) - test_production_aggregate_asset_view(capacity, technologies, timeslice) + test_production_aggregate_asset_view(capacity, technologies) def test_capacity_in_use(production: xr.DataArray, technologies: xr.Dataset): @@ -402,15 +400,15 @@ def test_min_production(technologies, capacity, timeslice): # If no minimum service factor is defined, the minimum production is zero assert "minimum_service_factor" not in technologies - production = minimum_production(technologies, capacity, timeslice) + production = minimum_production(technologies, capacity) assert (production == 0).all() # If minimum service factor is defined, then the minimum production is not zero # and it is less than the maximum production technologies["minimum_service_factor"] = 0.5 - production = minimum_production(technologies, capacity, timeslice) + production = minimum_production(technologies, capacity) assert not (production == 0).all() - assert (production <= maximum_production(technologies, capacity, timeslice)).all() + assert (production <= maximum_production(technologies, capacity)).all() def test_supply_capped_by_min_service(technologies, capacity, timeslice): @@ -419,7 +417,7 @@ def test_supply_capped_by_min_service(technologies, capacity, timeslice): from muse.quantities import minimum_production, supply technologies["minimum_service_factor"] = 0.3 - minprod = minimum_production(technologies, capacity, timeslice) + minprod = minimum_production(technologies, capacity) # If minimum service factor is defined, then the minimum production is not zero assert not (minprod == 0).all() diff --git a/tests/test_readers.py b/tests/test_readers.py index a23fc1650..b4400a06a 100644 --- a/tests/test_readers.py +++ b/tests/test_readers.py @@ -134,13 +134,6 @@ def test_check_foresight(settings: dict): check_foresight(settings) -def test_check_time_slices(settings: dict): - """Tests the check_budget_parameters function.""" - from muse.readers.toml import check_time_slices - - check_time_slices(settings) - - def test_check_global_data_files(settings: dict, user_data_files): """Tests the check_global_data_files function.""" from muse.readers.toml import check_global_data_files @@ -491,8 +484,8 @@ def test_read_technodata_timeslices(tmp_path): assert isinstance(data, xr.Dataset) assert set(data.dims) == {"technology", "region", "year", "timeslice"} assert dict(data.dtypes) == dict( - utilization_factor=np.float64, - minimum_service_factor=np.float64, + utilization_factor=np.int64, + minimum_service_factor=np.int64, ) assert list(data.coords["technology"].values) == ["gasCCGT", "windturbine"] assert list(data.coords["region"].values) == ["R1"] @@ -609,11 +602,9 @@ def test_read_csv_agent_parameters(default_model): def test_read_initial_market(default_model): from muse.readers.csv import read_initial_market - from muse.readers.toml import read_settings - settings = read_settings(default_model / "settings.toml") path = default_model / "input" / "Projections.csv" - data = read_initial_market(path, timeslices=settings.timeslices) + data = read_initial_market(path) assert isinstance(data, xr.Dataset) assert set(data.dims) == {"region", "year", "commodity", "timeslice"} diff --git a/tests/test_timeslices.py b/tests/test_timeslices.py index 7f478d027..d1dd4e72f 100644 --- a/tests/test_timeslices.py +++ b/tests/test_timeslices.py @@ -1,10 +1,8 @@ """Test timeslice utilities.""" -from pytest import approx, fixture +from pytest import fixture from xarray import DataArray -from muse.timeslices import QuantityType, convert_timeslice - @fixture def toml(): @@ -28,16 +26,9 @@ def toml(): @fixture def reference(toml): - from muse.timeslices import reference_timeslice - - return reference_timeslice(toml) - + from muse.timeslices import read_timeslices -@fixture -def transforms(toml, reference): - from muse.timeslices import aggregate_transforms - - return aggregate_transforms(toml, reference) + return read_timeslices(toml) @fixture @@ -60,60 +51,10 @@ def timeslice_dataarray(reference): ) -def test_convert_extensive_timeslice(reference, timeslice_dataarray, transforms): - z = convert_timeslice( - timeslice_dataarray, reference, finest=reference, transforms=transforms - ) - assert z.shape == reference.shape - assert z.values == approx( - [ - float( - timeslice_dataarray[0] * reference[0] / (reference[0] + reference[1]) - ), - float( - timeslice_dataarray[0] * reference[1] / (reference[0] + reference[1]) - ), - 0, - 0, - float(timeslice_dataarray[1]), - 0, - 0, - 0, - float(timeslice_dataarray[2]), - 0, - ] - ) - - -def test_convert_intensive_timeslice(reference, timeslice_dataarray, transforms): - z = convert_timeslice( - timeslice_dataarray, - reference, - finest=reference, - transforms=transforms, - quantity=QuantityType.INTENSIVE, - ) - - assert z.values == approx( - [ - float(timeslice_dataarray[0]), - float(timeslice_dataarray[0]), - 0, - 0, - float(timeslice_dataarray[1]), - 0, - 0, - 0, - float(timeslice_dataarray[2]), - 0, - ] - ) - - -def test_reference_timeslice(): +def test_read_timeslices(): from toml import loads - from muse.timeslices import reference_timeslice + from muse.timeslices import read_timeslices inputs = loads( """ @@ -139,7 +80,7 @@ def test_reference_timeslice(): """ ) - ts = reference_timeslice(inputs) + ts = read_timeslices(inputs) assert isinstance(ts, DataArray) assert "timeslice" in ts.coords @@ -147,10 +88,10 @@ def test_reference_timeslice(): def test_no_overlap(): from pytest import raises - from muse.timeslices import reference_timeslice + from muse.timeslices import read_timeslices with raises(ValueError): - reference_timeslice( + read_timeslices( """ [timeslices] winter.weekday.night = 396 @@ -163,97 +104,6 @@ def test_no_overlap(): ) -def test_aggregate_transforms_no_aggregates(): - from itertools import product - - from numpy import ndarray, zeros - - from muse.timeslices import aggregate_transforms, reference_timeslice - - reference = reference_timeslice( - """ - [timeslices] - spring.weekday = 396 - spring.weekend = 396 - autumn.weekday = 396 - autumn.weekend = 156 - """ - ) - - vectors = aggregate_transforms(timeslice=reference) - assert isinstance(vectors, dict) - assert set(vectors) == set(product(["spring", "autumn"], ["weekday", "weekend"])) - for i in range(reference.shape[0]): - index = reference.timeslice[i].values.tolist() - vector = vectors[index] - assert isinstance(vector, ndarray) - expected = zeros(reference.shape, dtype=int) - expected[i] = 1 - assert vector == approx(expected) - - -def test_aggregate_transforms_with_aggregates(): - from itertools import product - - from toml import loads - - from muse.timeslices import aggregate_transforms, reference_timeslice - - toml = loads( - """ - [timeslices] - spring.weekday.day = 396 - spring.weekday.night = 396 - spring.weekend.day = 156 - spring.weekend.night = 156 - summer.weekday.day = 396 - summer.weekday.night = 396 - summer.weekend.day = 156 - summer.weekend.night = 156 - autumn.weekday.day = 396 - autumn.weekday.night = 396 - autumn.weekend.day = 156 - autumn.weekend.night = 156 - winter.weekday.day = 396 - winter.weekday.night = 396 - winter.weekend.day = 156 - winter.weekend.night = 156 - - [timeslices.aggregates] - springautumn = ["spring", "autumn"] - allday = ["day", "night"] - week = ["weekday", "weekend"] - """ - ) - reference = reference_timeslice(toml) - - vectors = aggregate_transforms(toml, reference) - assert isinstance(vectors, dict) - assert set(vectors) == set( - product( - ["winter", "spring", "summer", "autumn", "springautumn"], - ["weekend", "weekday", "week"], - ["day", "night", "allday"], - ) - ) - - def to_bitstring(x): - return "".join(x.astype(str)) - - assert to_bitstring(vectors[("spring", "weekday", "night")]) == "0100000000000000" - assert to_bitstring(vectors[("autumn", "weekday", "night")]) == "0000000001000000" - assert to_bitstring(vectors[("spring", "weekend", "night")]) == "0001000000000000" - assert to_bitstring(vectors[("autumn", "weekend", "night")]) == "0000000000010000" - assert ( - to_bitstring(vectors[("springautumn", "weekday", "night")]) - == "0100000001000000" - ) - assert to_bitstring(vectors[("spring", "week", "night")]) == "0101000000000000" - assert ( - to_bitstring(vectors[("springautumn", "week", "night")]) == "0101000001010000" - ) - - def test_drop_timeslice(timeslice_dataarray): from muse.timeslices import drop_timeslice diff --git a/tests/test_trade.py b/tests/test_trade.py index bafa07db9..2398b47d9 100644 --- a/tests/test_trade.py +++ b/tests/test_trade.py @@ -102,14 +102,13 @@ def test_lp_costs(): technologies = examples.technodata("power", model="trade") search_space = examples.search_space("power", model="trade") - timeslices = examples.sector("power", model="trade").timeslices costs = ( search_space * np.arange(np.prod(search_space.shape)).reshape(search_space.shape) * xr.ones_like(technologies.dst_region) ) - lpcosts = lp_costs(technologies.sel(year=2020, drop=True), costs, timeslices) + lpcosts = lp_costs(technologies.sel(year=2020, drop=True), costs) assert "capacity" in lpcosts.data_vars assert "production" in lpcosts.data_vars assert set(lpcosts.capacity.dims) == {"agent", "replacement", "dst_region"}