From 5280ff6a717fff81153c823f1667aa1dde636b68 Mon Sep 17 00:00:00 2001 From: Tom Bland Date: Wed, 26 Mar 2025 11:00:37 +0000 Subject: [PATCH 1/6] Mandatory kwargs --- src/muse/constraints.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/muse/constraints.py b/src/muse/constraints.py index 64c5591c..910643ee 100644 --- a/src/muse/constraints.py +++ b/src/muse/constraints.py @@ -251,7 +251,7 @@ def constraints( capacity: xr.DataArray, search_space: xr.DataArray, technologies: xr.Dataset, - timeslice_level: str | None = None, + **kwargs, ) -> list[Constraint]: constraints = [ function( @@ -259,7 +259,7 @@ def constraints( capacity=capacity, search_space=search_space, technologies=technologies, - timeslice_level=timeslice_level, + **kwargs, ) for function in constraint_closures ] @@ -441,6 +441,7 @@ def max_production( capacity: xr.DataArray, search_space: xr.DataArray, technologies: xr.Dataset, + *, timeslice_level: str | None = None, **kwargs, ) -> Constraint: @@ -499,6 +500,7 @@ def demand_limiting_capacity( capacity: xr.DataArray, search_space: xr.DataArray, technologies: xr.Dataset, + *, timeslice_level: str | None = None, **kwargs, ) -> Constraint: @@ -709,6 +711,7 @@ def minimum_service( capacity: xr.DataArray, search_space: xr.DataArray, technologies: xr.Dataset, + *, timeslice_level: str | None = None, **kwargs, ) -> Constraint | None: From fb59252eb23ebedc9034b20fa71fbd560b1e4b0e Mon Sep 17 00:00:00 2001 From: Tom Bland Date: Wed, 26 Mar 2025 11:59:04 +0000 Subject: [PATCH 2/6] Stricter constraints check --- src/muse/constraints.py | 30 ++++++++++++++++++++---------- tests/test_constraints.py | 2 +- 2 files changed, 21 insertions(+), 11 deletions(-) diff --git a/src/muse/constraints.py b/src/muse/constraints.py index 910643ee..d3d82417 100644 --- a/src/muse/constraints.py +++ b/src/muse/constraints.py @@ -187,21 +187,27 @@ def decorated( **kwargs, ) - # Standardize constraint if constraint is not None: - if "kind" not in constraint.attrs: - constraint.attrs["kind"] = ConstraintKind.UPPER_BOUND + # Check constraint + if "b" not in constraint.data_vars: + raise RuntimeError("Constraint must contain a right-hand-side vector") if ( "capacity" not in constraint.data_vars and "production" not in constraint.data_vars ): - raise RuntimeError("Invalid constraint format") + raise RuntimeError("Constraint must contain a left-hand-side matrix") + if "capacity" in constraint.data_vars: + assert not constraint.capacity.dims == () + if "production" in constraint.data_vars: + assert not constraint.production.dims == () + if "kind" not in constraint.attrs: + raise RuntimeError("Constraint must contain a kind attribute") + + # Standardize constraint if "capacity" not in constraint.data_vars: constraint["capacity"] = 0 if "production" not in constraint.data_vars: constraint["production"] = 0 - if "b" not in constraint.data_vars: - constraint["b"] = 0 if "name" not in constraint.data_vars and "name" not in constraint.attrs: constraint.attrs["name"] = function.__name__ @@ -385,7 +391,7 @@ def max_capacity_expansion( ) if b.region.dims == (): - capa = 1 + capa = xr.ones_like(b) elif "dst_region" in b.dims: b = b.rename(region="src_region") capa = search_space.agent.region == b.src_region @@ -413,7 +419,8 @@ def demand( 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) + dict(b=b, production=xr.ones_like(b)), + attrs=dict(kind=ConstraintKind.LOWER_BOUND), ) @@ -476,6 +483,7 @@ def max_production( capa = capa.expand_dims(asset=search_space.asset) production = ones_like(capa) b = zeros_like(production) + # Include maxaddition constraint in max production to match region-dst_region if "dst_region" in technologies.dims: b = b.expand_dims(dst_region=technologies.dst_region) @@ -488,8 +496,9 @@ def max_production( capa = capa * broadcast_timeslice(maxadd, level=timeslice_level) production = production * broadcast_timeslice(maxadd, level=timeslice_level) b = b.rename(region="src_region") + return xr.Dataset( - dict(capacity=-cast(np.ndarray, capa), production=production, b=b), + dict(capacity=-capa, production=production, b=b), attrs=dict(kind=ConstraintKind.UPPER_BOUND), ) @@ -746,8 +755,9 @@ def minimum_service( capacity = capacity.expand_dims(asset=search_space.asset) production = ones_like(capacity) b = zeros_like(production) + return xr.Dataset( - dict(capacity=-cast(np.ndarray, capacity), production=production, b=b), + dict(capacity=-capacity, production=production, b=b), attrs=dict(kind=ConstraintKind.LOWER_BOUND), ) diff --git a/tests/test_constraints.py b/tests/test_constraints.py index 67b4f45b..7070cbae 100644 --- a/tests/test_constraints.py +++ b/tests/test_constraints.py @@ -468,7 +468,7 @@ def test_minimum_service( def test_max_capacity_expansion(max_capacity_expansion): - assert max_capacity_expansion.capacity == 1 + assert (max_capacity_expansion.capacity == 1).all() assert max_capacity_expansion.production == 0 assert max_capacity_expansion.b.dims == ("replacement",) assert max_capacity_expansion.b.shape == (4,) From 88bbeae907f936746a7aa6f827e3d618ddd6d367 Mon Sep 17 00:00:00 2001 From: Tom Bland Date: Wed, 26 Mar 2025 16:47:11 +0000 Subject: [PATCH 3/6] FIx doctest --- src/muse/constraints.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/muse/constraints.py b/src/muse/constraints.py index d3d82417..f55defb8 100644 --- a/src/muse/constraints.py +++ b/src/muse/constraints.py @@ -1065,8 +1065,7 @@ class ScipyAdapter: It is an upper bound for a straightforward sum over the capacities for a given technology. The matrix operator is simply the identity: - >>> assert constraint.capacity.data == np.array(1) - >>> assert len(constraint.capacity.dims) == 0 + >>> assert (constraint.capacity == 1).all() And the upperbound is expanded over the replacement technologies, but not over the assets. Hence the assets will be summed over in the final @@ -1077,9 +1076,8 @@ class ScipyAdapter: As shown above, it does not bind the production decision variables. Hence, production is zero. The matrix operator for the capacity is simply the identity. - Hence it can be inputted as the dimensionless scalar 1. The upper bound is - simply the maximum for replacement technology (and region, if that particular - dimension exists in the problem). + The upper bound is simply the maximum for replacement technology (and region, if + that particular dimension exists in the problem). The lp problem then becomes: From 20609a667f92b8236cd72f60a56982ecfc907e20 Mon Sep 17 00:00:00 2001 From: Tom Bland Date: Wed, 26 Mar 2025 17:05:51 +0000 Subject: [PATCH 4/6] Testing constraint dimensions --- tests/test_constraints.py | 47 +++++++++++++++++++++++++++++++++++---- 1 file changed, 43 insertions(+), 4 deletions(-) diff --git a/tests/test_constraints.py b/tests/test_constraints.py index 7070cbae..98ab1cb3 100644 --- a/tests/test_constraints.py +++ b/tests/test_constraints.py @@ -142,6 +142,49 @@ def constraints(request, market_demand, capacity, search_space, technologies): return constraints +def test_constraints_dimensions( + max_production, demand_constraint, demand_limiting_capacity, max_capacity_expansion +): + # Max production constraint + assert set(max_production.capacity.dims) == { + "asset", + "commodity", + "replacement", + "timeslice", + } + assert set(max_production.production.dims) == { + "asset", + "commodity", + "replacement", + "timeslice", + } + assert set(max_production.b.dims) == { + "asset", + "commodity", + "replacement", + "timeslice", + } + + # Demand constraint + assert set(demand_constraint.capacity.dims) == {} + assert set(demand_constraint.production.dims) == {"asset", "commodity", "timeslice"} + assert set(demand_constraint.b.dims) == {"asset", "commodity", "timeslice"} + + # Demand limiting capacity constraint + assert set(demand_limiting_capacity.capacity.dims) == { + "asset", + "commodity", + "replacement", + } + assert set(demand_limiting_capacity.production.dims) == {} + assert set(demand_limiting_capacity.b.dims) == {"asset", "commodity"} + + # Max capacity expansion constraint + assert set(max_capacity_expansion.capacity.dims) == {"replacement"} + assert set(max_capacity_expansion.production.dims) == {} + assert set(max_capacity_expansion.b.dims) == {"replacement"} + + def test_lp_constraints_matrix_b_is_scalar(constraint, lpcosts): """B is a scalar. @@ -513,10 +556,6 @@ def test_max_capacity_expansion_infinite_limits( 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() From a581b62b81a06798b8ad71ac3774dd996cf92428 Mon Sep 17 00:00:00 2001 From: Tom Bland Date: Wed, 26 Mar 2025 17:10:46 +0000 Subject: [PATCH 5/6] Fix test --- tests/test_constraints.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/test_constraints.py b/tests/test_constraints.py index 98ab1cb3..a1c61629 100644 --- a/tests/test_constraints.py +++ b/tests/test_constraints.py @@ -166,7 +166,7 @@ def test_constraints_dimensions( } # Demand constraint - assert set(demand_constraint.capacity.dims) == {} + assert set(demand_constraint.capacity.dims) == set() assert set(demand_constraint.production.dims) == {"asset", "commodity", "timeslice"} assert set(demand_constraint.b.dims) == {"asset", "commodity", "timeslice"} @@ -176,12 +176,12 @@ def test_constraints_dimensions( "commodity", "replacement", } - assert set(demand_limiting_capacity.production.dims) == {} + assert set(demand_limiting_capacity.production.dims) == set() assert set(demand_limiting_capacity.b.dims) == {"asset", "commodity"} # Max capacity expansion constraint assert set(max_capacity_expansion.capacity.dims) == {"replacement"} - assert set(max_capacity_expansion.production.dims) == {} + assert set(max_capacity_expansion.production.dims) == set() assert set(max_capacity_expansion.b.dims) == {"replacement"} From de9c9641b0beccba796f019cb604f010dae9c142 Mon Sep 17 00:00:00 2001 From: Tom Bland Date: Thu, 27 Mar 2025 13:57:48 +0000 Subject: [PATCH 6/6] Revert some changes --- src/muse/constraints.py | 12 ++++-------- tests/test_constraints.py | 6 +++--- 2 files changed, 7 insertions(+), 11 deletions(-) diff --git a/src/muse/constraints.py b/src/muse/constraints.py index 10396b26..8f4de551 100644 --- a/src/muse/constraints.py +++ b/src/muse/constraints.py @@ -196,10 +196,6 @@ def decorated( and "production" not in constraint.data_vars ): raise RuntimeError("Constraint must contain a left-hand-side matrix") - if "capacity" in constraint.data_vars: - assert not constraint.capacity.dims == () - if "production" in constraint.data_vars: - assert not constraint.production.dims == () if "kind" not in constraint.attrs: raise RuntimeError("Constraint must contain a kind attribute") @@ -409,7 +405,7 @@ def max_capacity_expansion( ) if b.region.dims == (): - capa = xr.ones_like(b) + capa = 1 elif "dst_region" in b.dims: b = b.rename(region="src_region") capa = search_space.agent.region == b.src_region @@ -437,8 +433,7 @@ def demand( b = b.rename(region="dst_region") assert "year" not in b.dims return xr.Dataset( - dict(b=b, production=xr.ones_like(b)), - attrs=dict(kind=ConstraintKind.LOWER_BOUND), + dict(b=b, production=1), attrs=dict(kind=ConstraintKind.LOWER_BOUND) ) @@ -1083,7 +1078,8 @@ class ScipyAdapter: It is an upper bound for a straightforward sum over the capacities for a given technology. The matrix operator is simply the identity: - >>> assert (constraint.capacity == 1).all() + >>> assert constraint.capacity.data == np.array(1) + >>> assert len(constraint.capacity.dims) == 0 And the upperbound is expanded over the replacement technologies, but not over the assets. Hence the assets will be summed over in the final diff --git a/tests/test_constraints.py b/tests/test_constraints.py index 364c92ce..70c2362b 100644 --- a/tests/test_constraints.py +++ b/tests/test_constraints.py @@ -167,7 +167,7 @@ def test_constraints_dimensions( # Demand constraint assert set(demand_constraint.capacity.dims) == set() - assert set(demand_constraint.production.dims) == {"asset", "commodity", "timeslice"} + assert set(demand_constraint.production.dims) == set() assert set(demand_constraint.b.dims) == {"asset", "commodity", "timeslice"} # Demand limiting capacity constraint @@ -180,7 +180,7 @@ def test_constraints_dimensions( assert set(demand_limiting_capacity.b.dims) == {"asset", "commodity"} # Max capacity expansion constraint - assert set(max_capacity_expansion.capacity.dims) == {"replacement"} + assert set(max_capacity_expansion.capacity.dims) == set() assert set(max_capacity_expansion.production.dims) == set() assert set(max_capacity_expansion.b.dims) == {"replacement"} @@ -511,7 +511,7 @@ def test_minimum_service( def test_max_capacity_expansion(max_capacity_expansion): - assert (max_capacity_expansion.capacity == 1).all() + 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,)