diff --git a/src/muse/agents/agent.py b/src/muse/agents/agent.py index 8d7b503ae..c323f593e 100644 --- a/src/muse/agents/agent.py +++ b/src/muse/agents/agent.py @@ -323,14 +323,15 @@ def next( ).values search = search.sel(asset=condtechs) + # Get technology parameters for the current year + techs = self.filter_input(technologies, year=current_year) + # Calculate constraints constraints = self.constraints( - search.demand, - self.assets, - search.search_space, - market, - technologies, - year=current_year, + demand=search.demand, + search_space=search.search_space, + capacity=market.capacity, + technologies=techs, ) # Calculate investments diff --git a/src/muse/constraints.py b/src/muse/constraints.py index 40026e4a2..a2b94d5e5 100644 --- a/src/muse/constraints.py +++ b/src/muse/constraints.py @@ -68,30 +68,26 @@ @register_constraints def constraints( demand: xr.DataArray, - assets: xr.Dataset, search_space: xr.DataArray, - market: xr.Dataset, + capacity: xr.DataArray, technologies: xr.Dataset, - year: Optional[int] = None, **kwargs, ) -> Constraint: pass demand: - The demand for the sectors products. In practice it is a demand share obtained in - :py:mod:`~muse.demand_share`. It is a data-array with dimensions including `asset`, - `commodity`, `timeslice`. -assets: - The capacity of the assets owned by the agent. + The demand for the sector's products in the investment year. In practice it is a + demand share obtained in :py:mod:`~muse.demand_share`. It is a data-array with + dimensions `asset`, `commodity` and `timeslice`. +capacity: + A data-array with dimensions `technology` and `year` defining the existing capacity + of each technology in the current year and investment year. search_space: A matrix `asset` vs `replacement` technology defining which replacement technologies will be considered for each existing asset. -market: - The market as obtained from the MCA. technologies: - Technodata characterizing the competing technologies. -year: - current year. + Technodata characterizing the competing technologies in the current year. Must have + dimensions `technology`, `commodity`. May also have `timeslice` dimension. ``**kwargs``: Any other parameter. """ @@ -117,9 +113,9 @@ def constraints( from muse.registration import registrator from muse.timeslices import drop_timeslice -CAPACITY_DIMS = "asset", "replacement", "region" +CAPACITY_DIMS = "asset", "replacement" """Default dimensions for capacity decision variables.""" -PRODUCT_DIMS = "commodity", "timeslice", "region" +PRODUCT_DIMS = "commodity", "timeslice" """Default dimensions for product decision variables.""" @@ -147,7 +143,7 @@ class ConstraintKind(Enum): CONSTRAINT_SIGNATURE = Callable[ - [xr.DataArray, xr.Dataset, xr.DataArray, xr.Dataset, xr.Dataset, KwArg(Any)], + [xr.DataArray, xr.DataArray, xr.DataArray, xr.Dataset, KwArg(Any)], Optional[Constraint], ] """Basic signature for functions producing constraints. @@ -173,15 +169,29 @@ def register_constraints(function: CONSTRAINT_SIGNATURE) -> CONSTRAINT_SIGNATURE @wraps(function) def decorated( demand: xr.DataArray, - assets: xr.Dataset, + capacity: xr.DataArray, search_space: xr.DataArray, - market: xr.Dataset, technologies: xr.Dataset, **kwargs, ) -> Constraint | None: """Computes and standardizes a constraint.""" + # Check inputs + assert set(demand.dims) == {"asset", "commodity", "timeslice"} + assert set(capacity.dims) == {"technology", "year"} + assert set(search_space.dims) == {"asset", "replacement"} + assert {"technology", "commodity"}.issubset(set(technologies.dims)) + assert set(technologies.dims).issubset({"technology", "commodity", "timeslice"}) + assert set(search_space.replacement.values) == set( + technologies.technology.values + ) + assert set(search_space.asset.values) == set(demand.asset.values) + constraint = function( # type: ignore - demand, assets, search_space, market, technologies, **kwargs + demand, + capacity, + search_space, + technologies, + **kwargs, ) if constraint is not None: if "kind" not in constraint.attrs: @@ -243,16 +253,19 @@ def normalize(x) -> MutableMapping: def constraints( demand: xr.DataArray, - assets: xr.Dataset, + capacity: xr.Dataset, search_space: xr.DataArray, - market: xr.Dataset, technologies: xr.Dataset, - year: int | None = None, + **kwargs, ) -> list[Constraint]: - if year is None: - year = int(market.year.min()) constraints = [ - function(demand, assets, search_space, market, technologies, year=year) + function( + demand, + capacity, + search_space, + technologies, + **kwargs, + ) for function in constraint_closures ] return [constraint for constraint in constraints if constraint is not None] @@ -263,13 +276,10 @@ def constraints( @register_constraints def max_capacity_expansion( demand: xr.DataArray, - assets: xr.Dataset, + capacity: xr.Dataset, search_space: xr.DataArray, - market: xr.Dataset, technologies: xr.Dataset, - year: int | None = None, - forecast: int | None = None, - interpolation: str = "linear", + **kwargs, ) -> Constraint: r"""Max-capacity addition, max-capacity growth, and capacity limits constraints. @@ -306,41 +316,12 @@ def max_capacity_expansion( \Gamma_t^{r, i} \geq 0 """ - from muse.utilities import filter_input, reduce_assets + from muse.utilities import filter_input - if year is None: - year = int(market.year.min()) - if forecast is None and len(getattr(market, "year", [])) <= 1: - forecast = 5 - elif forecast is None: - forecast = next(int(u) for u in sorted(market.year - year) if u > 0) - forecast_year = year + forecast - - capacity = ( - reduce_assets( - assets.capacity, - coords={"technology", "region"}.intersection(assets.capacity.coords), - ) - .interp(year=[year, forecast_year], method=interpolation) - .ffill("year") + # Expand capacity to cover all replacement technologies + capacity = capacity.rename(technology="replacement").reindex_like( + search_space, fill_value=0 ) - # case with technology and region in asset dimension - if capacity.region.dims != (): - names = [u for u in capacity.asset.coords if capacity[u].dims == ("asset",)] - index = pd.MultiIndex.from_arrays( - [capacity[u].values for u in names], names=names - ) - mindex_coords = xr.Coordinates.from_pandas_multiindex(index, "asset") - capacity = capacity.drop_vars(names).assign_coords(mindex_coords) - capacity = capacity.unstack("asset", fill_value=0).rename( - technology=search_space.replacement.name - ) - # case with only technology in asset dimension - else: - capacity = cast(xr.DataArray, capacity.set_index(asset="technology")).rename( - asset=search_space.replacement.name - ) - capacity = capacity.reindex_like(search_space.replacement, fill_value=0) replacement = search_space.replacement replacement = replacement.drop_vars( @@ -351,36 +332,27 @@ def max_capacity_expansion( ["max_capacity_addition", "max_capacity_growth", "total_capacity_limit"] ], technology=replacement, - year=year, ).drop_vars("technology") - regions = getattr(capacity, "region", None) - if regions is not None and "region" in technologies.dims: - techs = techs.sel(region=regions) - add_cap = techs.max_capacity_addition * forecast + time_frame = capacity.year[1] - capacity.year[0] + add_cap = techs.max_capacity_addition * time_frame limit = techs.total_capacity_limit - forecasted = capacity.sel(year=forecast_year, drop=True) + forecasted = capacity.isel(year=1, drop=True) total_cap = (limit - forecasted).clip(min=0).rename("total_cap") max_growth = techs.max_capacity_growth - initial = capacity.sel(year=year, drop=True) + initial = capacity.isel(year=0, drop=True) - growth_cap = initial * (max_growth * forecast + 1) - forecasted + growth_cap = initial * (max_growth * time_frame + 1) - forecasted growth_cap = growth_cap.where(growth_cap > 0, total_cap) zero_cap = add_cap.where(add_cap < total_cap, total_cap) with_growth = zero_cap.where(zero_cap < growth_cap, growth_cap) b = with_growth.where(initial > 0, zero_cap) - if b.region.dims == (): - capa = 1 - elif "dst_region" in b.dims: - b = b.rename(region="src_region") - capa = search_space.agent.region == b.src_region - return xr.Dataset( - dict(b=b, capacity=capa), + dict(b=b, capacity=1), attrs=dict(kind=ConstraintKind.UPPER_BOUND, name="max capacity expansion"), ) @@ -388,21 +360,16 @@ def max_capacity_expansion( @register_constraints def demand( demand: xr.DataArray, - assets: xr.Dataset, + capacity: xr.Dataset, search_space: xr.DataArray, - market: xr.Dataset, technologies: xr.Dataset, - year: int | None = None, - forecast: int = 5, - interpolation: str = "linear", + **kwargs, ) -> Constraint: """Constraints production to meet demand.""" from muse.commodities import is_enduse enduse = technologies.commodity.sel(commodity=is_enduse(technologies.comm_usage)) b = demand.sel(commodity=demand.commodity.isin(enduse)) - if "region" in b.dims and "dst_region" in assets.dims: - b = b.rename(region="dst_region") assert "year" not in b.dims return xr.Dataset( dict(b=b, production=1), attrs=dict(kind=ConstraintKind.LOWER_BOUND) @@ -412,12 +379,10 @@ def demand( @register_constraints def search_space( demand: xr.DataArray, - assets: xr.Dataset, + capacity: xr.Dataset, search_space: xr.DataArray, - market: xr.Dataset, technologies: xr.Dataset, - year: int | None = None, - forecast: int = 5, + **kwargs, ) -> Constraint | None: """Removes disabled technologies.""" if search_space.all(): @@ -432,11 +397,10 @@ def search_space( @register_constraints def max_production( demand: xr.DataArray, - assets: xr.Dataset, + capacity: xr.Dataset, search_space: xr.DataArray, - market: xr.Dataset, technologies: xr.Dataset, - year: int | None = None, + **kwargs, ) -> Constraint: """Constructs constraint between capacity and maximum production. @@ -448,8 +412,6 @@ def max_production( from muse.commodities import is_enduse from muse.timeslices import QuantityType, convert_timeslice - if year is None: - year = int(market.year.min()) commodities = technologies.commodity.sel( commodity=is_enduse(technologies.comm_usage) ) @@ -457,9 +419,7 @@ def max_production( replacement = replacement.drop_vars( [u for u in replacement.coords if u not in replacement.dims] ) - kwargs = dict(technology=replacement, year=year, commodity=commodities) - if "region" in search_space.coords and "region" in technologies.dims: - kwargs["region"] = search_space.region + kwargs = dict(technology=replacement, commodity=commodities) techs = ( technologies[["fixed_outputs", "utilization_factor"]] .sel(**kwargs) @@ -468,7 +428,7 @@ def max_production( capacity = ( convert_timeslice( techs.fixed_outputs, - market.timeslice, + demand.timeslice, QuantityType.EXTENSIVE, ) * techs.utilization_factor @@ -478,21 +438,6 @@ def max_production( capacity = capacity.expand_dims(asset=search_space.asset) production = ones_like(capacity) b = zeros_like(production) - # Include maxaddition constraint in max production to match region-dst_region - if "dst_region" in assets.dims: - b = b.expand_dims(dst_region=assets.dst_region) - capacity = capacity.rename(region="src_region") - production = production.rename(region="src_region") - maxadd = technologies.max_capacity_addition.rename(region="src_region") - if "year" in maxadd.dims: - maxadd = maxadd.sel(year=year) - - 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 - b = b.rename(region="src_region") return xr.Dataset( dict(capacity=-cast(np.ndarray, capacity), production=production, b=b), attrs=dict(kind=ConstraintKind.UPPER_BOUND), @@ -502,11 +447,10 @@ def max_production( @register_constraints def demand_limiting_capacity( demand_: xr.DataArray, - assets: xr.Dataset, + capacity: xr.Dataset, search_space: xr.DataArray, - market: xr.Dataset, technologies: xr.Dataset, - year: int | None = None, + **kwargs, ) -> Constraint: """Limits the maximum combined capacity to match the demand. @@ -521,12 +465,8 @@ def demand_limiting_capacity( accounts for the utilization factor of the technologies. """ # We start with the maximum production constraint and the demand constraint - capacity_constraint = max_production( - demand_, assets, search_space, market, technologies, year=year - ) - demand_constraint = demand( - demand_, assets, search_space, market, technologies, year=year - ) + capacity_constraint = max_production(demand_, capacity, search_space, technologies) + demand_constraint = demand(demand_, capacity, search_space, technologies) # We are interested in the demand of the demand constraint and the capacity of the # capacity constraint. @@ -722,11 +662,10 @@ def modify_dlc(technologies: xr.DataArray, demand: xr.DataArray) -> xr.DataArray @register_constraints def minimum_service( demand: xr.DataArray, - assets: xr.Dataset, + capacity: xr.Dataset, search_space: xr.DataArray, - market: xr.Dataset, technologies: xr.Dataset, - year: int | None = None, + **kwargs, ) -> Constraint | None: """Constructs constraint between capacity and minimum service.""" from xarray import ones_like, zeros_like @@ -738,8 +677,6 @@ def minimum_service( return None if np.all(technologies["minimum_service_factor"] == 0): return None - if year is None: - year = int(market.year.min()) commodities = technologies.commodity.sel( commodity=is_enduse(technologies.comm_usage) ) @@ -747,18 +684,16 @@ def minimum_service( replacement = replacement.drop_vars( [u for u in replacement.coords if u not in replacement.dims] ) - kwargs = dict(technology=replacement, year=year, commodity=commodities) - if "region" in search_space.coords and "region" in technologies.dims: - kwargs["region"] = assets.region + kwargs = dict(technology=replacement, commodity=commodities) 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, + demand.timeslice, QuantityType.EXTENSIVE, ) * techs.minimum_service_factor @@ -836,8 +771,6 @@ def lp_costs( technology=technologies.technology.isin(costs.replacement), ) - if "region" in technologies.fixed_outputs.dims and "region" in ts_costs.coords: - selection["region"] = ts_costs.region fouts = technologies.fixed_outputs.sel(selection).rename(technology="replacement") # lpcosts.dims = Frozen({'asset': 2, @@ -857,43 +790,6 @@ def lp_costs( return xr.Dataset(dict(capacity=costs, production=production)) -def merge_lp( - costs: xr.Dataset, *constraints: Constraint -) -> tuple[xr.Dataset, list[Constraint]]: - """Unify coordinate systems of costs and constraints. - - In practice, this function brings costs and constraints into a single xr.Dataset and - then splits things up again. This ensures the dimensions are not only compatible, - but also such that that their order in memory is the same. - """ - from xarray import merge - - data = merge( - [costs] - + [ - constraint.rename( - b=f"b{i}", capacity=f"capacity{i}", production=f"production{i}" - ) - for i, constraint in enumerate(constraints) - ] - ) - - unified_costs = cast(xr.Dataset, data[["capacity", "production"]]) - unified_constraints = [ - xr.Dataset( - { - "capacity": data[f"capacity{i}"], - "production": data[f"production{i}"], - "b": data[f"b{i}"], - }, - attrs=constraint.attrs, - ) - for i, constraint in enumerate(constraints) - ] - - return unified_costs, unified_constraints - - def lp_constraint(constraint: Constraint, lpcosts: xr.Dataset) -> Constraint: """Transforms the constraint to LP data. @@ -964,11 +860,11 @@ def lp_constraint_matrix( >>> from muse import constraints as cs >>> res = examples.sector("residential", model="medium") >>> technologies = res.technologies - >>> market = examples.residential_market("medium") >>> search = examples.search_space("residential", model="medium") >>> assets = next(a.assets for a in res.agents) + >>> capacity = reduce_assets(assets.capacity, coords=("region", "technology")) >>> demand = None # not used in max production - >>> constraint = cs.max_production(demand, assets, search, market, + >>> constraint = cs.max_production(demand, capacity, search, ... technologies) # noqa: E501 >>> lpcosts = cs.lp_costs( ... ( @@ -978,7 +874,7 @@ def lp_constraint_matrix( ... .sel(region=assets.region) ... ), ... costs=search * np.arange(np.prod(search.shape)).reshape(search.shape), - ... timeslices=market.timeslice, + ... timeslices=demand.timeslice, ... ) For a simple example, we can first check the case where b is scalar. The result @@ -1104,6 +1000,7 @@ class ScipyAdapter: >>> market = examples.residential_market("medium") >>> search = examples.search_space("residential", model="medium") >>> assets = next(a.assets for a in res.agents) + >>> capacity = reduce_assets(assets.capacity, coords=("region", "technology")) >>> market_demand = 0.8 * maximum_production( ... res.technologies.interp(year=2025), ... assets.capacity.sel(year=2025).groupby("technology").sum("asset"), @@ -1111,7 +1008,7 @@ class ScipyAdapter: ... ).rename(technology="asset") >>> costs = search * np.arange(np.prod(search.shape)).reshape(search.shape) >>> constraint = cs.max_capacity_expansion( - ... market_demand, assets, search, market, res.technologies, + ... market_demand, capacity, search, res.technologies, ... ) The constraint acts over capacity decision variables only: @@ -1143,7 +1040,7 @@ class ScipyAdapter: >>> technologies = res.technologies.interp(year=market.year.min() + 5) >>> inputs = cs.ScipyAdapter.factory( - ... technologies, costs, market.timeslice, constraint + ... technologies, costs, demand.timeslice, constraint ... ) The decision variables are always constrained between zero and infinity: @@ -1168,7 +1065,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, demand.timeslice) >>> capsize = lpcosts.capacity.size >>> prodsize = lpcosts.production.size >>> assert inputs.c.size == capsize + prodsize diff --git a/src/muse/sectors/subsector.py b/src/muse/sectors/subsector.py index 03798af0f..b3284bbb0 100644 --- a/src/muse/sectors/subsector.py +++ b/src/muse/sectors/subsector.py @@ -99,20 +99,31 @@ def aggregate_lp( ) # Calculate existing capacity - agent_market = market.copy() - agent_market["capacity"] = ( - reduce_assets(assets.capacity, coords=("region", "technology")) - .interp(year=market.year, method="linear", kwargs={"fill_value": 0.0}) - .swap_dims(dict(asset="technology")) - ) + capacity = reduce_assets( + assets.capacity, coords=("region", "technology") + ).interp(year=market.year, method="linear", kwargs={"fill_value": 0.0}) # Increment each agent (perform investments) for agent in self.agents: + agent_market = market.copy() + + # Select appropriate data for the region + techs = technologies.sel(region=agent.region) + agent_market = agent_market.sel(region=agent.region) + capa = ( + capacity.sel(asset=capacity.region == agent.region) + if "asset" in capacity.region.dims + else capacity + ) + agent_market["capacity"] = capa.swap_dims(dict(asset="technology")) + + # Select demands for the agent if "agent" in demands.coords: share = demands.sel(asset=demands.agent == agent.uuid) else: share = demands - agent.next(technologies, agent_market, share, time_period=time_period) + + agent.next(techs, agent_market, share, time_period=time_period) @classmethod def factory( diff --git a/tests/test_constraints.py b/tests/test_constraints.py index 20a433808..cbe42d2eb 100644 --- a/tests/test_constraints.py +++ b/tests/test_constraints.py @@ -3,9 +3,12 @@ import numpy as np import pandas as pd import xarray as xr -from pytest import approx, fixture +from pytest import approx, fixture, mark from muse.timeslices import drop_timeslice +from muse.utilities import reduce_assets + +pytestmark = mark.usefixtures("default_timeslice_globals") @fixture @@ -20,24 +23,9 @@ 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") - - -@fixture -def market(model): - from muse import examples - - return examples.residential_market(model) + return residential.technologies.squeeze("region").sel(year=2020) @fixture @@ -55,13 +43,13 @@ def costs(search_space): @fixture -def lpcosts(technologies, market, costs): +def lpcosts(technologies, timeslices, costs): from muse.constraints import lp_costs return lp_costs( - technologies.interp(year=market.year.min() + 5).drop_vars("year"), + technologies, costs=costs, - timeslices=market.timeslice, + timeslices=timeslices, ) @@ -71,66 +59,145 @@ def assets(residential): @fixture -def market_demand(assets, technologies, market): +def _capacity(assets): + return ( + reduce_assets(assets.capacity, coords=("region", "technology")) + .sel(year=[2020, 2025]) + .swap_dims(dict(asset="technology")) + ) + + +@fixture +def timeslices(market): + return market.timeslice + + +@fixture +def _demand(assets, technologies, timeslices): from muse.quantities import maximum_production return 0.8 * maximum_production( - technologies.interp(year=2025), + technologies, assets.capacity.sel(year=2025).groupby("technology").sum("asset"), - timeslices=market.timeslice, + timeslices=timeslices, ).rename(technology="asset") @fixture -def max_production(market_demand, assets, search_space, market, technologies): +def max_production(_demand, _capacity, search_space, technologies): from muse.constraints import max_production - return max_production(market_demand, assets, search_space, market, technologies) - - -@fixture -def constraint(max_production): - return max_production + return max_production(_demand, _capacity, search_space, technologies) @fixture -def demand_constraint(market_demand, assets, search_space, market, technologies): +def demand_constraint(_demand, _capacity, search_space, technologies): from muse.constraints import demand - return demand(market_demand, assets, search_space, market, technologies) + return demand(_demand, _capacity, search_space, technologies) @fixture -def max_capacity_expansion(market_demand, assets, search_space, market, technologies): +def max_capacity_expansion(_demand, _capacity, search_space, technologies): from muse.constraints import max_capacity_expansion - return max_capacity_expansion( - market_demand, assets, search_space, market, technologies - ) + return max_capacity_expansion(_demand, _capacity, search_space, technologies) @fixture -def demand_limiting_capacity(market_demand, assets, search_space, market, technologies): +def demand_limiting_capacity(_demand, _capacity, search_space, technologies): from muse.constraints import demand_limiting_capacity - return demand_limiting_capacity( - market_demand, assets, search_space, market, technologies + return demand_limiting_capacity(_demand, _capacity, search_space, technologies) + + +@fixture +def minimum_service(_demand, _capacity, search_space, technologies): + """Constraint with no MSF applied.""" + from muse.constraints import minimum_service + + return minimum_service(_demand, _capacity, search_space, technologies) + + +@fixture +def minimum_service2(_demand, _capacity, search_space, technologies): + """Constraint with MSF applied.""" + from muse.constraints import minimum_service + + minimum_service_factor = 0.4 * xr.ones_like(technologies.technology, dtype=float) + technologies["minimum_service_factor"] = minimum_service_factor + return minimum_service(_demand, _capacity, search_space, technologies) + + +@fixture +def constraint(max_production): + return max_production + + +def test_demand_constraint(demand_constraint): + assert demand_constraint.production == 1 + assert demand_constraint.capacity == 0 + assert set(demand_constraint.b.dims) == {"asset", "timeslice", "commodity"} + + +def test_minimum_service_constraint(minimum_service, minimum_service2): + # Test 1: without MSF + assert minimum_service is None + + # Test 2: with MSF + assert isinstance(minimum_service2, xr.Dataset) + dims = {"replacement", "asset", "commodity", "timeslice"} + assert set(minimum_service2.capacity.dims) == dims + assert set(minimum_service2.production.dims) == dims + assert set(minimum_service2.b.dims) == dims + + +def test_max_capacity_expansion_constraint(max_capacity_expansion): + assert max_capacity_expansion.capacity == 1 + assert max_capacity_expansion.production == 0 + assert max_capacity_expansion.b.dims == ("replacement",) + assert max_capacity_expansion.b.shape == (4,) + assert max_capacity_expansion.b.values == approx([50, 12, 12, 50]) + assert ( + max_capacity_expansion.replacement + == ["estove", "gasboiler", "gasstove", "heatpump"] + ).all() + + +def test_max_production_constraint(max_production): + dims = {"replacement", "asset", "commodity", "timeslice"} + assert set(max_production.capacity.dims) == dims + assert set(max_production.production.dims) == dims + assert set(max_production.b.dims) == dims + assert (max_production.capacity <= 0).all() + + +def test_demand_limiting_capacity( + demand_limiting_capacity, max_production, demand_constraint +): + # Check values are appropriate compared to other constraints + assert demand_limiting_capacity.capacity.values == approx( + -max_production.capacity.max("timeslice").values + if "timeslice" in max_production.capacity.dims + else -max_production.capacity.values + ) + assert demand_limiting_capacity.production == 0 + assert demand_limiting_capacity.b.values == approx( + demand_constraint.b.max("timeslice").values + if "timeslice" in demand_constraint.b.dims + else demand_constraint.b.values ) -@fixture(params=["timeslice_as_list", "timeslice_as_multindex"]) -def constraints(request, market_demand, assets, search_space, market, technologies): +@fixture +def constraints(_demand, _capacity, search_space, technologies): from muse import constraints as cs constraints = [ - cs.max_production(market_demand, assets, search_space, market, technologies), - cs.demand(market_demand, assets, search_space, market, technologies), - cs.max_capacity_expansion( - market_demand, assets, search_space, market, technologies - ), + cs.max_production(_demand, _capacity, search_space, technologies), + cs.demand(_demand, _capacity, search_space, technologies), + cs.max_capacity_expansion(_demand, _capacity, search_space, technologies), ] - if request.param == "timeslice_as_multindex": - constraints = [_as_list(cs) for cs in constraints] return constraints @@ -208,10 +275,10 @@ def test_lp_constraint(constraint, lpcosts): assert result.b.values == approx(0) -def test_to_scipy_adapter_maxprod(technologies, costs, max_production, timeslices): - from muse.constraints import ScipyAdapter, lp_costs - - technologies = technologies.interp(year=2025) +def test_to_scipy_adapter_maxprod( + technologies, costs, max_production, timeslices, lpcosts +): + from muse.constraints import ScipyAdapter adapter = ScipyAdapter.factory(technologies, costs, timeslices, max_production) assert set(adapter.kwargs) == {"c", "A_ub", "b_ub", "A_eq", "b_eq", "bounds"} @@ -224,7 +291,6 @@ 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) capsize = lpcosts.capacity.size prodsize = lpcosts.production.size assert adapter.c.size == capsize + prodsize @@ -233,10 +299,10 @@ 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): - from muse.constraints import ScipyAdapter, lp_costs - - technologies = technologies.interp(year=2025) +def test_to_scipy_adapter_demand( + technologies, costs, demand_constraint, lpcosts, timeslices +): + from muse.constraints import ScipyAdapter adapter = ScipyAdapter.factory(technologies, costs, timeslices, demand_constraint) assert set(adapter.kwargs) == {"c", "A_ub", "b_ub", "A_eq", "b_eq", "bounds"} @@ -251,7 +317,6 @@ 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) capsize = lpcosts.capacity.size prodsize = lpcosts.production.size assert adapter.c.size == capsize + prodsize @@ -265,11 +330,9 @@ 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, timeslices, lpcosts ): - from muse.constraints import ScipyAdapter, lp_costs - - technologies = technologies.interp(year=2025) + from muse.constraints import ScipyAdapter adapter = ScipyAdapter.factory( technologies, costs, timeslices, max_capacity_expansion @@ -287,7 +350,6 @@ 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) capsize = lpcosts.capacity.size prodsize = lpcosts.production.size assert adapter.c.size == capsize + prodsize @@ -297,10 +359,8 @@ 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): - from muse.constraints import ScipyAdapter, lp_costs - - technologies = technologies.interp(year=2025) +def test_to_scipy_adapter_no_constraint(technologies, costs, timeslices, lpcosts): + from muse.constraints import ScipyAdapter adapter = ScipyAdapter.factory(technologies, costs, timeslices) assert set(adapter.kwargs) == {"c", "A_ub", "b_ub", "A_eq", "b_eq", "bounds"} @@ -311,18 +371,14 @@ 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) capsize = lpcosts.capacity.size prodsize = lpcosts.production.size assert adapter.c.size == capsize + prodsize -def test_back_to_muse_capacity(technologies, costs, timeslices): - from muse.constraints import ScipyAdapter, lp_costs - - technologies = technologies.interp(year=2025) +def test_back_to_muse_capacity(technologies, lpcosts): + from muse.constraints import ScipyAdapter - lpcosts = lp_costs(technologies, costs, timeslices) data = ScipyAdapter._unified_dataset(technologies, lpcosts) lpquantity = ScipyAdapter._selected_quantity(data, "capacity") assert set(lpquantity.dims) == {"d(asset)", "d(replacement)"} @@ -332,12 +388,9 @@ def test_back_to_muse_capacity(technologies, costs, timeslices): assert (copy == lpcosts.capacity).all() -def test_back_to_muse_production(technologies, costs, timeslices): - from muse.constraints import ScipyAdapter, lp_costs - - technologies = technologies.interp(year=2025) +def test_back_to_muse_production(technologies, lpcosts): + from muse.constraints import ScipyAdapter - lpcosts = lp_costs(technologies, costs, timeslices) data = ScipyAdapter._unified_dataset(technologies, lpcosts) lpquantity = ScipyAdapter._selected_quantity(data, "production") assert set(lpquantity.dims) == { @@ -352,11 +405,8 @@ 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): - from muse.constraints import ScipyAdapter, lp_costs - - technologies = technologies.interp(year=2025) - lpcosts = lp_costs(technologies, costs, timeslices) +def test_back_to_muse_all(technologies, rng, lpcosts): + from muse.constraints import ScipyAdapter data = ScipyAdapter._unified_dataset(technologies, lpcosts) lpcapacity = ScipyAdapter._selected_quantity(data, "capacity") @@ -383,11 +433,8 @@ 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): - from muse.constraints import ScipyAdapter, lp_costs - - technologies = technologies.interp(year=2025) - lpcosts = lp_costs(technologies, costs, timeslices) +def test_scipy_adapter_back_to_muse(technologies, costs, timeslices, rng, lpcosts): + from muse.constraints import ScipyAdapter data = ScipyAdapter._unified_dataset(technologies, lpcosts) lpcapacity = ScipyAdapter._selected_quantity(data, "capacity") @@ -427,8 +474,6 @@ def test_scipy_adapter_standard_constraints( ): from muse.constraints import ScipyAdapter - technologies = technologies.interp(year=2025) - adapter = ScipyAdapter.factory(technologies, costs, timeslices, *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") @@ -452,65 +497,3 @@ def test_scipy_solver(technologies, costs, constraints): ) assert isinstance(solution, xr.DataArray) assert set(solution.dims) == {"asset", "replacement"} - - -def test_minimum_service( - market_demand, assets, search_space, market, technologies, costs, constraints -): - from muse.constraints import minimum_service - - minimum_service_constraint = minimum_service( - market_demand, assets, search_space, market, technologies - ) - - # test it is none (when appropriate) - assert minimum_service_constraint is None - - # add the column to technologies - minimum_service_factor = 0.4 * xr.ones_like(technologies.technology, dtype=float) - technologies["minimum_service_factor"] = minimum_service_factor - - # append minimum_service_constraint to constraints - minimum_service_constraint = minimum_service( - market_demand, assets, search_space, market, technologies - ) - constraints.append(minimum_service_constraint) - - # test that it is no longer none - assert isinstance(minimum_service_constraint, xr.Dataset) - - -def test_max_capacity_expansion(max_capacity_expansion): - assert max_capacity_expansion.capacity == 1 - assert max_capacity_expansion.production == 0 - assert max_capacity_expansion.b.dims == ("replacement",) - assert max_capacity_expansion.b.shape == (4,) - assert max_capacity_expansion.b.values == approx([50, 12, 12, 50]) - assert ( - max_capacity_expansion.replacement - == ["estove", "gasboiler", "gasstove", "heatpump"] - ).all() - - -def test_max_production(max_production): - dims = {"replacement", "asset", "commodity", "timeslice"} - assert set(max_production.capacity.dims) == dims - assert set(max_production.production.dims) == dims - assert set(max_production.b.dims) == dims - assert (max_production.capacity <= 0).all() - - -def test_demand_limiting_capacity( - demand_limiting_capacity, max_production, demand_constraint -): - assert demand_limiting_capacity.capacity.values == approx( - -max_production.capacity.max("timeslice").values - if "timeslice" in max_production.capacity.dims - else -max_production.capacity.values - ) - assert demand_limiting_capacity.production == 0 - assert demand_limiting_capacity.b.values == approx( - demand_constraint.b.max("timeslice").values - if "timeslice" in demand_constraint.b.dims - else demand_constraint.b.values - ) diff --git a/tests/test_trade.py b/tests/test_trade.py index bafa07db9..6a25adf30 100644 --- a/tests/test_trade.py +++ b/tests/test_trade.py @@ -14,15 +14,12 @@ def constraints_args(sector="power", model="trade") -> Mapping[str, Any]: power = examples.sector(model=model, sector=sector) search_space = examples.search_space("power", model="trade") market = examples.matching_market("power", "trade") - assets = reduce_assets( - agent_concatenation({u.uuid: u.assets for u in list(power.agents)}), - coords=["agent", "technology", "region"], - ).set_coords(["agent", "technology", "region"]) + assets = agent_concatenation({u.uuid: u.assets for u in list(power.agents)}) + capacity = reduce_assets(assets.capacity, coords=("region", "technology")) return dict( demand=market.consumption.sel(year=market.year.min(), drop=True), - assets=assets, + capacity=capacity, search_space=search_space, - market=market, technologies=power.technologies, )