From aa736ebb8faa37e8d7952facc4e216e7119429b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrik=20Sch=C3=B6nfeldt?= Date: Fri, 2 Aug 2024 15:24:58 +0200 Subject: [PATCH 1/5] Allow lower limit for generic_integral_limit I did not touch the multi-period-model as I think the constraint for this (experimental) mode needs refactoring anyway. --- src/oemof/solph/constraints/integral_limit.py | 52 +++++++++++++++---- tests/lp_files/emission_budget_limit.lp | 2 +- tests/lp_files/emission_limit.lp | 2 +- tests/lp_files/emission_limit_no_error.lp | 2 +- 4 files changed, 44 insertions(+), 14 deletions(-) diff --git a/src/oemof/solph/constraints/integral_limit.py b/src/oemof/solph/constraints/integral_limit.py index 3d2bc3ffa..b2605c4c1 100644 --- a/src/oemof/solph/constraints/integral_limit.py +++ b/src/oemof/solph/constraints/integral_limit.py @@ -12,6 +12,7 @@ SPDX-License-Identifier: MIT """ +import warnings from pyomo import environ as po @@ -29,7 +30,7 @@ def emission_limit(om, flows=None, limit=None): """ generic_integral_limit( - om, keyword="emission_factor", flows=flows, limit=limit + om, keyword="emission_factor", flows=flows, upper_limit=limit ) @@ -49,7 +50,9 @@ def emission_limit_per_period(om, flows=None, limit=None): ) -def generic_integral_limit(om, keyword, flows=None, limit=None): +def generic_integral_limit( + om, keyword, flows=None, upper_limit=None, lower_limit=None, limit=None +): r"""Set a global limit for flows weighted by attribute named keyword. The attribute named keyword has to be added to every flow you want to take into account. @@ -69,8 +72,10 @@ def generic_integral_limit(om, keyword, flows=None, limit=None): used. keyword : string attribute to consider - limit : numeric - Absolute limit of keyword attribute for the energy system. + upper_limit : numeric + Absolute upper limit of keyword attribute for the energy system. + lower_limit : numeric + Absolute lower limit of keyword attribute for the energy system. Note ---- @@ -80,7 +85,10 @@ def generic_integral_limit(om, keyword, flows=None, limit=None): **Constraint:** .. math:: \sum_{i \in F_E} \sum_{t \in T} P_i(p, t) \cdot w_i(t) - \cdot \tau(t) \leq M + \cdot \tau(t) \leq UB + + .. math:: \sum_{i \in F_E} \sum_{t \in T} P_i(p, t) \cdot w_i(t) + \cdot \tau(t) \geq LB With `F_I` being the set of flows considered for the integral limit and @@ -95,7 +103,8 @@ def generic_integral_limit(om, keyword, flows=None, limit=None): :math:`P_n(p, t)` V power flow :math:`n` at time index :math:`p, t` :math:`w_N(t)` P weight given to Flow named according to `keyword` :math:`\tau(t)` P width of time step :math:`t` - :math:`L` P global limit given by keyword `limit` + :math:`UB` P global limit given by keyword `upper_limit` + :math:`LB` P global limit given by keyword `lower_limit` ================= ==== ==================================================== Examples @@ -124,6 +133,20 @@ def generic_integral_limit(om, keyword, flows=None, limit=None): flows = _check_and_set_flows(om, flows, keyword) limit_name = "integral_limit_" + keyword + if limit is not None: + msg = ( + "The keyword argument 'limit' to generic_integral_limit has been" + "renamed to 'upper_limit'. The transitional wrapper will be" + "deleted in a future version." + ) + warnings.warn(msg, FutureWarning) + upper_limit = limit + + if upper_limit is None and lower_limit is None: + raise ValueError( + "At least one of upper_limit an lower_limit needs to be defined." + ) + setattr( om, limit_name, @@ -138,11 +161,18 @@ def generic_integral_limit(om, keyword, flows=None, limit=None): ), ) - setattr( - om, - limit_name + "_constraint", - po.Constraint(expr=(getattr(om, limit_name) <= limit)), - ) + if upper_limit is not None: + setattr( + om, + limit_name + "_upper_limit", + po.Constraint(expr=(getattr(om, limit_name) <= upper_limit)), + ) + if lower_limit is not None: + setattr( + om, + limit_name + "_lower_limit", + po.Constraint(expr=(getattr(om, limit_name) >= lower_limit)), + ) return om diff --git a/tests/lp_files/emission_budget_limit.lp b/tests/lp_files/emission_budget_limit.lp index 825ded62d..495b4bd48 100644 --- a/tests/lp_files/emission_budget_limit.lp +++ b/tests/lp_files/emission_budget_limit.lp @@ -6,7 +6,7 @@ objective: s.t. -c_u_integral_limit_emission_factor_constraint_: +c_u_integral_limit_emission_factor_upper_limit_: +0.5 flow(source1_electricityBus_0) -1.0 flow(source1_electricityBus_1) +2.0 flow(source1_electricityBus_2) diff --git a/tests/lp_files/emission_limit.lp b/tests/lp_files/emission_limit.lp index 373339e8a..380823e3c 100644 --- a/tests/lp_files/emission_limit.lp +++ b/tests/lp_files/emission_limit.lp @@ -6,7 +6,7 @@ objective: s.t. -c_u_integral_limit_emission_factor_constraint_: +c_u_integral_limit_emission_factor_upper_limit_: +0.5 flow(source1_electricityBus_0) -1.0 flow(source1_electricityBus_1) +2.0 flow(source1_electricityBus_2) diff --git a/tests/lp_files/emission_limit_no_error.lp b/tests/lp_files/emission_limit_no_error.lp index bbf05d19d..4a2fe33bc 100644 --- a/tests/lp_files/emission_limit_no_error.lp +++ b/tests/lp_files/emission_limit_no_error.lp @@ -6,7 +6,7 @@ objective: s.t. -c_u_integral_limit_emission_factor_constraint_: +c_u_integral_limit_emission_factor_upper_limit_: +0.8 flow(source1_electricityBus_0) +0.8 flow(source1_electricityBus_1) +0.8 flow(source1_electricityBus_2) From 1705996a77f53c03379e58ad816366e098b88901 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrik=20Sch=C3=B6nfeldt?= Date: Fri, 2 Aug 2024 15:53:34 +0200 Subject: [PATCH 2/5] Add unittests for lower integral limit --- tests/constraint_tests.py | 23 ++++++++++++++++ tests/lp_files/emission_limit_lower.lp | 38 ++++++++++++++++++++++++++ tests/test_constraints_module.py | 11 +++++--- 3 files changed, 68 insertions(+), 4 deletions(-) create mode 100644 tests/lp_files/emission_limit_lower.lp diff --git a/tests/constraint_tests.py b/tests/constraint_tests.py index 07b0c3f0f..93bd5784e 100644 --- a/tests/constraint_tests.py +++ b/tests/constraint_tests.py @@ -904,6 +904,29 @@ def test_flow_without_emission_for_emission_constraint_no_error(self): self.compare_lp_files("emission_limit_no_error.lp", my_om=om) + def test_flow_without_emission_for_emission_constraint_lower(self): + """Test that no error is thrown if no flows are explicitly passed""" + bel = solph.buses.Bus(label="electricityBus") + source1 = solph.components.Source( + label="source1", + outputs={ + bel: solph.flows.Flow( + nominal_value=100, + custom_attributes={"emission_factor": 0.8}, + ) + }, + ) + source2 = solph.components.Source( + label="source2", outputs={bel: solph.flows.Flow(nominal_value=100)} + ) + self.energysystem.add(bel, source1, source2) + om = self.get_om() + solph.constraints.generic_integral_limit( + om, keyword="emission_factor", lower_limit=777 + ) + + self.compare_lp_files("emission_limit_lower.lp", my_om=om) + def test_equate_variables_constraint(self): """Testing the equate_variables function in the constraint module.""" bus1 = solph.buses.Bus(label="Bus1") diff --git a/tests/lp_files/emission_limit_lower.lp b/tests/lp_files/emission_limit_lower.lp new file mode 100644 index 000000000..92fa30731 --- /dev/null +++ b/tests/lp_files/emission_limit_lower.lp @@ -0,0 +1,38 @@ +\* Source Pyomo model name=Model *\ + +min +objective: ++0 ONE_VAR_CONSTANT + +s.t. + +c_l_integral_limit_emission_factor_lower_limit_: ++0.8 flow(source1_electricityBus_0) ++0.8 flow(source1_electricityBus_1) ++0.8 flow(source1_electricityBus_2) +>= 777 + +c_e_BusBlock_balance(electricityBus_0)_: ++1 flow(source1_electricityBus_0) ++1 flow(source2_electricityBus_0) += 0 + +c_e_BusBlock_balance(electricityBus_1)_: ++1 flow(source1_electricityBus_1) ++1 flow(source2_electricityBus_1) += 0 + +c_e_BusBlock_balance(electricityBus_2)_: ++1 flow(source1_electricityBus_2) ++1 flow(source2_electricityBus_2) += 0 + +bounds + 1 <= ONE_VAR_CONSTANT <= 1 + 0 <= flow(source1_electricityBus_0) <= 100 + 0 <= flow(source1_electricityBus_1) <= 100 + 0 <= flow(source1_electricityBus_2) <= 100 + 0 <= flow(source2_electricityBus_0) <= 100 + 0 <= flow(source2_electricityBus_1) <= 100 + 0 <= flow(source2_electricityBus_2) <= 100 +end diff --git a/tests/test_constraints_module.py b/tests/test_constraints_module.py index fbf4fc15b..c2614001b 100644 --- a/tests/test_constraints_module.py +++ b/tests/test_constraints_module.py @@ -1,9 +1,11 @@ +import pytest + import pandas as pd from oemof import solph -def test_special(): +def test_integral_limit_wrapper(): date_time_index = pd.date_range("1/1/2012", periods=5, freq="h") energysystem = solph.EnergySystem( timeindex=date_time_index, @@ -22,9 +24,10 @@ def test_special(): flow_with_keyword = { (src1, bel): flow1, } - solph.constraints.generic_integral_limit( - model, "my_factor", flow_with_keyword, limit=777 - ) + with pytest.warns(FutureWarning): + solph.constraints.generic_integral_limit( + model, "my_factor", flow_with_keyword, limit=777 + ) def test_something_else(): From 4ebd2ef43ad7357556e313bc098278b1c0857eee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrik=20Sch=C3=B6nfeldt?= Date: Fri, 2 Aug 2024 15:56:51 +0200 Subject: [PATCH 3/5] Fix import order --- tests/test_constraints_module.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/test_constraints_module.py b/tests/test_constraints_module.py index c2614001b..775717faf 100644 --- a/tests/test_constraints_module.py +++ b/tests/test_constraints_module.py @@ -1,6 +1,5 @@ -import pytest - import pandas as pd +import pytest from oemof import solph From df8bafab3e48f8d28dc3c84ffc55532492277cb2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrik=20Sch=C3=B6nfeldt?= Date: Fri, 2 Aug 2024 16:04:16 +0200 Subject: [PATCH 4/5] Add test for wrongly set limit --- src/oemof/solph/constraints/integral_limit.py | 2 +- tests/test_constraints_module.py | 14 ++++++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/src/oemof/solph/constraints/integral_limit.py b/src/oemof/solph/constraints/integral_limit.py index b2605c4c1..16258d37e 100644 --- a/src/oemof/solph/constraints/integral_limit.py +++ b/src/oemof/solph/constraints/integral_limit.py @@ -144,7 +144,7 @@ def generic_integral_limit( if upper_limit is None and lower_limit is None: raise ValueError( - "At least one of upper_limit an lower_limit needs to be defined." + "At least one of upper_limit and lower_limit needs to be defined." ) setattr( diff --git a/tests/test_constraints_module.py b/tests/test_constraints_module.py index 775717faf..afaa2ba68 100644 --- a/tests/test_constraints_module.py +++ b/tests/test_constraints_module.py @@ -29,6 +29,20 @@ def test_integral_limit_wrapper(): ) +def test_limetless_limit(): + date_time_index = pd.date_range("1/1/2012", periods=5, freq="h") + energysystem = solph.EnergySystem( + timeindex=date_time_index, + infer_last_interval=True, + ) + model = solph.Model(energysystem) + with pytest.raises( + ValueError, + match="At least one of upper_limit and lower_limit needs to be defined.", + ): + solph.constraints.generic_integral_limit(model, "my_factor") + + def test_something_else(): date_time_index = pd.date_range("1/1/2012", periods=5, freq="h") energysystem = solph.EnergySystem( From 3753d5d9aef291255c326e56653fbbe8e3d1d3a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrik=20Sch=C3=B6nfeldt?= Date: Fri, 2 Aug 2024 16:07:59 +0200 Subject: [PATCH 5/5] Shorten line length --- tests/test_constraints_module.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_constraints_module.py b/tests/test_constraints_module.py index afaa2ba68..82b696474 100644 --- a/tests/test_constraints_module.py +++ b/tests/test_constraints_module.py @@ -38,7 +38,7 @@ def test_limetless_limit(): model = solph.Model(energysystem) with pytest.raises( ValueError, - match="At least one of upper_limit and lower_limit needs to be defined.", + match="At least one of upper_limit and lower_limit", ): solph.constraints.generic_integral_limit(model, "my_factor")