From d574666b733925f366f47a8afa37a3cfb5691807 Mon Sep 17 00:00:00 2001 From: eva38032 Date: Fri, 22 Nov 2024 12:47:00 +0100 Subject: [PATCH 01/46] Add kwarg fail_on_infeasable in model.solve --- src/oemof/solph/_models.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/oemof/solph/_models.py b/src/oemof/solph/_models.py index 5ecbbd474..2c9f1cf0a 100644 --- a/src/oemof/solph/_models.py +++ b/src/oemof/solph/_models.py @@ -389,7 +389,7 @@ def results(self): """ return processing.results(self) - def solve(self, solver="cbc", solver_io="lp", **kwargs): + def solve(self, solver="cbc", solver_io="lp", fail_on_infeasable=True, **kwargs): r"""Takes care of communication with solver to solve the model. Parameters @@ -436,9 +436,14 @@ def solve(self, solver="cbc", solver_io="lp", **kwargs): "Optimization ended with status {0} and termination " "condition {1}" ) - warnings.warn( - msg.format(status, termination_condition), UserWarning - ) + + if fail_on_infeasable: + raise RuntimeError(msg) + else: + warnings.warn( + msg.format(status, termination_condition), UserWarning + ) + self.es.results = solver_results self.solver_results = solver_results From e329a14a07b343bf99b5f63677113559e97b4f9b Mon Sep 17 00:00:00 2001 From: eva38032 Date: Fri, 22 Nov 2024 12:56:43 +0100 Subject: [PATCH 02/46] Change keyword name to allow_nonoptimal Reason: other nonoptimal statuses besides infeasable are possible, e.g. unbounded --- src/oemof/solph/_models.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/oemof/solph/_models.py b/src/oemof/solph/_models.py index 2c9f1cf0a..33bfb2c1f 100644 --- a/src/oemof/solph/_models.py +++ b/src/oemof/solph/_models.py @@ -389,7 +389,7 @@ def results(self): """ return processing.results(self) - def solve(self, solver="cbc", solver_io="lp", fail_on_infeasable=True, **kwargs): + def solve(self, solver="cbc", solver_io="lp", allow_nonoptimal=False, **kwargs): r"""Takes care of communication with solver to solve the model. Parameters @@ -437,12 +437,12 @@ def solve(self, solver="cbc", solver_io="lp", fail_on_infeasable=True, **kwargs) "condition {1}" ) - if fail_on_infeasable: - raise RuntimeError(msg) - else: + if allow_nonoptimal: warnings.warn( msg.format(status, termination_condition), UserWarning ) + else: + raise RuntimeError(msg) self.es.results = solver_results self.solver_results = solver_results From 1439e9ededb24282ffdc2fe3155e11ae73e1c67f Mon Sep 17 00:00:00 2001 From: eva38032 Date: Fri, 22 Nov 2024 12:58:58 +0100 Subject: [PATCH 03/46] Save solver results before checking status So that results are saved, even if error is raised --- src/oemof/solph/_models.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/oemof/solph/_models.py b/src/oemof/solph/_models.py index 33bfb2c1f..1382836eb 100644 --- a/src/oemof/solph/_models.py +++ b/src/oemof/solph/_models.py @@ -429,6 +429,9 @@ def solve(self, solver="cbc", solver_io="lp", allow_nonoptimal=False, **kwargs): "Termination condition" ] + self.es.results = solver_results + self.solver_results = solver_results + if status == "ok" and termination_condition == "optimal": logging.info("Optimization successful...") else: @@ -442,11 +445,8 @@ def solve(self, solver="cbc", solver_io="lp", allow_nonoptimal=False, **kwargs): msg.format(status, termination_condition), UserWarning ) else: - raise RuntimeError(msg) - - self.es.results = solver_results - self.solver_results = solver_results - + raise RuntimeError(msg) + return solver_results def relax_problem(self): From bdc784517ea94bca444c81effe48398ea4273330 Mon Sep 17 00:00:00 2001 From: eva38032 Date: Fri, 22 Nov 2024 13:05:13 +0100 Subject: [PATCH 04/46] Add info to whatsnew --- docs/whatsnew/v0-6-0.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/whatsnew/v0-6-0.rst b/docs/whatsnew/v0-6-0.rst index c5700ae76..86f20db2f 100644 --- a/docs/whatsnew/v0-6-0.rst +++ b/docs/whatsnew/v0-6-0.rst @@ -25,6 +25,8 @@ Bug fixes Other changes ############# +* `Model.solve()` will now fail, if solver status is nonoptimal. + Added new keyword `allow_nonoptimal` to deactivate this behaviour. Known issues ############ @@ -34,3 +36,4 @@ Contributors ############ * Patrik Schönfeldt +* Eva Schischke From 907bdef58e024bdbeb0966f3e32787362491b3d9 Mon Sep 17 00:00:00 2001 From: eva38032 Date: Fri, 22 Nov 2024 13:07:42 +0100 Subject: [PATCH 05/46] add name to AUTHORS.rst and CITATION.cff --- AUTHORS.rst | 1 + CITATION.cff | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/AUTHORS.rst b/AUTHORS.rst index 85443fee1..045076d72 100644 --- a/AUTHORS.rst +++ b/AUTHORS.rst @@ -39,3 +39,4 @@ Authors * Stephan Günther * Uwe Krien * Tobi Rohrer +* Eva Schischke diff --git a/CITATION.cff b/CITATION.cff index 19f8f340a..cd89949ab 100644 --- a/CITATION.cff +++ b/CITATION.cff @@ -170,4 +170,8 @@ authors: family-names: Gering given-names: Marie-Claire alias: "@MaGering" + - + family-names: Schischke + given-names: Eva + alias: "@esske" ... From 4d4283fa74c7b227d6d26d7438accaa663e9f1bf Mon Sep 17 00:00:00 2001 From: Eva Schischke Date: Fri, 20 Dec 2024 10:42:56 +0100 Subject: [PATCH 06/46] add tests --- src/oemof/solph/_models.py | 22 ++++++++-------- tests/test_models.py | 52 +++++++++++++++++++++++++++++++++++--- 2 files changed, 60 insertions(+), 14 deletions(-) diff --git a/src/oemof/solph/_models.py b/src/oemof/solph/_models.py index 1382836eb..94ebb989a 100644 --- a/src/oemof/solph/_models.py +++ b/src/oemof/solph/_models.py @@ -389,7 +389,9 @@ def results(self): """ return processing.results(self) - def solve(self, solver="cbc", solver_io="lp", allow_nonoptimal=False, **kwargs): + def solve( + self, solver="cbc", solver_io="lp", allow_nonoptimal=False, **kwargs + ): r"""Takes care of communication with solver to solve the model. Parameters @@ -415,8 +417,8 @@ def solve(self, solver="cbc", solver_io="lp", allow_nonoptimal=False, **kwargs): """ solve_kwargs = kwargs.get("solve_kwargs", {}) solver_cmdline_options = kwargs.get("cmdline_options", {}) - opt = SolverFactory(solver, solver_io=solver_io) + # set command line options options = opt.options for k in solver_cmdline_options: @@ -424,10 +426,8 @@ def solve(self, solver="cbc", solver_io="lp", allow_nonoptimal=False, **kwargs): solver_results = opt.solve(self, **solve_kwargs) - status = solver_results["Solver"][0]["Status"] - termination_condition = solver_results["Solver"][0][ - "Termination condition" - ] + status = solver_results.Solver.Status + termination_condition = solver_results.Solver.Termination_condition self.es.results = solver_results self.solver_results = solver_results @@ -436,8 +436,10 @@ def solve(self, solver="cbc", solver_io="lp", allow_nonoptimal=False, **kwargs): logging.info("Optimization successful...") else: msg = ( - "Optimization ended with status {0} and termination " - "condition {1}" + f"The solver did not return an optimal solution. " + f"Instead the optimization ended with\n " + f" - status: {status}\n" + f" - termination condition: {termination_condition}" ) if allow_nonoptimal: @@ -445,8 +447,8 @@ def solve(self, solver="cbc", solver_io="lp", allow_nonoptimal=False, **kwargs): msg.format(status, termination_condition), UserWarning ) else: - raise RuntimeError(msg) - + raise RuntimeError(msg) + return solver_results def relax_problem(self): diff --git a/tests/test_models.py b/tests/test_models.py index e5c51f9bd..aa8f6b36d 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -17,7 +17,7 @@ from oemof import solph -def test_infeasible_model(): +def test_infeasible_model_warning(): es = solph.EnergySystem(timeincrement=[1]) bel = solph.buses.Bus(label="bus") es.add(bel) @@ -32,9 +32,53 @@ def test_infeasible_model(): ) ) m = solph.Model(es) - with warnings.catch_warnings(record=True) as w: - m.solve(solver="cbc") - assert "Optimization ended with status" in str(w[0].message) + with warnings.catch_warnings(record=False) as w: + m.solve(solver="cbc", allow_nonoptimal=True) + assert "The solver did not return an optimal solution" in str( + w[0].message + ) + + +def test_infeasible_model_error(): + es = solph.EnergySystem(timeincrement=[1]) + bel = solph.buses.Bus(label="bus") + es.add(bel) + es.add( + solph.components.Sink( + inputs={bel: solph.flows.Flow(nominal_value=5, fix=[1])} + ) + ) + es.add( + solph.components.Source( + outputs={bel: solph.flows.Flow(nominal_value=4, variable_costs=5)} + ) + ) + m = solph.Model(es) + try: + m.solve(solver="cbc", allow_nonoptimal=False) + except Exception as e: + assert "The solver did not return an optimal solution" in str(e) + + +def test_unbounded_model(): + es = solph.EnergySystem(timeincrement=[1]) + bel = solph.buses.Bus(label="bus") + es.add(bel) + # Add a Sink with a higher demand + es.add(solph.components.Sink(inputs={bel: solph.flows.Flow()})) + + # Add a Source with a very high supply + es.add( + solph.components.Source( + outputs={bel: solph.flows.Flow(variable_costs=-5)} + ) + ) + m = solph.Model(es) + + try: + m.solve(solver="cbc", allow_nonoptimal=False) + except Exception as e: + assert "The solver did not return an optimal solution" in str(e) @pytest.mark.filterwarnings( From 165b0eacc72a0e586cac3008f1680497835c2dde Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrik=20Sch=C3=B6nfeldt?= Date: Tue, 7 Jan 2025 15:50:26 +0100 Subject: [PATCH 07/46] Ad stub for the EV charging tutorial. --- examples/ev_charging/ev_charging.py | 104 ++++++++++++++++++++++++++++ 1 file changed, 104 insertions(+) create mode 100644 examples/ev_charging/ev_charging.py diff --git a/examples/ev_charging/ev_charging.py b/examples/ev_charging/ev_charging.py new file mode 100644 index 000000000..1e47e54af --- /dev/null +++ b/examples/ev_charging/ev_charging.py @@ -0,0 +1,104 @@ +# %% +""" +First of all, we create some input data. We use Pandas to do so and will also +import matplotlib to plot the data right away. +""" + +import pandas as pd +import matplotlib.pyplot as plt + +# %% + +time_index = pd.date_range( + start="2025-01-01", + end="2025-01-02", + freq="5min", + inclusive="both", +) + +# Define the trip demand series for the real trip scenario. +# As the demand is a power time series, it has N-1 entries +# when compared to the N entires of time axis for the energy. +ev_demand = pd.Series(0, index=time_index[:-1]) + +# Morning Driving: 07:00 to 08:00 +driving_start_morning = pd.Timestamp("2025-01-01 07:10") +driving_end_morning = pd.Timestamp("2025-01-01 08:10") +ev_demand.loc[driving_start_morning:driving_end_morning] = 10 # kW + +# Evening Driving: 17:00 to 18:00. +# Note that time points work even if they are not in the index. +driving_start_evening = pd.Timestamp("2025-01-01 17:13:37") +driving_end_evening = pd.Timestamp("2025-01-01 18:47:11") +ev_demand.loc[driving_start_evening:driving_end_evening] = 9 # kW + +plt.figure(figsize=(5, 2)) +plt.plot(ev_demand) +plt.ylabel("Power (kW)") +plt.gcf().autofmt_xdate() +plt.show() + +# %% +""" +Now, let's create an energy system model of the electric vehicle that +follows the driving pattern. It uses the same time index defined above +and consists of a Battery (Charged in the beginning) and an electricity demand. +""" +import oemof.solph as solph + +def create_base_system(): + energy_system = solph.EnergySystem( + timeindex=time_index, + infer_last_interval=False, + ) + + b_el = solph.Bus(label="Car Electricity") + energy_system.add(b_el) + + # As we have a demand time series which is actually in kW, + # we use a common "hack" here: We set the nominal capacity to 1 (kW), + # so that multiplication by the time series will just yield the correct result. + demand_driving = solph.components.Sink( + label="Driving Demand", + inputs={b_el: solph.Flow(nominal_capacity=1, fix=ev_demand)}, + ) + + energy_system.add(demand_driving) + + car_battery = solph.components.GenericStorage( + label="Car Battery", + nominal_capacity=50, # kWh + inputs={b_el: solph.Flow()}, + outputs={b_el: solph.Flow()}, + initial_storage_level=1, # full in the beginning + loss_rate=0.001, # 0.1 % / hr + balanced=False, # True: content at beginning and end need to be equal + ) + energy_system.add(car_battery) + + return energy_system + + +es = create_base_system() + +# %% +""" +Solve the model and show results +""" +model = solph.Model(es) +model.solve(solve_kwargs={"tee": False}) +results = solph.processing.results(model) + +battery_series = solph.views.node(results, "Car Battery")["sequences"] + +plt.plot(battery_series[(("Car Battery", "None"), "storage_content")]) +plt.ylabel("Energy (kWh)") +plt.ylim(0, 51) +plt.twinx() +energy_leaves_battery = battery_series[ + (("Car Battery", "Car Electricity"), "flow") +] +plt.step(energy_leaves_battery.index, energy_leaves_battery, "r-") +plt.ylabel("Power (kW)") +plt.gcf().autofmt_xdate() +plt.show() From a9e0b3cc29b9710c4996eb4f5de05522ac8640ea Mon Sep 17 00:00:00 2001 From: antonella Date: Sat, 11 Jan 2025 16:39:25 +0100 Subject: [PATCH 08/46] add unidirectional loading --- examples/ev_charging/ev_charging.py | 80 +++++++++++++++++++++++++++++ 1 file changed, 80 insertions(+) diff --git a/examples/ev_charging/ev_charging.py b/examples/ev_charging/ev_charging.py index 1e47e54af..dd2166499 100644 --- a/examples/ev_charging/ev_charging.py +++ b/examples/ev_charging/ev_charging.py @@ -102,3 +102,83 @@ def create_base_system(): plt.ylabel("Power (kW)") plt.gcf().autofmt_xdate() plt.show() + +# %% +""" +Now, let's assume the car battery is half full at the beginning and you want to +leave for your first trip with an almost fully charged battery (not regarding the +loss rate ). The trip demand will not be regarded. +""" + +def create_unidirectional_loading(): + energy_system = solph.EnergySystem( + timeindex=time_index, + infer_last_interval=False, + ) + + b_el = solph.Bus(label="Car Electricity") + energy_system.add(b_el) + + # To be able to load the battery a electric source e.g. electric grid is necessary + el_grid = solph.components.Source( + label="Electric Grid", + outputs={b_el: solph.Flow()} + + ) + + energy_system.add(el_grid) + + + # The car is half full and has to be full when the car leaves the first time + # In this case before 7:10, e.g. timestep 86 + + timestep_loading_finished = len(ev_demand[:ev_demand.gt(0).idxmax()]) + + # We need a timeseries which represents the timesteps where loading is allowed (=1) + # In this case the first 86 timesteps + + loading_allowed=pd.Series(0, index=time_index[:-1]) + loading_allowed[:timestep_loading_finished]=1 + + # Assuming the maximal loading power is 10 kw (nominal_capacity = 10) and only within + # the first 87 timesteps is allowed to load the battery (max= loading_allowed) + # To define the battery has to be loaded 25 kWh. + # Only unidirectional loading is allowed, so the output nominal_capacity is set to 0 + car_battery = solph.components.GenericStorage( + label="Car Battery", + nominal_capacity=50, # kWh + inputs={b_el: solph.Flow(nominal_capacity=10, max=loading_allowed,full_load_time_min=2.5)}, + outputs={b_el: solph.Flow(nominal_capacity=0)}, + initial_storage_level=0.5, # halffull in the beginning + loss_rate=0.001, # 0.1 % / hr + balanced=False, # True: content at beginning and end need to be equal + ) + energy_system.add(car_battery) + + return energy_system + +es = create_unidirectional_loading() + +# %% +""" +Solve the model and show results +""" +model = solph.Model(es) +model.solve(solve_kwargs={"tee": False}) +results = solph.processing.results(model) + +battery_series = solph.views.node(results, "Car Battery")["sequences"] + +plt.plot(battery_series[(("Car Battery", "None"), "storage_content")]) +plt.ylabel("Energy (kWh)") +plt.ylim(0, 51) +plt.twinx() +energy_leaves_battery = battery_series[ + (( "Car Electricity","Car Battery"), "flow") +] +plt.step(energy_leaves_battery.index, energy_leaves_battery, "r-") +plt.ylabel("Power (kW)") +plt.gcf().autofmt_xdate() +plt.show() + +# %% From 22e83e1bba30a13e1b1365447136e483e66e3277 Mon Sep 17 00:00:00 2001 From: antonella Date: Sat, 11 Jan 2025 16:43:02 +0100 Subject: [PATCH 09/46] unidirectional loading --- examples/ev_charging/ev_charging.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/ev_charging/ev_charging.py b/examples/ev_charging/ev_charging.py index dd2166499..baad3dfc3 100644 --- a/examples/ev_charging/ev_charging.py +++ b/examples/ev_charging/ev_charging.py @@ -105,9 +105,9 @@ def create_base_system(): # %% """ -Now, let's assume the car battery is half full at the beginning and you want to +Now, let's assume the car battery is half fully loaded at the beginning and you want to leave for your first trip with an almost fully charged battery (not regarding the -loss rate ). The trip demand will not be regarded. +loss rate). The trip demand will not be regarded. """ def create_unidirectional_loading(): From 45dacd21a5babd5eb6ff84d712417ac673df315e Mon Sep 17 00:00:00 2001 From: antonella Date: Sun, 12 Jan 2025 09:21:11 +0100 Subject: [PATCH 10/46] add example unidirectional loading at home --- examples/ev_charging/ev_charging.py | 103 ++++++++++++++++++++++++++-- 1 file changed, 98 insertions(+), 5 deletions(-) diff --git a/examples/ev_charging/ev_charging.py b/examples/ev_charging/ev_charging.py index baad3dfc3..b9b436679 100644 --- a/examples/ev_charging/ev_charging.py +++ b/examples/ev_charging/ev_charging.py @@ -1,11 +1,13 @@ # %% """ First of all, we create some input data. We use Pandas to do so and will also -import matplotlib to plot the data right away. +import matplotlib to plot the data right away and import solph """ +from copy import deepcopy import pandas as pd import matplotlib.pyplot as plt +import oemof.solph as solph # %% @@ -44,7 +46,7 @@ follows the driving pattern. It uses the same time index defined above and consists of a Battery (Charged in the beginning) and an electricity demand. """ -import oemof.solph as solph + def create_base_system(): energy_system = solph.EnergySystem( @@ -105,12 +107,12 @@ def create_base_system(): # %% """ -Now, let's assume the car battery is half fully loaded at the beginning and you want to +Now, let's assume the car battery is half loaded at the beginning and you want to leave for your first trip with an almost fully charged battery (not regarding the loss rate). The trip demand will not be regarded. """ -def create_unidirectional_loading(): +def create_unidirectional_loading_until_defined_timestep(): energy_system = solph.EnergySystem( timeindex=time_index, infer_last_interval=False, @@ -157,7 +159,7 @@ def create_unidirectional_loading(): return energy_system -es = create_unidirectional_loading() +es = create_unidirectional_loading_until_defined_timestep() # %% """ @@ -181,4 +183,95 @@ def create_unidirectional_loading(): plt.gcf().autofmt_xdate() plt.show() + + +# %% +""" +Assuming the car can be loaded at home and the car is always available to be loaded (at home), when not driven. +The car is half loaded in the beginning and should be loaded when car is at home. +""" + +def create_unidirectional_loading(): + + + + # Again setting up the energy system + energy_system = solph.EnergySystem( + timeindex=time_index, + infer_last_interval=False, + ) + + b_el = solph.Bus(label="Car Electricity") + energy_system.add(b_el) + + # To be able to load the battery a electric source e.g. electric grid is necessary + el_grid = solph.components.Source( + label="Electric Grid", + outputs={b_el: solph.Flow()} + + ) + + energy_system.add(el_grid) + + + # The car is half full and has to be full when the car leaves the first time + # In this case before 7:10, e.g. timestep 86 + + timestep_loading_finished = len(ev_demand[:ev_demand.gt(0).idxmax()]) + + # We need a timeseries which represents the timesteps where loading is allowed (=1) + # In this case the first 86 timesteps + + loading_allowed=pd.Series(0, index=time_index[:-1]) + loading_allowed[:timestep_loading_finished]=1 + + # The maximal charging_capacity is assumed to be 10 kW + charging_cap = 10 + + # The car can only be loaded if at home + loading_allowed = [charging_cap if demand==0 else 0 for demand in ev_demand ] + + # The is now regared as loss of the car battery + # To make sure the car battery will be loaded, gain is added to the battery + + gain = -1 + + car_battery = solph.components.GenericStorage( + label="Car Battery", + nominal_capacity=50, # kWh + inputs={b_el: solph.Flow(nominal_capacity=1, max=loading_allowed, variable_costs=gain)}, + outputs={b_el: solph.Flow(nominal_capacity=0)}, + initial_storage_level=0.5, # halffull in the beginning + fixed_losses_absolute= ev_demand, + loss_rate=0.001, # 0.1 % / hr + balanced=False, # True: content at beginning and end need to be equal + ) + energy_system.add(car_battery) + + return energy_system + +es = create_unidirectional_loading() + + +# %% +""" +Solve the model and show results +""" +model = solph.Model(es) +model.solve(solve_kwargs={"tee": False}) +results = solph.processing.results(model) + +battery_series = solph.views.node(results, "Car Battery")["sequences"] + +plt.plot(battery_series[(("Car Battery", "None"), "storage_content")]) +plt.ylabel("Energy (kWh)") +plt.ylim(0, 51) +plt.twinx() +energy_leaves_battery = battery_series[ + (( "Car Electricity","Car Battery"), "flow") +] +plt.step(energy_leaves_battery.index, energy_leaves_battery, "r-") +plt.ylabel("Power (kW)") +plt.gcf().autofmt_xdate() +plt.show() # %% From f6f7e91934686fe451a16244c5da4b7436f7d350 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrik=20Sch=C3=B6nfeldt?= Date: Tue, 14 Jan 2025 16:18:08 +0100 Subject: [PATCH 11/46] Adhere to Black --- examples/ev_charging/ev_charging.py | 55 ++++++++++++++++------------- 1 file changed, 31 insertions(+), 24 deletions(-) diff --git a/examples/ev_charging/ev_charging.py b/examples/ev_charging/ev_charging.py index b9b436679..0c4de8b85 100644 --- a/examples/ev_charging/ev_charging.py +++ b/examples/ev_charging/ev_charging.py @@ -112,6 +112,7 @@ def create_base_system(): loss rate). The trip demand will not be regarded. """ + def create_unidirectional_loading_until_defined_timestep(): energy_system = solph.EnergySystem( timeindex=time_index, @@ -123,24 +124,21 @@ def create_unidirectional_loading_until_defined_timestep(): # To be able to load the battery a electric source e.g. electric grid is necessary el_grid = solph.components.Source( - label="Electric Grid", - outputs={b_el: solph.Flow()} - + label="Electric Grid", outputs={b_el: solph.Flow()} ) energy_system.add(el_grid) - # The car is half full and has to be full when the car leaves the first time # In this case before 7:10, e.g. timestep 86 - timestep_loading_finished = len(ev_demand[:ev_demand.gt(0).idxmax()]) + timestep_loading_finished = len(ev_demand[: ev_demand.gt(0).idxmax()]) # We need a timeseries which represents the timesteps where loading is allowed (=1) # In this case the first 86 timesteps - loading_allowed=pd.Series(0, index=time_index[:-1]) - loading_allowed[:timestep_loading_finished]=1 + loading_allowed = pd.Series(0, index=time_index[:-1]) + loading_allowed[:timestep_loading_finished] = 1 # Assuming the maximal loading power is 10 kw (nominal_capacity = 10) and only within # the first 87 timesteps is allowed to load the battery (max= loading_allowed) @@ -149,7 +147,13 @@ def create_unidirectional_loading_until_defined_timestep(): car_battery = solph.components.GenericStorage( label="Car Battery", nominal_capacity=50, # kWh - inputs={b_el: solph.Flow(nominal_capacity=10, max=loading_allowed,full_load_time_min=2.5)}, + inputs={ + b_el: solph.Flow( + nominal_capacity=10, + max=loading_allowed, + full_load_time_min=2.5, + ) + }, outputs={b_el: solph.Flow(nominal_capacity=0)}, initial_storage_level=0.5, # halffull in the beginning loss_rate=0.001, # 0.1 % / hr @@ -159,6 +163,7 @@ def create_unidirectional_loading_until_defined_timestep(): return energy_system + es = create_unidirectional_loading_until_defined_timestep() # %% @@ -176,7 +181,7 @@ def create_unidirectional_loading_until_defined_timestep(): plt.ylim(0, 51) plt.twinx() energy_leaves_battery = battery_series[ - (( "Car Electricity","Car Battery"), "flow") + (("Car Electricity", "Car Battery"), "flow") ] plt.step(energy_leaves_battery.index, energy_leaves_battery, "r-") plt.ylabel("Power (kW)") @@ -184,16 +189,14 @@ def create_unidirectional_loading_until_defined_timestep(): plt.show() - # %% """ Assuming the car can be loaded at home and the car is always available to be loaded (at home), when not driven. The car is half loaded in the beginning and should be loaded when car is at home. """ -def create_unidirectional_loading(): - +def create_unidirectional_loading(): # Again setting up the energy system energy_system = solph.EnergySystem( @@ -206,30 +209,29 @@ def create_unidirectional_loading(): # To be able to load the battery a electric source e.g. electric grid is necessary el_grid = solph.components.Source( - label="Electric Grid", - outputs={b_el: solph.Flow()} - + label="Electric Grid", outputs={b_el: solph.Flow()} ) energy_system.add(el_grid) - # The car is half full and has to be full when the car leaves the first time # In this case before 7:10, e.g. timestep 86 - timestep_loading_finished = len(ev_demand[:ev_demand.gt(0).idxmax()]) + timestep_loading_finished = len(ev_demand[: ev_demand.gt(0).idxmax()]) # We need a timeseries which represents the timesteps where loading is allowed (=1) # In this case the first 86 timesteps - loading_allowed=pd.Series(0, index=time_index[:-1]) - loading_allowed[:timestep_loading_finished]=1 + loading_allowed = pd.Series(0, index=time_index[:-1]) + loading_allowed[:timestep_loading_finished] = 1 - # The maximal charging_capacity is assumed to be 10 kW + # The maximal charging_capacity is assumed to be 10 kW charging_cap = 10 # The car can only be loaded if at home - loading_allowed = [charging_cap if demand==0 else 0 for demand in ev_demand ] + loading_allowed = [ + charging_cap if demand == 0 else 0 for demand in ev_demand + ] # The is now regared as loss of the car battery # To make sure the car battery will be loaded, gain is added to the battery @@ -239,10 +241,14 @@ def create_unidirectional_loading(): car_battery = solph.components.GenericStorage( label="Car Battery", nominal_capacity=50, # kWh - inputs={b_el: solph.Flow(nominal_capacity=1, max=loading_allowed, variable_costs=gain)}, + inputs={ + b_el: solph.Flow( + nominal_capacity=1, max=loading_allowed, variable_costs=gain + ) + }, outputs={b_el: solph.Flow(nominal_capacity=0)}, initial_storage_level=0.5, # halffull in the beginning - fixed_losses_absolute= ev_demand, + fixed_losses_absolute=ev_demand, loss_rate=0.001, # 0.1 % / hr balanced=False, # True: content at beginning and end need to be equal ) @@ -250,6 +256,7 @@ def create_unidirectional_loading(): return energy_system + es = create_unidirectional_loading() @@ -268,7 +275,7 @@ def create_unidirectional_loading(): plt.ylim(0, 51) plt.twinx() energy_leaves_battery = battery_series[ - (( "Car Electricity","Car Battery"), "flow") + (("Car Electricity", "Car Battery"), "flow") ] plt.step(energy_leaves_battery.index, energy_leaves_battery, "r-") plt.ylabel("Power (kW)") From 17c1854a93a1b69da1bef6a2464bd3321cedb5b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrik=20Sch=C3=B6nfeldt?= Date: Tue, 14 Jan 2025 16:23:03 +0100 Subject: [PATCH 12/46] Move ev tutorial to tutorial directory --- {examples/ev_charging => tutorial/introductory}/ev_charging.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename {examples/ev_charging => tutorial/introductory}/ev_charging.py (100%) diff --git a/examples/ev_charging/ev_charging.py b/tutorial/introductory/ev_charging.py similarity index 100% rename from examples/ev_charging/ev_charging.py rename to tutorial/introductory/ev_charging.py From db6b401cdd81b729a1706ea300fa92757864b74a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrik=20Sch=C3=B6nfeldt?= Date: Tue, 14 Jan 2025 17:07:12 +0100 Subject: [PATCH 13/46] Include tutorials in pyproject.toml --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index a8bf4323d..230213b6d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -17,6 +17,7 @@ include = [ "examples/", "src/", "tests/", + "tutorials/", ".bumpversion.cfg", ".coveragerc", ".editorconfig", From f7e5105330d1daa6a6b81cd9dc5276390c6e8dbe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrik=20Sch=C3=B6nfeldt?= Date: Tue, 14 Jan 2025 17:07:40 +0100 Subject: [PATCH 14/46] Clearify docstring of GenericStorage --- src/oemof/solph/components/_generic_storage.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/oemof/solph/components/_generic_storage.py b/src/oemof/solph/components/_generic_storage.py index 49b969f87..a8e645571 100644 --- a/src/oemof/solph/components/_generic_storage.py +++ b/src/oemof/solph/components/_generic_storage.py @@ -106,7 +106,8 @@ class GenericStorage(Node): see: min_storage_level storage_costs : numeric (iterable or scalar), :math:`c_{storage}(t)` Cost (per energy) for having energy in the storage, starting from - time point :math:`t_{1}`. + time point :math:`t_{1}`. (:math:`t_{0}` is left out to avoid counting + it twice if balanced=True.) lifetime_inflow : int, :math:`n_{in}` Determine the lifetime of an inflow; only applicable for multi-period models which can invest in storage capacity and have an From ed3560456436e20dcb9b1acb362b3f138625da5b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrik=20Sch=C3=B6nfeldt?= Date: Tue, 14 Jan 2025 21:10:04 +0100 Subject: [PATCH 15/46] Start restructuring EV tutorial --- tutorial/introductory/ev_charging.py | 72 ++++++++++++++++------------ 1 file changed, 41 insertions(+), 31 deletions(-) diff --git a/tutorial/introductory/ev_charging.py b/tutorial/introductory/ev_charging.py index 0c4de8b85..ec8cfde26 100644 --- a/tutorial/introductory/ev_charging.py +++ b/tutorial/introductory/ev_charging.py @@ -1,15 +1,15 @@ -# %% +# %%[imports] """ First of all, we create some input data. We use Pandas to do so and will also import matplotlib to plot the data right away and import solph """ -from copy import deepcopy +import numpy as np import pandas as pd import matplotlib.pyplot as plt import oemof.solph as solph -# %% +# %%[trip_data] time_index = pd.date_range( start="2025-01-01", @@ -34,19 +34,20 @@ driving_end_evening = pd.Timestamp("2025-01-01 18:47:11") ev_demand.loc[driving_start_evening:driving_end_evening] = 9 # kW -plt.figure(figsize=(5, 2)) +plt.figure() plt.plot(ev_demand) plt.ylabel("Power (kW)") plt.gcf().autofmt_xdate() -plt.show() -# %% +# %%[base_system] """ Now, let's create an energy system model of the electric vehicle that follows the driving pattern. It uses the same time index defined above -and consists of a Battery (Charged in the beginning) and an electricity demand. +and consists of a Battery (partly charged in the beginning) +and an electricity demand. """ +b_el = solph.Bus(label="Car Electricity") def create_base_system(): energy_system = solph.EnergySystem( @@ -54,7 +55,6 @@ def create_base_system(): infer_last_interval=False, ) - b_el = solph.Bus(label="Car Electricity") energy_system.add(b_el) # As we have a demand time series which is actually in kW, @@ -67,14 +67,21 @@ def create_base_system(): energy_system.add(demand_driving) + # We define a "storage revenue" (negative costs) for the last time step, + # so that energy inside the storage in the last time step is worth + # something. + storage_revenue = np.zeros(len(time_index) - 1) + storage_revenue[-1] = -0.6 # 60ct/kWh in the last time step + car_battery = solph.components.GenericStorage( label="Car Battery", nominal_capacity=50, # kWh inputs={b_el: solph.Flow()}, outputs={b_el: solph.Flow()}, - initial_storage_level=1, # full in the beginning + initial_storage_level=0.75, # 75 % full in the beginning loss_rate=0.001, # 0.1 % / hr balanced=False, # True: content at beginning and end need to be equal + storage_costs=storage_revenue, # Only has an effect on charging. ) energy_system.add(car_battery) @@ -83,36 +90,39 @@ def create_base_system(): es = create_base_system() -# %% +# %%[solve_and_plot] """ Solve the model and show results """ -model = solph.Model(es) -model.solve(solve_kwargs={"tee": False}) -results = solph.processing.results(model) -battery_series = solph.views.node(results, "Car Battery")["sequences"] +def solve_and_plot(): + model = solph.Model(es) + model.solve(solve_kwargs={"tee": False}) + results = solph.processing.results(model) -plt.plot(battery_series[(("Car Battery", "None"), "storage_content")]) -plt.ylabel("Energy (kWh)") -plt.ylim(0, 51) -plt.twinx() -energy_leaves_battery = battery_series[ - (("Car Battery", "Car Electricity"), "flow") -] -plt.step(energy_leaves_battery.index, energy_leaves_battery, "r-") -plt.ylabel("Power (kW)") -plt.gcf().autofmt_xdate() -plt.show() + battery_series = solph.views.node(results, "Car Battery")["sequences"] -# %% -""" -Now, let's assume the car battery is half loaded at the beginning and you want to -leave for your first trip with an almost fully charged battery (not regarding the -loss rate). The trip demand will not be regarded. -""" + plt.figure() + plt.plot(battery_series[(("Car Battery", "None"), "storage_content")]) + plt.ylabel("Energy (kWh)") + plt.ylim(0, 51) + plt.twinx() + energy_leaves_battery = battery_series[ + (("Car Battery", "Car Electricity"), "flow") + ] + plt.step(energy_leaves_battery.index, energy_leaves_battery, "r-") + plt.ylabel("Power (kW)") + plt.gcf().autofmt_xdate() + plt.show() +solve_and_plot() + +# %%[charging] +""" +Now, let's assume the car battery can be charged (11 kW). +This, of course, can only happen while the car is present. +""" def create_unidirectional_loading_until_defined_timestep(): energy_system = solph.EnergySystem( timeindex=time_index, From 69a8b7a29a835d197629bd3bfa3d5457db58ff3c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrik=20Sch=C3=B6nfeldt?= Date: Wed, 15 Jan 2025 11:01:00 +0100 Subject: [PATCH 16/46] Fix typo in pyproject.toml --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 230213b6d..8fde4dd92 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -17,7 +17,7 @@ include = [ "examples/", "src/", "tests/", - "tutorials/", + "tutorial/", ".bumpversion.cfg", ".coveragerc", ".editorconfig", From c49e3a02066f98644c259db4f0009ea6acd792b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrik=20Sch=C3=B6nfeldt?= Date: Wed, 15 Jan 2025 11:57:50 +0100 Subject: [PATCH 17/46] Proceed with ev charging example --- tutorials/introductory/ev_charging.py | 76 +++++++++------------------ 1 file changed, 26 insertions(+), 50 deletions(-) diff --git a/tutorials/introductory/ev_charging.py b/tutorials/introductory/ev_charging.py index ec8cfde26..a553f2077 100644 --- a/tutorials/introductory/ev_charging.py +++ b/tutorials/introductory/ev_charging.py @@ -47,7 +47,6 @@ and an electricity demand. """ -b_el = solph.Bus(label="Car Electricity") def create_base_system(): energy_system = solph.EnergySystem( @@ -55,6 +54,8 @@ def create_base_system(): infer_last_interval=False, ) + b_el = solph.Bus(label="Car Electricity") + energy_system.add(b_el) # As we have a demand time series which is actually in kW, @@ -88,13 +89,12 @@ def create_base_system(): return energy_system -es = create_base_system() - # %%[solve_and_plot] """ Solve the model and show results """ + def solve_and_plot(): model = solph.Model(es) model.solve(solve_kwargs={"tee": False}) @@ -105,76 +105,52 @@ def solve_and_plot(): plt.figure() plt.plot(battery_series[(("Car Battery", "None"), "storage_content")]) plt.ylabel("Energy (kWh)") - plt.ylim(0, 51) + plt.ylim(0, 60) plt.twinx() + plt.ylim(0, 12) energy_leaves_battery = battery_series[ (("Car Battery", "Car Electricity"), "flow") ] plt.step(energy_leaves_battery.index, energy_leaves_battery, "r-") + plt.grid() plt.ylabel("Power (kW)") plt.gcf().autofmt_xdate() - plt.show() + +es = create_base_system() solve_and_plot() +plt.show() # %%[charging] """ -Now, let's assume the car battery can be charged (11 kW). +Now, let's assume the car battery can be charged (230 V, 16 A). This, of course, can only happen while the car is present. """ -def create_unidirectional_loading_until_defined_timestep(): - energy_system = solph.EnergySystem( - timeindex=time_index, - infer_last_interval=False, - ) - - b_el = solph.Bus(label="Car Electricity") - energy_system.add(b_el) - - # To be able to load the battery a electric source e.g. electric grid is necessary - el_grid = solph.components.Source( - label="Electric Grid", outputs={b_el: solph.Flow()} - ) - - energy_system.add(el_grid) - - # The car is half full and has to be full when the car leaves the first time - # In this case before 7:10, e.g. timestep 86 - timestep_loading_finished = len(ev_demand[: ev_demand.gt(0).idxmax()]) - # We need a timeseries which represents the timesteps where loading is allowed (=1) - # In this case the first 86 timesteps +def add_unidirectional_loading(): + car_present = pd.Series(1, index=time_index[:-1]) + car_present.loc[driving_start_morning:driving_end_evening] = 0 # kW - loading_allowed = pd.Series(0, index=time_index[:-1]) - loading_allowed[:timestep_loading_finished] = 1 + b_el = es.node["Car Electricity"] - # Assuming the maximal loading power is 10 kw (nominal_capacity = 10) and only within - # the first 87 timesteps is allowed to load the battery (max= loading_allowed) - # To define the battery has to be loaded 25 kWh. - # Only unidirectional loading is allowed, so the output nominal_capacity is set to 0 - car_battery = solph.components.GenericStorage( - label="Car Battery", - nominal_capacity=50, # kWh - inputs={ - b_el: solph.Flow( - nominal_capacity=10, - max=loading_allowed, - full_load_time_min=2.5, - ) - }, - outputs={b_el: solph.Flow(nominal_capacity=0)}, - initial_storage_level=0.5, # halffull in the beginning - loss_rate=0.001, # 0.1 % / hr - balanced=False, # True: content at beginning and end need to be equal + # To be able to load the battery a electric source e.g. electric grid is necessary. + # We set the maximum use to 1 (so 3.68 kW are usable) if the car is present, + # while it is 0 between the morning start and the evening arrival back home. + charger230V = solph.components.Source( + label="230V charger", + outputs={b_el: solph.Flow(nominal_capacity=3.68, max=car_present)}, ) - energy_system.add(car_battery) - return energy_system + es.add(charger230V) -es = create_unidirectional_loading_until_defined_timestep() +es = create_base_system() +add_unidirectional_loading() +solve_and_plot() +plt.show() +exit() # %% """ From 64bbd17e67e7028262d95342f8b83b6c7398523b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrik=20Sch=C3=B6nfeldt?= Date: Wed, 15 Jan 2025 14:42:44 +0100 Subject: [PATCH 18/46] Continue EV charging tutorial --- tutorials/introductory/ev_charging.py | 239 ++++++++++++++------------ 1 file changed, 128 insertions(+), 111 deletions(-) diff --git a/tutorials/introductory/ev_charging.py b/tutorials/introductory/ev_charging.py index a553f2077..63a1b6607 100644 --- a/tutorials/introductory/ev_charging.py +++ b/tutorials/introductory/ev_charging.py @@ -35,6 +35,7 @@ ev_demand.loc[driving_start_evening:driving_end_evening] = 9 # kW plt.figure() +plt.title("Driving pattern") plt.plot(ev_demand) plt.ylabel("Power (kW)") plt.gcf().autofmt_xdate() @@ -54,16 +55,16 @@ def create_base_system(): infer_last_interval=False, ) - b_el = solph.Bus(label="Car Electricity") + b_car = solph.Bus(label="Car Electricity") - energy_system.add(b_el) + energy_system.add(b_car) - # As we have a demand time series which is actually in kW, - # we use a common "hack" here: We set the nominal capacity to 1 (kW), - # so that multiplication by the time series will just yield the correct result. + # As we have a demand time series which is actually in kW, we use a common + # "hack" here: We set the nominal capacity to 1 (kW), so that + # multiplication by the time series will just yield the correct result. demand_driving = solph.components.Sink( label="Driving Demand", - inputs={b_el: solph.Flow(nominal_capacity=1, fix=ev_demand)}, + inputs={b_car: solph.Flow(nominal_capacity=1, fix=ev_demand)}, ) energy_system.add(demand_driving) @@ -72,15 +73,16 @@ def create_base_system(): # so that energy inside the storage in the last time step is worth # something. storage_revenue = np.zeros(len(time_index) - 1) - storage_revenue[-1] = -0.6 # 60ct/kWh in the last time step + storage_revenue[-1] = -0.6 # 60 ct/kWh in the last time step car_battery = solph.components.GenericStorage( label="Car Battery", nominal_capacity=50, # kWh - inputs={b_el: solph.Flow()}, - outputs={b_el: solph.Flow()}, + inputs={b_car: solph.Flow()}, + outputs={b_car: solph.Flow()}, initial_storage_level=0.75, # 75 % full in the beginning loss_rate=0.001, # 0.1 % / hr + inflow_conversion_factor=0.9, # 90 % charging efficiency balanced=False, # True: content at beginning and end need to be equal storage_costs=storage_revenue, # Only has an effect on charging. ) @@ -95,7 +97,7 @@ def create_base_system(): """ -def solve_and_plot(): +def solve_and_plot(plot_title): model = solph.Model(es) model.solve(solve_kwargs={"tee": False}) results = solph.processing.results(model) @@ -103,168 +105,183 @@ def solve_and_plot(): battery_series = solph.views.node(results, "Car Battery")["sequences"] plt.figure() + plt.title(plot_title) plt.plot(battery_series[(("Car Battery", "None"), "storage_content")]) plt.ylabel("Energy (kWh)") plt.ylim(0, 60) plt.twinx() plt.ylim(0, 12) + energy_enters_battery = battery_series[ + (("Car Electricity", "Car Battery"), "flow") + ] energy_leaves_battery = battery_series[ (("Car Battery", "Car Electricity"), "flow") ] - plt.step(energy_leaves_battery.index, energy_leaves_battery, "r-") + plt.step(energy_leaves_battery.index, energy_leaves_battery, "r--") + plt.step(energy_enters_battery.index, energy_enters_battery, "r-") plt.grid() plt.ylabel("Power (kW)") plt.gcf().autofmt_xdate() es = create_base_system() -solve_and_plot() -plt.show() +solve_and_plot("Driving demand only") -# %%[charging] +# %%[AC_30ct_charging] """ -Now, let's assume the car battery can be charged (230 V, 16 A). -This, of course, can only happen while the car is present. +Now, let's assume the car battery can be charged at home. Unfortunately, there +is only a power so cket available, limiting the charging process to 16 A at +230 V. This, of course, can only happen while the car is present. """ -def add_unidirectional_loading(): - car_present = pd.Series(1, index=time_index[:-1]) - car_present.loc[driving_start_morning:driving_end_evening] = 0 # kW +def add_domestic_socket_charging(): + car_at_home = pd.Series(1, index=time_index[:-1]) + car_at_home.loc[driving_start_morning:driving_end_evening] = 0 - b_el = es.node["Car Electricity"] + b_car = es.node["Car Electricity"] - # To be able to load the battery a electric source e.g. electric grid is necessary. - # We set the maximum use to 1 (so 3.68 kW are usable) if the car is present, - # while it is 0 between the morning start and the evening arrival back home. + # To be able to load the battery a electric source e.g. electric grid is + # necessary. We set the maximum use to 1 if the car is present, while it + # is 0 between the morning start and the evening arrival back home. + # While the car itself can potentially charge with at a higher power, + # we just add an AC source with 16 A at 230 V. charger230V = solph.components.Source( - label="230V charger", - outputs={b_el: solph.Flow(nominal_capacity=3.68, max=car_present)}, + label="230V AC", + outputs={ + b_car: solph.Flow( + nominal_capacity=3.68, # 230 V * 16 A = 3.68 kW + variable_costs=0.3, # 30 ct/kWh + max=car_at_home, + ) + }, ) es.add(charger230V) es = create_base_system() -add_unidirectional_loading() -solve_and_plot() -plt.show() -exit() +add_domestic_socket_charging() +solve_and_plot("Domestic power socket charging") -# %% +# %%[DC_charging] """ -Solve the model and show results +Now, we add an 11 kW charger (no costs) which is available at work. +This, of course, can only happen while the car is present at work. """ -model = solph.Model(es) -model.solve(solve_kwargs={"tee": False}) -results = solph.processing.results(model) - -battery_series = solph.views.node(results, "Car Battery")["sequences"] - -plt.plot(battery_series[(("Car Battery", "None"), "storage_content")]) -plt.ylabel("Energy (kWh)") -plt.ylim(0, 51) -plt.twinx() -energy_leaves_battery = battery_series[ - (("Car Electricity", "Car Battery"), "flow") -] -plt.step(energy_leaves_battery.index, energy_leaves_battery, "r-") -plt.ylabel("Power (kW)") -plt.gcf().autofmt_xdate() -plt.show() -# %% -""" -Assuming the car can be loaded at home and the car is always available to be loaded (at home), when not driven. -The car is half loaded in the beginning and should be loaded when car is at home. -""" +def add_11kW_charging(): + car_at_work = pd.Series(0, index=time_index[:-1]) + car_at_work.loc[driving_end_morning:driving_start_evening] = 1 + b_car = es.node["Car Electricity"] -def create_unidirectional_loading(): - - # Again setting up the energy system - energy_system = solph.EnergySystem( - timeindex=time_index, - infer_last_interval=False, + # variable_costs in the Flow default to 0, so it's free + charger11kW = solph.components.Source( + label="11kW", + outputs={ + b_car: solph.Flow( + nominal_capacity=11, # 11 kW + max=car_at_work, + ) + }, ) - b_el = solph.Bus(label="Car Electricity") - energy_system.add(b_el) + es.add(charger11kW) - # To be able to load the battery a electric source e.g. electric grid is necessary - el_grid = solph.components.Source( - label="Electric Grid", outputs={b_el: solph.Flow()} - ) - energy_system.add(el_grid) +es = create_base_system() +add_domestic_socket_charging() +add_11kW_charging() +solve_and_plot("Home and work charging") - # The car is half full and has to be full when the car leaves the first time - # In this case before 7:10, e.g. timestep 86 - timestep_loading_finished = len(ev_demand[: ev_demand.gt(0).idxmax()]) +# %%[DC_charging_fixed] +""" +To avoid the energy from looping in the battery, we introduce marginal costs +to battery charging. This is a way to model cyclic aging of the battery. +""" - # We need a timeseries which represents the timesteps where loading is allowed (=1) - # In this case the first 86 timesteps - loading_allowed = pd.Series(0, index=time_index[:-1]) - loading_allowed[:timestep_loading_finished] = 1 +def create_base_system(): + energy_system = solph.EnergySystem( + timeindex=time_index, + infer_last_interval=False, + ) - # The maximal charging_capacity is assumed to be 10 kW - charging_cap = 10 + b_car = solph.Bus(label="Car Electricity") - # The car can only be loaded if at home - loading_allowed = [ - charging_cap if demand == 0 else 0 for demand in ev_demand - ] + energy_system.add(b_car) - # The is now regared as loss of the car battery - # To make sure the car battery will be loaded, gain is added to the battery + demand_driving = solph.components.Sink( + label="Driving Demand", + inputs={b_car: solph.Flow(nominal_capacity=1, fix=ev_demand)}, + ) - gain = -1 + energy_system.add(demand_driving) + + storage_revenue = np.zeros(len(time_index) - 1) + storage_revenue[-1] = -0.6 car_battery = solph.components.GenericStorage( label="Car Battery", - nominal_capacity=50, # kWh + nominal_capacity=50, inputs={ - b_el: solph.Flow( - nominal_capacity=1, max=loading_allowed, variable_costs=gain + b_car: solph.Flow( + variable_costs=1e-4, # models cyclic aging ) }, - outputs={b_el: solph.Flow(nominal_capacity=0)}, - initial_storage_level=0.5, # halffull in the beginning - fixed_losses_absolute=ev_demand, - loss_rate=0.001, # 0.1 % / hr - balanced=False, # True: content at beginning and end need to be equal + outputs={b_car: solph.Flow()}, + initial_storage_level=0.75, + loss_rate=0.001, + inflow_conversion_factor=0.9, + balanced=False, + storage_costs=storage_revenue, ) energy_system.add(car_battery) return energy_system -es = create_unidirectional_loading() - +es = create_base_system() +add_domestic_socket_charging() +add_11kW_charging() +solve_and_plot("Home and work charging (fixed)") -# %% +# %%[AC_var_charging] """ -Solve the model and show results +Now, we replace the home socket charging by a version with variable +electricity prices. """ -model = solph.Model(es) -model.solve(solve_kwargs={"tee": False}) -results = solph.processing.results(model) - -battery_series = solph.views.node(results, "Car Battery")["sequences"] - -plt.plot(battery_series[(("Car Battery", "None"), "storage_content")]) -plt.ylabel("Energy (kWh)") -plt.ylim(0, 51) -plt.twinx() -energy_leaves_battery = battery_series[ - (("Car Electricity", "Car Battery"), "flow") -] -plt.step(energy_leaves_battery.index, energy_leaves_battery, "r-") -plt.ylabel("Power (kW)") -plt.gcf().autofmt_xdate() + + +def add_domestic_socket_charging_var(): + car_at_home = pd.Series(1, index=time_index[:-1]) + car_at_home.loc[driving_start_morning:driving_end_evening] = 0 + + b_car = es.node["Car Electricity"] + + # Same as above, but electricity is cheaper every other step. + # Thus, battery is only charged these steps. + charger230V = solph.components.Source( + label="230V AC", + outputs={ + b_car: solph.Flow( + nominal_capacity=3.68, # 230 V * 16 A = 3.68 kW + variable_costs=[0.2, 0.3] * (len(time_index) // 2), + max=car_at_home, + ) + }, + ) + + es.add(charger230V) + + +es = create_base_system() +add_domestic_socket_charging_var() +add_11kW_charging() +solve_and_plot("Variable price charging") + plt.show() -# %% From ead6dd72853ea1c8ca1f6f081e27e716f4c22d65 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrik=20Sch=C3=B6nfeldt?= Date: Wed, 15 Jan 2025 17:23:15 +0100 Subject: [PATCH 19/46] Introduced balanced battery in EV tutorial --- tutorials/introductory/ev_charging.py | 114 +++++++++++--------------- 1 file changed, 47 insertions(+), 67 deletions(-) diff --git a/tutorials/introductory/ev_charging.py b/tutorials/introductory/ev_charging.py index 63a1b6607..1dc74d0da 100644 --- a/tutorials/introductory/ev_charging.py +++ b/tutorials/introductory/ev_charging.py @@ -30,8 +30,8 @@ # Evening Driving: 17:00 to 18:00. # Note that time points work even if they are not in the index. -driving_start_evening = pd.Timestamp("2025-01-01 17:13:37") -driving_end_evening = pd.Timestamp("2025-01-01 18:47:11") +driving_start_evening = pd.Timestamp("2025-01-01 16:13:37") +driving_end_evening = pd.Timestamp("2025-01-01 17:45:11") ev_demand.loc[driving_start_evening:driving_end_evening] = 9 # kW plt.figure() @@ -69,6 +69,10 @@ def create_base_system(): energy_system.add(demand_driving) + return energy_system, b_car + + +def add_charged_battery(energy_system, b_car): # We define a "storage revenue" (negative costs) for the last time step, # so that energy inside the storage in the last time step is worth # something. @@ -80,7 +84,7 @@ def create_base_system(): nominal_capacity=50, # kWh inputs={b_car: solph.Flow()}, outputs={b_car: solph.Flow()}, - initial_storage_level=0.75, # 75 % full in the beginning + initial_storage_level=1, # full in the beginning loss_rate=0.001, # 0.1 % / hr inflow_conversion_factor=0.9, # 90 % charging efficiency balanced=False, # True: content at beginning and end need to be equal @@ -88,8 +92,6 @@ def create_base_system(): ) energy_system.add(car_battery) - return energy_system - # %%[solve_and_plot] """ @@ -124,7 +126,8 @@ def solve_and_plot(plot_title): plt.gcf().autofmt_xdate() -es = create_base_system() +es, b_car = create_base_system() +add_charged_battery(es, b_car) solve_and_plot("Driving demand only") @@ -136,12 +139,10 @@ def solve_and_plot(plot_title): """ -def add_domestic_socket_charging(): +def add_domestic_socket_charging(energy_system, b_car): car_at_home = pd.Series(1, index=time_index[:-1]) car_at_home.loc[driving_start_morning:driving_end_evening] = 0 - b_car = es.node["Car Electricity"] - # To be able to load the battery a electric source e.g. electric grid is # necessary. We set the maximum use to 1 if the car is present, while it # is 0 between the morning start and the evening arrival back home. @@ -158,26 +159,25 @@ def add_domestic_socket_charging(): }, ) - es.add(charger230V) + energy_system.add(charger230V) -es = create_base_system() -add_domestic_socket_charging() +es, b_car = create_base_system() +add_charged_battery(es, b_car) +add_domestic_socket_charging(es, b_car) solve_and_plot("Domestic power socket charging") # %%[DC_charging] """ -Now, we add an 11 kW charger (no costs) which is available at work. +Now, we add an 11 kW charger (free of charge) which is available at work. This, of course, can only happen while the car is present at work. """ -def add_11kW_charging(): +def add_11kW_charging(energy_system, b_car): car_at_work = pd.Series(0, index=time_index[:-1]) car_at_work.loc[driving_end_morning:driving_start_evening] = 1 - b_car = es.node["Car Electricity"] - # variable_costs in the Flow default to 0, so it's free charger11kW = solph.components.Source( label="11kW", @@ -192,96 +192,76 @@ def add_11kW_charging(): es.add(charger11kW) -es = create_base_system() -add_domestic_socket_charging() -add_11kW_charging() +es, b_car = create_base_system() +add_charged_battery(es, b_car) +add_domestic_socket_charging(es, b_car) +add_11kW_charging(es, b_car) solve_and_plot("Home and work charging") # %%[DC_charging_fixed] """ +Charging and discharging at the same time is almost always a sign that +something is not moddeled accurately in the energy system. To avoid the energy from looping in the battery, we introduce marginal costs to battery charging. This is a way to model cyclic aging of the battery. +As we can recharge now, we also set the "balanced" argument to the default +value and drop the (optional) incentive to recharge. """ -def create_base_system(): - energy_system = solph.EnergySystem( - timeindex=time_index, - infer_last_interval=False, - ) - - b_car = solph.Bus(label="Car Electricity") - - energy_system.add(b_car) - - demand_driving = solph.components.Sink( - label="Driving Demand", - inputs={b_car: solph.Flow(nominal_capacity=1, fix=ev_demand)}, - ) - - energy_system.add(demand_driving) - - storage_revenue = np.zeros(len(time_index) - 1) - storage_revenue[-1] = -0.6 - +def add_balanced_battery(energy_system, b_car): car_battery = solph.components.GenericStorage( label="Car Battery", nominal_capacity=50, - inputs={ - b_car: solph.Flow( - variable_costs=1e-4, # models cyclic aging - ) - }, + inputs={b_car: solph.Flow(variable_costs=0.1)}, outputs={b_car: solph.Flow()}, - initial_storage_level=0.75, loss_rate=0.001, inflow_conversion_factor=0.9, - balanced=False, - storage_costs=storage_revenue, + balanced=True, # this is the default: SOC(T=0) = SOC(T=T_max) + min_storage_level=0.1, # 10 % as reserve ) energy_system.add(car_battery) - return energy_system - -es = create_base_system() -add_domestic_socket_charging() -add_11kW_charging() +es, b_car = create_base_system() +add_balanced_battery(es, b_car) +add_domestic_socket_charging(es, b_car) +add_11kW_charging(es, b_car) solve_and_plot("Home and work charging (fixed)") -# %%[AC_var_charging] +# %%[AC_discharging] """ -Now, we replace the home socket charging by a version with variable -electricity prices. +Now, we add an option to use the car battery bidirectionally. +The car can be charged at work and used at home to save 30 ct/kWh. """ -def add_domestic_socket_charging_var(): +def add_domestic_socket_discharging(energy_system, b_car): car_at_home = pd.Series(1, index=time_index[:-1]) car_at_home.loc[driving_start_morning:driving_end_evening] = 0 - b_car = es.node["Car Electricity"] - # Same as above, but electricity is cheaper every other step. # Thus, battery is only charged these steps. - charger230V = solph.components.Source( - label="230V AC", - outputs={ + discharger230V = solph.components.Sink( + label="230V AC discharge", + inputs={ b_car: solph.Flow( nominal_capacity=3.68, # 230 V * 16 A = 3.68 kW - variable_costs=[0.2, 0.3] * (len(time_index) // 2), + variable_costs=-0.3, max=car_at_home, ) }, ) - es.add(charger230V) + energy_system.add(discharger230V) -es = create_base_system() -add_domestic_socket_charging_var() -add_11kW_charging() -solve_and_plot("Variable price charging") +es, b_car = create_base_system() +add_balanced_battery(es, b_car) +add_domestic_socket_charging(es, b_car) +add_domestic_socket_discharging(es, b_car) +add_11kW_charging(es, b_car) +solve_and_plot("Bidirectional use") plt.show() From 3613ec50f11f4966bb5b8596e76e8be740b0cd64 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrik=20Sch=C3=B6nfeldt?= Date: Thu, 16 Jan 2025 09:44:04 +0100 Subject: [PATCH 20/46] Fix inconsistency in evcharging tutorial --- tutorials/introductory/ev_charging.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tutorials/introductory/ev_charging.py b/tutorials/introductory/ev_charging.py index 1dc74d0da..8611fabe3 100644 --- a/tutorials/introductory/ev_charging.py +++ b/tutorials/introductory/ev_charging.py @@ -189,7 +189,7 @@ def add_11kW_charging(energy_system, b_car): }, ) - es.add(charger11kW) + energy_system.add(charger11kW) es, b_car = create_base_system() From 18c58e27297d530430b009da5f509c3c99cedede Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrik=20Sch=C3=B6nfeldt?= Date: Thu, 16 Jan 2025 09:45:40 +0100 Subject: [PATCH 21/46] Fix typo --- tutorials/introductory/ev_charging.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tutorials/introductory/ev_charging.py b/tutorials/introductory/ev_charging.py index 8611fabe3..ae0c3ac3e 100644 --- a/tutorials/introductory/ev_charging.py +++ b/tutorials/introductory/ev_charging.py @@ -134,7 +134,7 @@ def solve_and_plot(plot_title): # %%[AC_30ct_charging] """ Now, let's assume the car battery can be charged at home. Unfortunately, there -is only a power so cket available, limiting the charging process to 16 A at +is only a power socket available, limiting the charging process to 16 A at 230 V. This, of course, can only happen while the car is present. """ From 67804b114e60d4999642ac98b0acd879dc7618eb Mon Sep 17 00:00:00 2001 From: Krien Date: Thu, 16 Jan 2025 17:53:55 +0100 Subject: [PATCH 22/46] Fix parameter processing for custom attributes. --- src/oemof/solph/processing.py | 2 +- tests/test_outputlib/test_processing.py | 43 +++++++++++++++++++++++++ 2 files changed, 44 insertions(+), 1 deletion(-) create mode 100644 tests/test_outputlib/test_processing.py diff --git a/src/oemof/solph/processing.py b/src/oemof/solph/processing.py index 86e31744a..60fb9cea3 100644 --- a/src/oemof/solph/processing.py +++ b/src/oemof/solph/processing.py @@ -589,7 +589,7 @@ def detect_scalars_and_sequences(com): def move_undetected_scalars(com): for ckey, value in list(com["sequences"].items()): - if isinstance(value, str): + if isinstance(value, (str, int, float, np.number)): com["scalars"][ckey] = value del com["sequences"][ckey] elif isinstance(value, _FakeSequence): diff --git a/tests/test_outputlib/test_processing.py b/tests/test_outputlib/test_processing.py new file mode 100644 index 000000000..8a25418c3 --- /dev/null +++ b/tests/test_outputlib/test_processing.py @@ -0,0 +1,43 @@ +import pandas as pd + +from oemof.solph import EnergySystem, create_time_index +from oemof.solph import Model +from oemof.solph import processing +from oemof.solph.buses import Bus +from oemof.solph.components import Sink +from oemof.solph.components import Source +from oemof.solph.flows import Flow + + +def test_custom_attribut_with_numeric_value(): + date_time_index = create_time_index(2012, number=6) + energysystem = EnergySystem(timeindex=date_time_index) + bs = Bus(label="bus") + energysystem.add(bs) + src_custom_int = Source( + label="source_with_custom_attribute_int", + outputs={bs: Flow(nominal_value=5, fix=[3] * 7)}, + custom_attributes={"integer": 9}, + ) + s1 = pd.Series([1.4, 2.3], index=["a", "b"]) + snk_custom_float = Sink( + label="source_with_custom_attribute_float", + inputs={bs: Flow()}, + custom_properties={"numpy-float": s1["a"]}, + ) + energysystem.add(snk_custom_float, src_custom_int) + + # create optimization model based on energy_system + optimization_model = Model(energysystem=energysystem) + + parameter = processing.parameter_as_dict(optimization_model) + assert ( + parameter[snk_custom_float, None]["scalars"][ + "custom_properties_numpy-float" + ] + == 1.4 + ) + assert ( + parameter[src_custom_int, None]["scalars"]["custom_properties_integer"] + == 9 + ) From 006b2a24e9f259c4ee3aaa79dcf935608a0862e0 Mon Sep 17 00:00:00 2001 From: Krien Date: Thu, 16 Jan 2025 18:20:48 +0100 Subject: [PATCH 23/46] Check for too high dimensions in sequences --- src/oemof/solph/_plumbing.py | 5 +++++ tests/test_plumbing.py | 10 ++++++++++ 2 files changed, 15 insertions(+) diff --git a/src/oemof/solph/_plumbing.py b/src/oemof/solph/_plumbing.py index 07e6a4517..b6fe2808e 100644 --- a/src/oemof/solph/_plumbing.py +++ b/src/oemof/solph/_plumbing.py @@ -48,6 +48,11 @@ def sequence(iterable_or_scalar): 10 """ + if len(np.shape(iterable_or_scalar)) > 1: + d = len(np.shape(iterable_or_scalar)) + raise ValueError( + f"Dimension too high ({d} > 1) for {iterable_or_scalar}" + ) if isinstance(iterable_or_scalar, str): return iterable_or_scalar elif isinstance(iterable_or_scalar, abc.Iterable): diff --git a/tests/test_plumbing.py b/tests/test_plumbing.py index f97ff96c1..fc726c40f 100644 --- a/tests/test_plumbing.py +++ b/tests/test_plumbing.py @@ -7,6 +7,7 @@ SPDX-License-Identifier: MIT """ import numpy as np +import pandas as pd import pytest from oemof.solph._plumbing import _FakeSequence @@ -66,6 +67,15 @@ def test_sequence(): assert seq_ab == "ab" +def test_dimension_is_too_high_to_create_a_sequence(): + df = pd.DataFrame({"epc": 5}, index=["a"]) + with pytest.raises(ValueError, match="Dimension too high"): + sequence(df) + n2 = [[4]] + with pytest.raises(ValueError, match="Dimension too high"): + sequence(n2) + + def test_valid_sequence(): np_array = np.array([0, 1, 2, 3, 4]) assert valid_sequence(np_array, 5) From a09d36e51ce045242bfc08e3873e117791ebb723 Mon Sep 17 00:00:00 2001 From: Krien Date: Thu, 16 Jan 2025 18:44:59 +0100 Subject: [PATCH 24/46] Add string test --- tests/test_outputlib/test_processing.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/tests/test_outputlib/test_processing.py b/tests/test_outputlib/test_processing.py index 8a25418c3..1a93bc461 100644 --- a/tests/test_outputlib/test_processing.py +++ b/tests/test_outputlib/test_processing.py @@ -25,7 +25,12 @@ def test_custom_attribut_with_numeric_value(): inputs={bs: Flow()}, custom_properties={"numpy-float": s1["a"]}, ) - energysystem.add(snk_custom_float, src_custom_int) + src_custom_str = Source( + label="source_with_custom_attribute_string", + outputs={bs: Flow(nominal_value=5, fix=[3] * 7)}, + custom_attributes={"string": "name"}, + ) + energysystem.add(snk_custom_float, src_custom_int, src_custom_str) # create optimization model based on energy_system optimization_model = Model(energysystem=energysystem) @@ -41,3 +46,7 @@ def test_custom_attribut_with_numeric_value(): parameter[src_custom_int, None]["scalars"]["custom_properties_integer"] == 9 ) + assert ( + parameter[src_custom_str, None]["scalars"]["custom_properties_string"] + == "name" + ) From 5497882a1d84e5fa6d5553c39e033ebbc37f649f Mon Sep 17 00:00:00 2001 From: Krien Date: Thu, 16 Jan 2025 18:46:40 +0100 Subject: [PATCH 25/46] Fix isort issues --- tests/test_outputlib/test_processing.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/test_outputlib/test_processing.py b/tests/test_outputlib/test_processing.py index 1a93bc461..93182e226 100644 --- a/tests/test_outputlib/test_processing.py +++ b/tests/test_outputlib/test_processing.py @@ -1,7 +1,8 @@ import pandas as pd -from oemof.solph import EnergySystem, create_time_index +from oemof.solph import EnergySystem from oemof.solph import Model +from oemof.solph import create_time_index from oemof.solph import processing from oemof.solph.buses import Bus from oemof.solph.components import Sink From d8c8f7a2534470ede3bedbfac5f8161cdc0aec73 Mon Sep 17 00:00:00 2001 From: Uwe Krien Date: Fri, 17 Jan 2025 11:14:38 +0100 Subject: [PATCH 26/46] Use Number class instead of a list of numeric types MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Patrik Schönfeldt --- src/oemof/solph/processing.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/oemof/solph/processing.py b/src/oemof/solph/processing.py index 60fb9cea3..1ddc6af19 100644 --- a/src/oemof/solph/processing.py +++ b/src/oemof/solph/processing.py @@ -589,7 +589,7 @@ def detect_scalars_and_sequences(com): def move_undetected_scalars(com): for ckey, value in list(com["sequences"].items()): - if isinstance(value, (str, int, float, np.number)): + if isinstance(value, (str, numbers.Number)): com["scalars"][ckey] = value del com["sequences"][ckey] elif isinstance(value, _FakeSequence): From 61fab345c183ec5120636f8bcc4640adf7dea1c7 Mon Sep 17 00:00:00 2001 From: Krien Date: Fri, 17 Jan 2025 11:22:25 +0100 Subject: [PATCH 27/46] Extend error message. --- src/oemof/solph/_plumbing.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/oemof/solph/_plumbing.py b/src/oemof/solph/_plumbing.py index b6fe2808e..83b40d08a 100644 --- a/src/oemof/solph/_plumbing.py +++ b/src/oemof/solph/_plumbing.py @@ -51,7 +51,9 @@ def sequence(iterable_or_scalar): if len(np.shape(iterable_or_scalar)) > 1: d = len(np.shape(iterable_or_scalar)) raise ValueError( - f"Dimension too high ({d} > 1) for {iterable_or_scalar}" + f"Dimension too high ({d} > 1) for {iterable_or_scalar}\n" + "The dimension of a number is 0, of a list 1, of a table 2 and so " + "on." ) if isinstance(iterable_or_scalar, str): return iterable_or_scalar From ef4944a2c08f3a5459cb7e49d0e7d3c240563253 Mon Sep 17 00:00:00 2001 From: Krien Date: Fri, 17 Jan 2025 11:24:08 +0100 Subject: [PATCH 28/46] Rename custom_attributes to custom_properties to be in line with other classes --- src/oemof/solph/components/_source.py | 18 ++++++++++++++---- tests/test_outputlib/test_processing.py | 2 +- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/src/oemof/solph/components/_source.py b/src/oemof/solph/components/_source.py index d775d16d8..1e097b877 100644 --- a/src/oemof/solph/components/_source.py +++ b/src/oemof/solph/components/_source.py @@ -27,6 +27,9 @@ class Source(Node): outputs: dict A dictionary mapping input nodes to corresponding outflows (i.e. output values). + custom_properties: dict + Additional keyword arguments for use in user-defined equations or for + information purposes. Examples -------- @@ -47,16 +50,23 @@ class Source(Node): >>> str(pv_plant.outputs[bel].output) 'electricity' + >>> bgas = solph.buses.Bus(label='gas') + >>> gas_source = solph.components.Source( + ... label='gas_import', + ... outputs={bgas: solph.flows.Flow()}, + ... custom_properties={"emission": 201}) # g/kWh + >>> gas_source.custom_properties["emission"] + 201 """ - def __init__(self, label=None, *, outputs, custom_attributes=None): + def __init__(self, label=None, *, outputs, custom_properties=None): if outputs is None: outputs = {} - if custom_attributes is None: - custom_attributes = {} + if custom_properties is None: + custom_properties = {} super().__init__( - label=label, outputs=outputs, custom_properties=custom_attributes + label=label, outputs=outputs, custom_properties=custom_properties ) def constraint_group(self): diff --git a/tests/test_outputlib/test_processing.py b/tests/test_outputlib/test_processing.py index 93182e226..265805bee 100644 --- a/tests/test_outputlib/test_processing.py +++ b/tests/test_outputlib/test_processing.py @@ -29,7 +29,7 @@ def test_custom_attribut_with_numeric_value(): src_custom_str = Source( label="source_with_custom_attribute_string", outputs={bs: Flow(nominal_value=5, fix=[3] * 7)}, - custom_attributes={"string": "name"}, + custom_properties={"string": "name"}, ) energysystem.add(snk_custom_float, src_custom_int, src_custom_str) From e98d4a223b687393552373f2fb6a058b86f4733e Mon Sep 17 00:00:00 2001 From: Krien Date: Fri, 17 Jan 2025 11:27:11 +0100 Subject: [PATCH 29/46] Add missing import --- src/oemof/solph/processing.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/oemof/solph/processing.py b/src/oemof/solph/processing.py index 1ddc6af19..3e0e4523e 100644 --- a/src/oemof/solph/processing.py +++ b/src/oemof/solph/processing.py @@ -16,6 +16,7 @@ """ +import numbers import sys from collections import abc from itertools import groupby From 349a800cd31e501cb3c25d1634c5ced30284969e Mon Sep 17 00:00:00 2001 From: Krien Date: Fri, 17 Jan 2025 11:31:19 +0100 Subject: [PATCH 30/46] Fix name of attribute in test --- tests/test_outputlib/test_processing.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_outputlib/test_processing.py b/tests/test_outputlib/test_processing.py index 265805bee..17e2b7677 100644 --- a/tests/test_outputlib/test_processing.py +++ b/tests/test_outputlib/test_processing.py @@ -18,7 +18,7 @@ def test_custom_attribut_with_numeric_value(): src_custom_int = Source( label="source_with_custom_attribute_int", outputs={bs: Flow(nominal_value=5, fix=[3] * 7)}, - custom_attributes={"integer": 9}, + custom_properties={"integer": 9}, ) s1 = pd.Series([1.4, 2.3], index=["a", "b"]) snk_custom_float = Sink( From b249b771b6b4147744d715c37d86c21eea47ddaf Mon Sep 17 00:00:00 2001 From: Krien Date: Fri, 17 Jan 2025 11:41:31 +0100 Subject: [PATCH 31/46] Add an additional hint to the error message --- src/oemof/solph/_plumbing.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/oemof/solph/_plumbing.py b/src/oemof/solph/_plumbing.py index 83b40d08a..8a4e3976e 100644 --- a/src/oemof/solph/_plumbing.py +++ b/src/oemof/solph/_plumbing.py @@ -53,7 +53,8 @@ def sequence(iterable_or_scalar): raise ValueError( f"Dimension too high ({d} > 1) for {iterable_or_scalar}\n" "The dimension of a number is 0, of a list 1, of a table 2 and so " - "on." + "on.\nPlease notice that a table with one column is still a table " + "with 2 dimensions and not a Series." ) if isinstance(iterable_or_scalar, str): return iterable_or_scalar From 0eb2fbca43124fc3353550f46b10308f40efbd17 Mon Sep 17 00:00:00 2001 From: antonella Date: Sat, 18 Jan 2025 18:39:20 +0100 Subject: [PATCH 32/46] add dynamic prices --- tutorials/introductory/ev_charging.py | 69 +++++++++++++++++++++++++++ 1 file changed, 69 insertions(+) diff --git a/tutorials/introductory/ev_charging.py b/tutorials/introductory/ev_charging.py index ae0c3ac3e..0a473dd38 100644 --- a/tutorials/introductory/ev_charging.py +++ b/tutorials/introductory/ev_charging.py @@ -265,3 +265,72 @@ def add_domestic_socket_discharging(energy_system, b_car): solve_and_plot("Bidirectional use") plt.show() + +# %%[AC_variable_costs] +""" Now the energy system stays the same. But dynamic prices are avaiable at home, +so the loading and unloading if the price is low or the gain is high. The prices are used +at home""" + +# assuming the prices are low in the night and early morning (until 8 a.m. and after 4 p.m) and high ad later morning midday and afternoon (between 6 a.m. and 4 p.m.) + +dynamic_price = pd.Series(0, index=time_index[:-1]) +dynamic_price.loc[:time_index[8*12]] = 0.05 +dynamic_price.loc[time_index[8*12]:time_index[16*12]] = 0.5 +dynamic_price.loc[time_index[16*12]:]= 0.7 + + + +def add_domestic_socket_charging_variable_costs(energy_system, b_car,dynamic_price): + car_at_home = pd.Series(1, index=time_index[:-1]) + car_at_home.loc[driving_start_morning:driving_end_evening] = 0 + + + + # To be able to load the battery a electric source e.g. electric grid is + # necessary. We set the maximum use to 1 if the car is present, while it + # is 0 between the morning start and the evening arrival back home. + # While the car itself can potentially charge with at a higher power, + # we just add an AC source with 16 A at 230 V. + charger230V = solph.components.Source( + label="230V AC", + outputs={ + b_car: solph.Flow( + nominal_capacity=3.68, # 230 V * 16 A = 3.68 kW + variable_costs=dynamic_price, # 30 ct/kWh + max=car_at_home, + ) + }, + ) + + energy_system.add(charger230V) + + +def add_domestic_socket_discharging_variable_costs(energy_system, b_car,dynamic_price): + car_at_home = pd.Series(1, index=time_index[:-1]) + car_at_home.loc[driving_start_morning:driving_end_evening] = 0 + + # Same as above, but electricity is cheaper every other step. + # Thus, battery is only charged these steps. + discharger230V = solph.components.Sink( + label="230V AC discharge", + inputs={ + b_car: solph.Flow( + nominal_capacity=3.68, # 230 V * 16 A = 3.68 kW + variable_costs=-dynamic_price, + max=car_at_home, + ) + }, + ) + + energy_system.add(discharger230V) + + +es, b_car = create_base_system() +add_balanced_battery(es, b_car) +add_domestic_socket_charging_variable_costs(es, b_car,dynamic_price) +add_domestic_socket_discharging_variable_costs(es, b_car,dynamic_price) +add_11kW_charging(es, b_car) +solve_and_plot("Bidirectional use variable costs") + +plt.show() +# %% From 716817244c305a979859d3866b8f7f06adb2403c Mon Sep 17 00:00:00 2001 From: antonella Date: Sat, 18 Jan 2025 18:41:30 +0100 Subject: [PATCH 33/46] change comments --- tutorials/introductory/ev_charging.py | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/tutorials/introductory/ev_charging.py b/tutorials/introductory/ev_charging.py index 0a473dd38..465c97b02 100644 --- a/tutorials/introductory/ev_charging.py +++ b/tutorials/introductory/ev_charging.py @@ -286,17 +286,13 @@ def add_domestic_socket_charging_variable_costs(energy_system, b_car,dynamic_pri - # To be able to load the battery a electric source e.g. electric grid is - # necessary. We set the maximum use to 1 if the car is present, while it - # is 0 between the morning start and the evening arrival back home. - # While the car itself can potentially charge with at a higher power, - # we just add an AC source with 16 A at 230 V. + # use the configuration as before but use the dynamic prices charger230V = solph.components.Source( label="230V AC", outputs={ b_car: solph.Flow( nominal_capacity=3.68, # 230 V * 16 A = 3.68 kW - variable_costs=dynamic_price, # 30 ct/kWh + variable_costs=dynamic_price, max=car_at_home, ) }, @@ -309,8 +305,7 @@ def add_domestic_socket_discharging_variable_costs(energy_system, b_car,dynamic_ car_at_home = pd.Series(1, index=time_index[:-1]) car_at_home.loc[driving_start_morning:driving_end_evening] = 0 - # Same as above, but electricity is cheaper every other step. - # Thus, battery is only charged these steps. + # use the configuration as before but use the dynamic prices as gain discharger230V = solph.components.Sink( label="230V AC discharge", inputs={ From 5836e4db08d9651aacbf0ab091b1b226d028ea24 Mon Sep 17 00:00:00 2001 From: antonella Date: Sat, 18 Jan 2025 18:43:19 +0100 Subject: [PATCH 34/46] change docu --- tutorials/introductory/ev_charging.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/tutorials/introductory/ev_charging.py b/tutorials/introductory/ev_charging.py index 465c97b02..cd1df78d6 100644 --- a/tutorials/introductory/ev_charging.py +++ b/tutorials/introductory/ev_charging.py @@ -268,8 +268,7 @@ def add_domestic_socket_discharging(energy_system, b_car): # %%[AC_variable_costs] """ Now the energy system stays the same. But dynamic prices are avaiable at home, -so the loading and unloading if the price is low or the gain is high. The prices are used -at home""" +so the loading and unloading if the price is low or the gain is high.""" # assuming the prices are low in the night and early morning (until 8 a.m. and after 4 p.m) and high ad later morning midday and afternoon (between 6 a.m. and 4 p.m.) @@ -284,8 +283,6 @@ def add_domestic_socket_charging_variable_costs(energy_system, b_car,dynamic_pri car_at_home = pd.Series(1, index=time_index[:-1]) car_at_home.loc[driving_start_morning:driving_end_evening] = 0 - - # use the configuration as before but use the dynamic prices charger230V = solph.components.Source( label="230V AC", From 92ec0ff972cec38b0d29369a0120a12fe5d8c062 Mon Sep 17 00:00:00 2001 From: antonella Date: Sat, 18 Jan 2025 19:51:32 +0100 Subject: [PATCH 35/46] black --- tutorials/introductory/ev_charging.py | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/tutorials/introductory/ev_charging.py b/tutorials/introductory/ev_charging.py index cd1df78d6..17a4ce206 100644 --- a/tutorials/introductory/ev_charging.py +++ b/tutorials/introductory/ev_charging.py @@ -4,9 +4,10 @@ import matplotlib to plot the data right away and import solph """ +import matplotlib.pyplot as plt import numpy as np import pandas as pd -import matplotlib.pyplot as plt + import oemof.solph as solph # %%[trip_data] @@ -273,13 +274,14 @@ def add_domestic_socket_discharging(energy_system, b_car): # assuming the prices are low in the night and early morning (until 8 a.m. and after 4 p.m) and high ad later morning midday and afternoon (between 6 a.m. and 4 p.m.) dynamic_price = pd.Series(0, index=time_index[:-1]) -dynamic_price.loc[:time_index[8*12]] = 0.05 -dynamic_price.loc[time_index[8*12]:time_index[16*12]] = 0.5 -dynamic_price.loc[time_index[16*12]:]= 0.7 - +dynamic_price.loc[: time_index[8 * 12]] = 0.05 +dynamic_price.loc[time_index[8 * 12] : time_index[16 * 12]] = 0.5 +dynamic_price.loc[time_index[16 * 12] :] = 0.7 -def add_domestic_socket_charging_variable_costs(energy_system, b_car,dynamic_price): +def add_domestic_socket_charging_variable_costs( + energy_system, b_car, dynamic_price +): car_at_home = pd.Series(1, index=time_index[:-1]) car_at_home.loc[driving_start_morning:driving_end_evening] = 0 @@ -289,7 +291,7 @@ def add_domestic_socket_charging_variable_costs(energy_system, b_car,dynamic_pri outputs={ b_car: solph.Flow( nominal_capacity=3.68, # 230 V * 16 A = 3.68 kW - variable_costs=dynamic_price, + variable_costs=dynamic_price, max=car_at_home, ) }, @@ -298,7 +300,9 @@ def add_domestic_socket_charging_variable_costs(energy_system, b_car,dynamic_pri energy_system.add(charger230V) -def add_domestic_socket_discharging_variable_costs(energy_system, b_car,dynamic_price): +def add_domestic_socket_discharging_variable_costs( + energy_system, b_car, dynamic_price +): car_at_home = pd.Series(1, index=time_index[:-1]) car_at_home.loc[driving_start_morning:driving_end_evening] = 0 @@ -319,8 +323,8 @@ def add_domestic_socket_discharging_variable_costs(energy_system, b_car,dynamic_ es, b_car = create_base_system() add_balanced_battery(es, b_car) -add_domestic_socket_charging_variable_costs(es, b_car,dynamic_price) -add_domestic_socket_discharging_variable_costs(es, b_car,dynamic_price) +add_domestic_socket_charging_variable_costs(es, b_car, dynamic_price) +add_domestic_socket_discharging_variable_costs(es, b_car, dynamic_price) add_11kW_charging(es, b_car) solve_and_plot("Bidirectional use variable costs") From 135257e34e2569dfa881c8fcde9181d1c4fa9f06 Mon Sep 17 00:00:00 2001 From: antonella Date: Sat, 18 Jan 2025 20:00:25 +0100 Subject: [PATCH 36/46] fixing --- tutorials/introductory/ev_charging.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/tutorials/introductory/ev_charging.py b/tutorials/introductory/ev_charging.py index 17a4ce206..38fd5493d 100644 --- a/tutorials/introductory/ev_charging.py +++ b/tutorials/introductory/ev_charging.py @@ -268,10 +268,14 @@ def add_domestic_socket_discharging(energy_system, b_car): plt.show() # %%[AC_variable_costs] -""" Now the energy system stays the same. But dynamic prices are avaiable at home, -so the loading and unloading if the price is low or the gain is high.""" +""" +Now the energy system stays the same. But dynamic prices +are avaiable at home, so the loading and unloading if the +price is low or the gain is high.""" -# assuming the prices are low in the night and early morning (until 8 a.m. and after 4 p.m) and high ad later morning midday and afternoon (between 6 a.m. and 4 p.m.) +# assuming the prices are low in the night and early morning +# (until 8 a.m. and after 4 p.m) and high at later morning, +# midday and afternoon (between 6 a.m. and 4 p.m.) dynamic_price = pd.Series(0, index=time_index[:-1]) dynamic_price.loc[: time_index[8 * 12]] = 0.05 @@ -306,7 +310,8 @@ def add_domestic_socket_discharging_variable_costs( car_at_home = pd.Series(1, index=time_index[:-1]) car_at_home.loc[driving_start_morning:driving_end_evening] = 0 - # use the configuration as before but use the dynamic prices as gain + # use the configuration as before but use the dynamic prices + # as gain discharger230V = solph.components.Sink( label="230V AC discharge", inputs={ From b7a28cc8897dd4d5ac1d2417b864d805066470ab Mon Sep 17 00:00:00 2001 From: antonella Date: Sat, 18 Jan 2025 20:04:04 +0100 Subject: [PATCH 37/46] fixing flake8 --- tutorials/introductory/ev_charging.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tutorials/introductory/ev_charging.py b/tutorials/introductory/ev_charging.py index 38fd5493d..0c7db5a7f 100644 --- a/tutorials/introductory/ev_charging.py +++ b/tutorials/introductory/ev_charging.py @@ -268,13 +268,13 @@ def add_domestic_socket_discharging(energy_system, b_car): plt.show() # %%[AC_variable_costs] -""" -Now the energy system stays the same. But dynamic prices +""" +Now the energy system stays the same. But dynamic prices are avaiable at home, so the loading and unloading if the price is low or the gain is high.""" -# assuming the prices are low in the night and early morning -# (until 8 a.m. and after 4 p.m) and high at later morning, +# assuming the prices are low in the night and early morning +# # (until 8 a.m. and after 4 p.m) and high at later morning, # midday and afternoon (between 6 a.m. and 4 p.m.) dynamic_price = pd.Series(0, index=time_index[:-1]) @@ -310,7 +310,7 @@ def add_domestic_socket_discharging_variable_costs( car_at_home = pd.Series(1, index=time_index[:-1]) car_at_home.loc[driving_start_morning:driving_end_evening] = 0 - # use the configuration as before but use the dynamic prices + # use the configuration as before but use the dynamic prices # as gain discharger230V = solph.components.Sink( label="230V AC discharge", From 66d357643007b50da9fd9a598e5e2d0f691ce632 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrik=20Sch=C3=B6nfeldt?= Date: Mon, 20 Jan 2025 14:01:42 +0100 Subject: [PATCH 38/46] Refactor variable price example --- tutorials/introductory/ev_charging.py | 33 +++++++++------------------ 1 file changed, 11 insertions(+), 22 deletions(-) diff --git a/tutorials/introductory/ev_charging.py b/tutorials/introductory/ev_charging.py index 0c7db5a7f..6c888adbe 100644 --- a/tutorials/introductory/ev_charging.py +++ b/tutorials/introductory/ev_charging.py @@ -263,9 +263,7 @@ def add_domestic_socket_discharging(energy_system, b_car): add_domestic_socket_charging(es, b_car) add_domestic_socket_discharging(es, b_car) add_11kW_charging(es, b_car) -solve_and_plot("Bidirectional use") - -plt.show() +solve_and_plot("Bidirectional use (constant costs)") # %%[AC_variable_costs] """ @@ -277,13 +275,15 @@ def add_domestic_socket_discharging(energy_system, b_car): # # (until 8 a.m. and after 4 p.m) and high at later morning, # midday and afternoon (between 6 a.m. and 4 p.m.) -dynamic_price = pd.Series(0, index=time_index[:-1]) -dynamic_price.loc[: time_index[8 * 12]] = 0.05 -dynamic_price.loc[time_index[8 * 12] : time_index[16 * 12]] = 0.5 -dynamic_price.loc[time_index[16 * 12] :] = 0.7 +dynamic_price = pd.Series(0.5, index=time_index[:-1]) +dynamic_price.loc[: pd.Timestamp("2025-01-01 06:00")] = 0.05 +dynamic_price.loc[ + pd.Timestamp("2025-01-01 06:00") : pd.Timestamp("2025-01-01 10:00") +] = 0.5 +dynamic_price.loc[pd.Timestamp("2025-01-01 16:00") :] = 0.7 -def add_domestic_socket_charging_variable_costs( +def add_domestic_socket_variable_costs( energy_system, b_car, dynamic_price ): car_at_home = pd.Series(1, index=time_index[:-1]) @@ -301,15 +301,6 @@ def add_domestic_socket_charging_variable_costs( }, ) - energy_system.add(charger230V) - - -def add_domestic_socket_discharging_variable_costs( - energy_system, b_car, dynamic_price -): - car_at_home = pd.Series(1, index=time_index[:-1]) - car_at_home.loc[driving_start_morning:driving_end_evening] = 0 - # use the configuration as before but use the dynamic prices # as gain discharger230V = solph.components.Sink( @@ -323,15 +314,13 @@ def add_domestic_socket_discharging_variable_costs( }, ) - energy_system.add(discharger230V) + energy_system.add(charger230V, discharger230V) es, b_car = create_base_system() add_balanced_battery(es, b_car) -add_domestic_socket_charging_variable_costs(es, b_car, dynamic_price) -add_domestic_socket_discharging_variable_costs(es, b_car, dynamic_price) +add_domestic_socket_variable_costs(es, b_car, dynamic_price) add_11kW_charging(es, b_car) -solve_and_plot("Bidirectional use variable costs") +solve_and_plot("Bidirectional use (variable costs)") plt.show() -# %% From 7b4ff42bdd4ea808ef4cfeba31aa5ac07cc13482 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrik=20Sch=C3=B6nfeldt?= Date: Mon, 20 Jan 2025 14:03:35 +0100 Subject: [PATCH 39/46] Adhere to Black --- tutorials/introductory/ev_charging.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/tutorials/introductory/ev_charging.py b/tutorials/introductory/ev_charging.py index 6c888adbe..be58da3c6 100644 --- a/tutorials/introductory/ev_charging.py +++ b/tutorials/introductory/ev_charging.py @@ -283,9 +283,7 @@ def add_domestic_socket_discharging(energy_system, b_car): dynamic_price.loc[pd.Timestamp("2025-01-01 16:00") :] = 0.7 -def add_domestic_socket_variable_costs( - energy_system, b_car, dynamic_price -): +def add_domestic_socket_variable_costs(energy_system, b_car, dynamic_price): car_at_home = pd.Series(1, index=time_index[:-1]) car_at_home.loc[driving_start_morning:driving_end_evening] = 0 From 1cce93ff392404b60d13649addcebda83ed7c07b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrik=20Sch=C3=B6nfeldt?= Date: Mon, 20 Jan 2025 17:40:42 +0100 Subject: [PATCH 40/46] Apply suggestions from code review Replace assert by pytest use --- tests/test_models.py | 32 +++++--------------------------- 1 file changed, 5 insertions(+), 27 deletions(-) diff --git a/tests/test_models.py b/tests/test_models.py index 8cb8bf9b0..3493693b1 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -17,7 +17,7 @@ from oemof import solph -def test_infeasible_model_warning(): +def test_infeasible_model(): es = solph.EnergySystem(timeincrement=[1]) bel = solph.buses.Bus(label="bus") es.add(bel) @@ -34,32 +34,12 @@ def test_infeasible_model_warning(): ) ) m = solph.Model(es) - with warnings.catch_warnings(record=False) as w: - m.solve(solver="cbc", allow_nonoptimal=True) - assert "The solver did not return an optimal solution" in str( - w[0].message - ) + with pytest.warns(UserWarning, match="The solver did not return an optimal solution"): + m.solve(solver="cbc", allow_nonoptimal=True) -def test_infeasible_model_error(): - es = solph.EnergySystem(timeincrement=[1]) - bel = solph.buses.Bus(label="bus") - es.add(bel) - es.add( - solph.components.Sink( - inputs={bel: solph.flows.Flow(nominal_value=5, fix=[1])} - ) - ) - es.add( - solph.components.Source( - outputs={bel: solph.flows.Flow(nominal_value=4, variable_costs=5)} - ) - ) - m = solph.Model(es) - try: + with pytest.raises(RuntimeError, match="The solver did not return an optimal solution"): m.solve(solver="cbc", allow_nonoptimal=False) - except Exception as e: - assert "The solver did not return an optimal solution" in str(e) def test_unbounded_model(): @@ -77,10 +57,8 @@ def test_unbounded_model(): ) m = solph.Model(es) - try: + with pytest.raises(RuntimeError, match="The solver did not return an optimal solution"): m.solve(solver="cbc", allow_nonoptimal=False) - except Exception as e: - assert "The solver did not return an optimal solution" in str(e) @pytest.mark.filterwarnings( From 61151bb6c62748e7691d4cd08ba5e35350c3341f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrik=20Sch=C3=B6nfeldt?= Date: Mon, 20 Jan 2025 17:46:14 +0100 Subject: [PATCH 41/46] Fix format of model tests --- tests/test_models.py | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/tests/test_models.py b/tests/test_models.py index 3493693b1..fc537444b 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -34,11 +34,14 @@ def test_infeasible_model(): ) ) m = solph.Model(es) - with pytest.warns(UserWarning, match="The solver did not return an optimal solution"): - m.solve(solver="cbc", allow_nonoptimal=True) - - - with pytest.raises(RuntimeError, match="The solver did not return an optimal solution"): + with pytest.warns( + UserWarning, match="The solver did not return an optimal solution" + ): + m.solve(solver="cbc", allow_nonoptimal=True) + + with pytest.raises( + RuntimeError, match="The solver did not return an optimal solution" + ): m.solve(solver="cbc", allow_nonoptimal=False) @@ -57,7 +60,9 @@ def test_unbounded_model(): ) m = solph.Model(es) - with pytest.raises(RuntimeError, match="The solver did not return an optimal solution"): + with pytest.raises( + RuntimeError, match="The solver did not return an optimal solution" + ): m.solve(solver="cbc", allow_nonoptimal=False) From c9570f4417cff7fc5f12d5d9679e13b6d4787599 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrik=20Sch=C3=B6nfeldt?= Date: Wed, 22 Jan 2025 08:39:15 +0100 Subject: [PATCH 42/46] Use cbc in offset_converter_example The example used to be belinear and required gurobi. This, however, is no longer the case (since solph v0.5.0). Thus, cbc can be used as the default solver. (Thanks to Rainer Gaier for finding this issue.) --- docs/whatsnew/v0-6-0.rst | 1 + .../offset_diesel_genset_nonconvex_investment.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/whatsnew/v0-6-0.rst b/docs/whatsnew/v0-6-0.rst index 25e9551be..35e32f9f0 100644 --- a/docs/whatsnew/v0-6-0.rst +++ b/docs/whatsnew/v0-6-0.rst @@ -35,6 +35,7 @@ Documentation Bug fixes ######### +* Remove unneeded use of gurobi from examples. Other changes ############# diff --git a/examples/offset_converter_example/offset_diesel_genset_nonconvex_investment.py b/examples/offset_converter_example/offset_diesel_genset_nonconvex_investment.py index 841a6b8a1..1ed94c110 100644 --- a/examples/offset_converter_example/offset_diesel_genset_nonconvex_investment.py +++ b/examples/offset_converter_example/offset_diesel_genset_nonconvex_investment.py @@ -281,7 +281,7 @@ def offset_converter_example(): # The higher the MipGap or ratioGap, the faster the solver would converge, # but the less accurate the results would be. solver_option = {"gurobi": {"MipGap": "0.02"}, "cbc": {"ratioGap": "0.02"}} - solver = "gurobi" + solver = "cbc" model = solph.Model(energy_system) model.solve( From 520f963ed885a436ab3fa5018ada1df409a7c84d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrik=20Sch=C3=B6nfeldt?= Date: Wed, 22 Jan 2025 09:32:07 +0100 Subject: [PATCH 43/46] Adjust saturating_storage example for solph v0.6 --- .../flexible_modelling/saturating_storage.py | 24 ++++++++++++------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/examples/flexible_modelling/saturating_storage.py b/examples/flexible_modelling/saturating_storage.py index 342a6733f..bdba0d2c0 100644 --- a/examples/flexible_modelling/saturating_storage.py +++ b/examples/flexible_modelling/saturating_storage.py @@ -19,7 +19,7 @@ Installation requirements ------------------------- -This example requires oemof.solph (v0.5.x), install by: +This example requires oemof.solph (v0.6.x), install by: .. code:: bash @@ -86,21 +86,21 @@ def saturating_storage_example(): model = solph.Model(es) def soc_limit_rule(m): - for p, ts in m.TIMEINDEX: + for ts in m.TIMESTEPS: soc = ( m.GenericStorageBlock.storage_content[battery, ts + 1] / storage_capacity ) expr = (1 - soc) / (1 - full_charging_limit) >= m.flow[ - bel, battery, p, ts + bel, battery, ts ] / inflow_capacity - getattr(m, "soc_limit").add((p, ts), expr) + getattr(m, "soc_limit").add(ts, expr) setattr( model, "soc_limit", po.Constraint( - model.TIMEINDEX, + model.TIMESTEPS, noruleinit=True, ), ) @@ -116,17 +116,23 @@ def soc_limit_rule(m): # create result object results = solph.processing.results(model) - plt.plot(results[(battery, None)]["sequences"], "r--", label="content") + plt.plot( + results[(battery, None)]["sequences"]["storage_content"], + "r--", + label="content", + ) plt.step( - 20 * results[(bel, battery)]["sequences"], "b-", label="20*inflow" + 20 * results[(bel, battery)]["sequences"]["flow"], + "b-", + label="20*inflow", ) plt.legend() plt.grid() plt.figure() plt.plot( - results[(battery, None)]["sequences"][1:], - results[(bel, battery)]["sequences"][:-1], + results[(battery, None)]["sequences"]["storage_content"][1:], + results[(bel, battery)]["sequences"]["flow"][:-1], "b-", ) plt.grid() From e03fa9086286d83e58b21fe4ef3cd6034f677012 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrik=20Sch=C3=B6nfeldt?= Date: Wed, 22 Jan 2025 09:32:39 +0100 Subject: [PATCH 44/46] Fix emission limit example --- examples/emission_constraint/emission_constraint.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/emission_constraint/emission_constraint.py b/examples/emission_constraint/emission_constraint.py index bced70d38..26f877330 100644 --- a/examples/emission_constraint/emission_constraint.py +++ b/examples/emission_constraint/emission_constraint.py @@ -109,7 +109,7 @@ def main(): constraints.emission_limit(model, limit=100) # print out the emission constraint - model.integral_limit_emission_factor_constraint.pprint() + model.integral_limit_emission_factor_upper_limit.pprint() model.integral_limit_emission_factor.pprint() # solve the model From 6e0a7573c7b09172b1c556cf17672424a3fbace1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrik=20Sch=C3=B6nfeldt?= Date: Wed, 22 Jan 2025 09:57:07 +0100 Subject: [PATCH 45/46] Fix rendering error in saturating storage example results --- .../time_index_example/non_equidistant_time_step_example.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/time_index_example/non_equidistant_time_step_example.py b/examples/time_index_example/non_equidistant_time_step_example.py index 33f40fb3d..7b9d3355d 100644 --- a/examples/time_index_example/non_equidistant_time_step_example.py +++ b/examples/time_index_example/non_equidistant_time_step_example.py @@ -135,7 +135,7 @@ def main(): ]["flow"] results_df["storage_relative"] = results[(storage_relative, None)][ "sequences" - ] + ]["storage_content"] results_df["storage_relative_inflow"] = results[(bus, storage_relative)][ "sequences" ]["flow"] From a62400520d02a19e5ac51f9dbc9c31edc68a29aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrik=20Sch=C3=B6nfeldt?= Date: Wed, 22 Jan 2025 09:58:19 +0100 Subject: [PATCH 46/46] Let examples work in smoke test In the smoke test, "os.path.abspath(__file__)" will give the path of the smoke test. Thus, loading files has to consider this. --- examples/basic_example/basic_example.py | 7 +++++-- .../offset_diesel_genset_nonconvex_investment.py | 13 ++++++++++--- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/examples/basic_example/basic_example.py b/examples/basic_example/basic_example.py index 2afd5a4e2..1847f5c2d 100644 --- a/examples/basic_example/basic_example.py +++ b/examples/basic_example/basic_example.py @@ -81,8 +81,11 @@ def get_data_from_file_path(file_path: str) -> pd.DataFrame: - dir = os.path.dirname(os.path.abspath(__file__)) - data = pd.read_csv(dir + "/" + file_path) + try: + data = pd.read_csv(file_path) + except FileNotFoundError: + dir = os.path.dirname(os.path.abspath(__file__)) + data = pd.read_csv(dir + "/" + file_path) return data diff --git a/examples/offset_converter_example/offset_diesel_genset_nonconvex_investment.py b/examples/offset_converter_example/offset_diesel_genset_nonconvex_investment.py index 1ed94c110..e3b861dc7 100644 --- a/examples/offset_converter_example/offset_diesel_genset_nonconvex_investment.py +++ b/examples/offset_converter_example/offset_diesel_genset_nonconvex_investment.py @@ -60,6 +60,15 @@ plt = None +def get_data_from_file_path(file_path: str) -> pd.DataFrame: + try: + data = pd.read_csv(file_path) + except FileNotFoundError: + dir = os.path.dirname(os.path.abspath(__file__)) + data = pd.read_csv(dir + "/" + file_path) + return data + + def offset_converter_example(): ########################################################################## # Initialize the energy system and calculate necessary parameters @@ -84,9 +93,7 @@ def offset_converter_example(): end_datetime = start_datetime + timedelta(days=n_days) # Import data. - current_directory = os.path.dirname(os.path.abspath(__file__)) - filename = os.path.join(current_directory, "diesel_genset_data.csv") - data = pd.read_csv(filepath_or_buffer=filename) + data = get_data_from_file_path("diesel_genset_data.csv") # Change the index of data to be able to select data based on the time range. data.index = pd.date_range(start="2022-01-01", periods=len(data), freq="h")