From 6d2716b15ee35ce26a2777da90ddb80db3586809 Mon Sep 17 00:00:00 2001 From: davidwickh <56641696+davidwickh@users.noreply.github.com> Date: Thu, 26 Oct 2023 00:12:03 +0100 Subject: [PATCH 01/10] Adding electricity_flow_variables.py --- optimiser/variables/__init__.py | 0 .../variables/electricity_flow_variables.py | 141 ++++++++++++++++++ 2 files changed, 141 insertions(+) create mode 100644 optimiser/variables/__init__.py create mode 100644 optimiser/variables/electricity_flow_variables.py diff --git a/optimiser/variables/__init__.py b/optimiser/variables/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/optimiser/variables/electricity_flow_variables.py b/optimiser/variables/electricity_flow_variables.py new file mode 100644 index 0000000..da791fa --- /dev/null +++ b/optimiser/variables/electricity_flow_variables.py @@ -0,0 +1,141 @@ +""" +Module contains the definition of the electricity flow variables. +""" +# pylint: disable=import-error +import pulp as pl # type: ignore + + +class RenewableElectricityFlowVariables: + """ + Class contains the definition of the renewable electricity flow variables. + + This represents the electricity flow from the renewable energy sources to the house and battery. + """ + + @classmethod + def get_renewable_electricity_to_house(cls, time_slices: float) -> pl.LpVariable: + """ + Method that returns the renewable electricity to house variable. + :param time_slices: + :return: + """ + return pl.LpVariable.dicts( + name="renewable_electricity_to_house_{t}", + indices=time_slices, + lowBound=0, + cat="Continuous", + ) + + @classmethod + def get_renewable_electricity_to_battery(cls, time_slices: float) -> pl.LpVariable: + """ + Method that returns the renewable electricity to battery variable. + :param time_slices: + :return: + """ + return pl.LpVariable.dicts( + name="renewable_electricity_to_battery_{t}", + indices=time_slices, + lowBound=0, + cat="Continuous", + ) + + @classmethod + def get_total_renewable_generation(cls, time_slices: float) -> pl.LpVariable: + """ + Method that returns the total renewable generation variable. + :param time_slices: + :return: + """ + return pl.LpVariable.dicts( + name="total_renewable_generation_{t}", + indices=time_slices, + lowBound=0, + cat="Continuous", + ) + + +class BatteryElectricityFlowVariables: + """ + Class contains the definition of the battery electricity flow variables. + + This represents the electricity flow from the battery to the house and grid. + """ + + @classmethod + def get_battery_electricity_to_house(cls, time_slices: float) -> pl.LpVariable: + """ + Method that returns the battery electricity to house variable. + :param time_slices: + :return: + """ + return pl.LpVariable.dicts( + name="battery_electricity_to_house_{time_slices}", + indices=time_slices, + lowBound=0, + cat="Continuous", + ) + + @classmethod + def get_battery_electricity_to_grid(cls, time_slices: float) -> pl.LpVariable: + """ + Method that returns the battery electricity to grid variable. + :param time_slices: + :return: + """ + return pl.LpVariable.dicts( + name="battery_electricity_to_grid_{t}", + indices=time_slices, + lowBound=0, + cat="Continuous", + ) + + @classmethod + def get_electricity_to_battery(cls, time_slices: float) -> pl.LpVariable: + """ + Method that returns the electricity to battery variable. + :param time_slices: + :return: + """ + return pl.LpVariable.dicts( + name="electricity_to_battery_{t}", + indices=time_slices, + lowBound=0, + cat="Continuous", + ) + + +class GridElectricityFlowVariables: + """ + Class contains the definition of the grid electricity flow variables. + + This represents the electricity flow from the grid to the house and battery. + """ + + @classmethod + def get_grid_electricity_to_house(cls, time_slices: float) -> pl.LpVariable: + """ + Method that returns the grid electricity to house variable. + :param time_slices: + :return: + """ + return pl.LpVariable.dicts( + name="grid_electricity_to_house_{t}", + indices=time_slices, + lowBound=0, + cat="Continuous", + ) + + @classmethod + def get_grid_electricity_to_battery(cls, time_slices: float) -> pl.LpVariable: + """ + Method that returns the grid electricity to battery variable. + :param time_slices: + :return: + """ + return pl.LpVariable.dicts( + name="grid_electricity_to_battery_{t}", + indices=time_slices, + lowBound=0, + cat="Continuous", + ) From 1ba377b96b3bc6f5756ca52230553224780ff4c8 Mon Sep 17 00:00:00 2001 From: davidwickh <56641696+davidwickh@users.noreply.github.com> Date: Thu, 26 Oct 2023 00:20:46 +0100 Subject: [PATCH 02/10] Adding fuel_cost_variables.py --- .../variables/electricity_flow_variables.py | 6 +- optimiser/variables/fuel_cost_variables.py | 78 +++++++++++++++++++ 2 files changed, 81 insertions(+), 3 deletions(-) create mode 100644 optimiser/variables/fuel_cost_variables.py diff --git a/optimiser/variables/electricity_flow_variables.py b/optimiser/variables/electricity_flow_variables.py index da791fa..682be6f 100644 --- a/optimiser/variables/electricity_flow_variables.py +++ b/optimiser/variables/electricity_flow_variables.py @@ -13,7 +13,7 @@ class RenewableElectricityFlowVariables: """ @classmethod - def get_renewable_electricity_to_house(cls, time_slices: float) -> pl.LpVariable: + def get_renewable_electricity_to_house(cls, time_slices: range) -> pl.LpVariable: """ Method that returns the renewable electricity to house variable. :param time_slices: @@ -27,7 +27,7 @@ def get_renewable_electricity_to_house(cls, time_slices: float) -> pl.LpVariable ) @classmethod - def get_renewable_electricity_to_battery(cls, time_slices: float) -> pl.LpVariable: + def get_renewable_electricity_to_battery(cls, time_slices: range) -> pl.LpVariable: """ Method that returns the renewable electricity to battery variable. :param time_slices: @@ -41,7 +41,7 @@ def get_renewable_electricity_to_battery(cls, time_slices: float) -> pl.LpVariab ) @classmethod - def get_total_renewable_generation(cls, time_slices: float) -> pl.LpVariable: + def get_total_renewable_generation(cls, time_slices: range) -> pl.LpVariable: """ Method that returns the total renewable generation variable. :param time_slices: diff --git a/optimiser/variables/fuel_cost_variables.py b/optimiser/variables/fuel_cost_variables.py new file mode 100644 index 0000000..bcaf71d --- /dev/null +++ b/optimiser/variables/fuel_cost_variables.py @@ -0,0 +1,78 @@ +""" +Module contains the definition of the fuel cost variables. +""" +# pylint: disable=import-error +import pulp as pl # type: ignore + + +class FuelCostVariables: + """ + Class contains the definition of the fuel cost variables. + """ + + @classmethod + def get_fuel_costs(cls, time_slices: range) -> pl.LpVariable: + """ + Method that returns the fuel costs variable. + :param time_slices: + :return: + """ + return pl.LpVariable.dicts( + name="fuel_costs_{t}", indices=time_slices, lowBound=0, cat="Continuous" + ) + + @classmethod + def get_battery_electricity_costs(cls, time_slices: range) -> pl.LpVariable: + """ + Method that returns the battery electricity costs variable. + :param time_slices: + :return: + """ + return pl.LpVariable.dicts( + name="battery_electricity_costs_{t}", + indices=time_slices, + lowBound=0, + cat="Continuous", + ) + + @classmethod + def get_grid_electricity_costs(cls, time_slices: range) -> pl.LpVariable: + """ + Method that returns the grid electricity costs variable. + :param time_slices: + :return: + """ + return pl.LpVariable.dicts( + name="grid_electricity_costs_{t}", + indices=time_slices, + lowBound=0, + cat="Continuous", + ) + + @classmethod + def get_renewable_electricity_costs(cls, time_slices: range) -> pl.LpVariable: + """ + Method that returns the renewable electricity costs variable. + :param time_slices: + :return: + """ + return pl.LpVariable.dicts( + name="renewable_electricity_costs_{t}", + indices=time_slices, + lowBound=0, + cat="Continuous", + ) + + @classmethod + def get_battery_to_grid_sales(cls, time_slices: range) -> pl.LpVariable: + """ + Method that returns the battery to grid sales variable. + :param time_slices: + :return: + """ + return pl.LpVariable.dicts( + name="battery_to_grid_sales_{t}", + indices=time_slices, + lowBound=0, + cat="Continuous", + ) From dbac0c0fe900a3c608fbd10ff9649d488c58338d Mon Sep 17 00:00:00 2001 From: davidwickh <56641696+davidwickh@users.noreply.github.com> Date: Thu, 26 Oct 2023 00:24:57 +0100 Subject: [PATCH 03/10] Adding battery_state_of_charge_variables.py --- .../battery_state_of_charge_variables.py | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 optimiser/variables/battery_state_of_charge_variables.py diff --git a/optimiser/variables/battery_state_of_charge_variables.py b/optimiser/variables/battery_state_of_charge_variables.py new file mode 100644 index 0000000..2a4da3b --- /dev/null +++ b/optimiser/variables/battery_state_of_charge_variables.py @@ -0,0 +1,46 @@ +""" +Module defines the variables used in the battery state of charge optimisation. +""" +# pylint: disable=import-error +import pulp as pl # type: ignore + +from optimiser.constants import OptimiserConstants + + +class BatteryStateOfChargeVariables: + """ + Class that defines the variables used in the battery state of charge optimisation. + """ + + @classmethod + def get_battery_state_of_charge_variables(cls, time_slices: range): + """ + Method that returns the battery state of charge variables. + :param time_slices: + :return: + """ + return { + time_slice: pl.LpVariable( + name=f"battery_state_of_charge_{time_slice}", + lowBound=0, + upBound=OptimiserConstants.BATTERY_CAPACITY, + cat=pl.LpContinuous, + ) + for time_slice in time_slices + } + + @classmethod + def get_battery_degradation_variables(cls, time_slices: range): + """ + Method that returns the battery degradation variables. + :param time_slices: + :return: + """ + return { + time_slice: pl.LpVariable( + name=f"battery_degradation_{time_slice}", + lowBound=0, + cat=pl.LpContinuous, + ) + for time_slice in time_slices + } From cb43eed50ab025ed797d7f0196ef96a017041ddd Mon Sep 17 00:00:00 2001 From: davidwickh <56641696+davidwickh@users.noreply.github.com> Date: Thu, 26 Oct 2023 00:37:16 +0100 Subject: [PATCH 04/10] Adding battery_state_of_charge_variables.py --- optimiser/variables/battery_state_of_charge_variables.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/optimiser/variables/battery_state_of_charge_variables.py b/optimiser/variables/battery_state_of_charge_variables.py index 2a4da3b..b677c59 100644 --- a/optimiser/variables/battery_state_of_charge_variables.py +++ b/optimiser/variables/battery_state_of_charge_variables.py @@ -13,7 +13,7 @@ class BatteryStateOfChargeVariables: """ @classmethod - def get_battery_state_of_charge_variables(cls, time_slices: range): + def get_battery_state_of_charge(cls, time_slices: range): """ Method that returns the battery state of charge variables. :param time_slices: @@ -30,7 +30,7 @@ def get_battery_state_of_charge_variables(cls, time_slices: range): } @classmethod - def get_battery_degradation_variables(cls, time_slices: range): + def get_battery_degradation(cls, time_slices: range): """ Method that returns the battery degradation variables. :param time_slices: From 6eb88dccf2bb80e54a5e9e980283b8b4fcaee451 Mon Sep 17 00:00:00 2001 From: davidwickh <56641696+davidwickh@users.noreply.github.com> Date: Thu, 26 Oct 2023 00:40:06 +0100 Subject: [PATCH 05/10] Adding main entry point for optimiser and utils --- optimiser/optimisation_problem.py | 435 +++++++++++++++--------------- optimiser/run.py | 17 ++ optimiser/utils.py | 17 ++ 3 files changed, 245 insertions(+), 224 deletions(-) create mode 100644 optimiser/run.py create mode 100644 optimiser/utils.py diff --git a/optimiser/optimisation_problem.py b/optimiser/optimisation_problem.py index ab80b94..55616ab 100644 --- a/optimiser/optimisation_problem.py +++ b/optimiser/optimisation_problem.py @@ -1,259 +1,246 @@ """ Main file for the optimisation problem """ +import logging +from dataclasses import dataclass, field + # pylint: disable=import-error, too-many-locals, too-many-statements, consider-using-generator, unused-variable import pulp as pl # type: ignore from optimiser.constants import OptimiserConstants +from optimiser.variables.battery_state_of_charge_variables import ( + BatteryStateOfChargeVariables, +) +from optimiser.variables.electricity_flow_variables import ( + RenewableElectricityFlowVariables, + BatteryElectricityFlowVariables, + GridElectricityFlowVariables, +) +from optimiser.variables.fuel_cost_variables import FuelCostVariables +logger = logging.getLogger(__name__) -def main(): + +@dataclass +class Optimiser: """ - Main function - :return: + Class that builds and runs the optimisation problem. """ - # Define the problem - problem = pl.LpProblem("BatteryOptimisation", pl.LpMinimize) - - # Time slices - time_slices = range(1, 25) - - demand = {} - for _t in time_slices: - demand[_t] = 100 - - # ---- Define variables ---- - # Renewable electricity flow - renewable_electricity_to_house = pl.LpVariable.dicts( - name="renewable_electricity_to_house_{t}", - indices=time_slices, - lowBound=0, - cat="Continuous", - ) - renewable_electricity_to_battery = pl.LpVariable.dicts( - name="renewable_electricity_to_battery_{t}", - indices=time_slices, - lowBound=0, - cat="Continuous", - ) - total_renewable_generation = pl.LpVariable.dicts( - name="total_renewable_generation_{t}", - indices=time_slices, - lowBound=0, - cat="Continuous", - ) - - # Battery electricity flow - battery_electricity_to_house = pl.LpVariable.dicts( - name="battery_electricity_to_house_{time_slices}", - indices=time_slices, - lowBound=0, - cat="Continuous", - ) - battery_electricity_to_grid = pl.LpVariable.dicts( - name="battery_electricity_to_grid_{t}", - indices=time_slices, - lowBound=0, - cat="Continuous", - ) - electricity_to_battery = pl.LpVariable.dicts( - name="electricity_to_battery_{t}", - indices=time_slices, - lowBound=0, - cat="Continuous", - ) - - # Grid electricity flow - grid_electricity_to_house = pl.LpVariable.dicts( - name="grid_electricity_to_house_{t}", - indices=time_slices, - lowBound=0, - cat="Continuous", - ) - grid_electricity_to_battery = pl.LpVariable.dicts( - name="grid_electricity_to_battery_{t}", - indices=time_slices, - lowBound=0, - cat="Continuous", - ) - - # Fuel Costs - fuel_costs = pl.LpVariable.dicts( - name="fuel_costs_{t}", indices=time_slices, lowBound=0, cat="Continuous" - ) - battery_electricity_costs = pl.LpVariable.dicts( - name="battery_electricity_costs_{t}", - indices=time_slices, - lowBound=0, - cat="Continuous", - ) - grid_electricity_costs = pl.LpVariable.dicts( - name="grid_electricity_costs_{t}", - indices=time_slices, - lowBound=0, - cat="Continuous", - ) - renewable_eletricity_costs = pl.LpVariable.dicts( - name="renewable_eletricity_costs_{t}", - indices=time_slices, - lowBound=0, - cat="Continuous", - ) - battery_to_grid_sales = pl.LpVariable.dicts( - name="battery_to_grid_sales_{t}", - indices=time_slices, - lowBound=0, - cat="Continuous", - ) - # Battery state of charge - battery_state_of_charge = pl.LpVariable.dicts( - name="battery_state_of_charge_{t}", - indices=time_slices, - lowBound=0, - cat="Continuous", - ) - battery_degradation = pl.LpVariable.dicts( - name="battery_degradation_{t}", - indices=time_slices, - lowBound=0, - cat="Continuous", - ) - - # Define objective function - problem += ( - pl.lpSum(battery_electricity_costs) - + pl.lpSum(grid_electricity_costs) - + pl.lpSum(renewable_eletricity_costs) - ), "Objective - minimise fuel costs" - - # Define constraints - for _t in time_slices: - # Electricity flow constraints - problem += ( - renewable_electricity_to_house[_t] - + battery_electricity_to_house[_t] - + grid_electricity_to_house[_t] - ) - demand[_t] == 0, f"Demand {_t}" - - problem += ( - renewable_electricity_to_battery[_t] - + grid_electricity_to_battery[_t] - - battery_electricity_to_grid[_t] - ) - electricity_to_battery[_t] == 0, f"Electricity flow constraint {_t}" - - # Renewable production constraints - problem += ( - renewable_electricity_to_house[_t] + renewable_electricity_to_battery[_t] - ) - total_renewable_generation[_t] == 0, f"Total renewable generation {_t}" - problem += ( - (total_renewable_generation[_t]) - <= OptimiserConstants.max_renewable_generation, + problem: pl.LpProblem = field( + default_factory=lambda: pl.LpProblem("BatteryOptimisation", pl.LpMinimize) + ) + time_slices: range = field(default_factory=lambda: range(1, 25)) + demand = {t: 1 for t in range(1, 25)} + + @classmethod + def build_optimiser(cls): + """ + Method that servers to build the optimiser. At a high level this method: + - Creates the optimisation variables + - Creates the objective function + - Creates the constraints + :return: + """ + logger.info("Building optimiser") + cls._define_objective_function() + for _t in cls.time_slices: + cls._define_electricity_flow_constraints(_t) + cls._define_renewable_production_constraints(_t) + cls._define_battery_constraints(_t) + cls._define_financial_constraints(_t) + cls.problem.writeLP("BatteryOptimisation.lp") + logger.info("Finished building optimiser") + + @classmethod + def _define_objective_function(cls): + """ + Method that defines the objective function. + :return: + """ + cls.problem += ( + pl.lpSum(FuelCostVariables.get_battery_electricity_costs(cls.time_slices)) + + pl.lpSum(FuelCostVariables.get_grid_electricity_costs(cls.time_slices)) + + pl.lpSum( + FuelCostVariables.get_renewable_electricity_costs(cls.time_slices) + ) + ), "Objective - minimise fuel costs" + + @classmethod + def _define_electricity_flow_constraints(cls, _t): + """ + Method that defines the electricity flow constraints. + :param _t: + :return: + """ + cls.problem += ( + RenewableElectricityFlowVariables.get_renewable_electricity_to_house(_t) + + BatteryElectricityFlowVariables.get_battery_electricity_to_house(_t) + + GridElectricityFlowVariables.get_grid_electricity_to_house(_t) + ) - cls.demand[_t] == 0, f"Demand {_t}" + + cls.problem += ( + RenewableElectricityFlowVariables.get_renewable_electricity_to_battery(_t) + + GridElectricityFlowVariables.get_grid_electricity_to_battery(_t) + - BatteryElectricityFlowVariables.get_battery_electricity_to_grid(_t) + ) - BatteryElectricityFlowVariables.get_electricity_to_battery( + _t + ) == 0, f"Electricity flow constraint {_t}" + + @classmethod + def _define_renewable_production_constraints(cls, _t): + """ + Method that defines the renewable production constraints. + :param _t: + :return: + """ + cls.problem += ( + RenewableElectricityFlowVariables.get_renewable_electricity_to_house(_t) + + RenewableElectricityFlowVariables.get_renewable_electricity_to_battery(_t) + ) - RenewableElectricityFlowVariables.get_total_renewable_generation( + _t + ) == 0, f"Total renewable generation {_t}" + + cls.problem += ( + RenewableElectricityFlowVariables.get_total_renewable_generation(_t) + <= OptimiserConstants.MAX_RENEWABLE_GENERATION, f"Maximum renewable generation {_t}", ) - # Battery constraints - # At the start of the model the battery is empty + @classmethod + def _define_battery_constraints(cls, _t): + """ + Method that defines the battery constraints. + :param _t: + :return: + """ if _t == 1: - problem += battery_state_of_charge[_t] == 0, f"Battery state of charge {_t}" + cls.problem += ( + BatteryStateOfChargeVariables.get_battery_state_of_charge(_t) == 0, + f"Battery state of charge {_t}", + ) else: - problem += ( - battery_state_of_charge[_t - 1] - + electricity_to_battery[_t] - - battery_electricity_to_house[_t] - - battery_electricity_to_grid[_t] - - battery_degradation[_t] - ) - battery_state_of_charge[_t] == 0, f"Battery state of charge {_t}" - - # Rate of electricity leaving the battery cannot exceed the maximum discharge rate - problem += ( - (battery_electricity_to_house[_t] + battery_electricity_to_grid[_t]) - <= OptimiserConstants.max_battery_discharge_rate - * OptimiserConstants.timeslice_size, + cls.problem += ( + BatteryStateOfChargeVariables.get_battery_state_of_charge(_t - 1) + + BatteryElectricityFlowVariables.get_electricity_to_battery(_t) + - BatteryElectricityFlowVariables.get_battery_electricity_to_house(_t) + - BatteryElectricityFlowVariables.get_battery_electricity_to_grid(_t) + - BatteryStateOfChargeVariables.get_battery_degradation(_t) + ) - BatteryStateOfChargeVariables.get_battery_state_of_charge( + _t + ) == 0, f"Battery state of charge {_t}" + + cls.problem += ( + ( + BatteryElectricityFlowVariables.get_battery_electricity_to_house(_t) + + BatteryElectricityFlowVariables.get_battery_electricity_to_grid(_t) + ) + <= OptimiserConstants.MAX_BATTERY_DISCHARGE_RATE + * OptimiserConstants.TIMESLICE_SIZE, f"Battery discharge rate {_t}", ) - # Rate of electricity entering the battery cannot exceed the maximum charge rate - problem += ( - electricity_to_battery[_t] - <= OptimiserConstants.max_battery_charge_rate - * OptimiserConstants.timeslice_size, + cls.problem += ( + BatteryElectricityFlowVariables.get_electricity_to_battery(_t) + <= OptimiserConstants.MAX_BATTERY_CHARGE_RATE + * OptimiserConstants.TIMESLICE_SIZE, f"Battery charge rate {_t}", ) - # The battery cannot exceed its maximum capacity - problem += ( - battery_state_of_charge[_t] <= OptimiserConstants.max_battery_cap, + cls.problem += ( + BatteryStateOfChargeVariables.get_battery_state_of_charge(_t) + <= OptimiserConstants.MAX_BATTERY_CAP, f"Battery capacity {_t}", ) - # The battery cannot be less than 0 - problem += ( - battery_state_of_charge[_t] >= 0, + cls.problem += ( + BatteryStateOfChargeVariables.get_battery_state_of_charge(_t) >= 0, f"Battery minimum state of charge {_t}", ) - # Financial constraints - problem += ( - (grid_electricity_to_house[_t] + grid_electricity_to_battery[_t]) + @classmethod + def _define_financial_constraints(cls, _t): + """ + Method that defines the financial constraints. + :param _t: + :return: + """ + cls.problem += ( + ( + GridElectricityFlowVariables.get_grid_electricity_to_house(_t) + + GridElectricityFlowVariables.get_grid_electricity_to_battery(_t) + ) * OptimiserConstants.unit_price_grid_electricity - ) - grid_electricity_costs[_t] == 0, f"Grid electricity costs {_t}" - problem += ( - battery_electricity_to_house[_t] + ) - FuelCostVariables.get_grid_electricity_costs( + _t + ) == 0, f"Grid electricity costs {_t}" + + cls.problem += ( + BatteryElectricityFlowVariables.get_battery_electricity_to_house(_t) * OptimiserConstants.unit_price_battery_electricity - ) - battery_electricity_costs[_t] == 0, f"Battery electricity costs {_t}" - problem += ( - renewable_electricity_to_house[_t] - * OptimiserConstants.unit_price_renewable_electricity - ) - renewable_eletricity_costs[_t] == 0, f"Renewable electricity costs {_t}" - problem += ( - battery_to_grid_sales[_t] * OptimiserConstants.unit_price_battery_sale_price - ) - battery_to_grid_sales[_t] == 0, f"Battery to grid sales {_t}" + ) - FuelCostVariables.get_battery_electricity_costs( + _t + ) == 0, f"Battery electricity costs {_t}" - problem.writeLP("BatteryOptimisation.lp") - problem.solve() - print(pl.LpStatus[problem.status]) + cls.problem += ( + RenewableElectricityFlowVariables.get_renewable_electricity_to_house(_t) + * OptimiserConstants.unit_price_renewable_electricity + ) - FuelCostVariables.get_renewable_electricity_costs( + _t + ) == 0, f"Renewable electricity costs {_t}" - print("---- Costs ----") - print("Total Cost = £", pl.value(problem.objective)) - print( - "Total Battery Costs = ", - sum([pl.value(battery_electricity_costs[t]) for t in time_slices]), - ) - print( - "Total Grid Costs = ", - sum([pl.value(grid_electricity_costs[t]) for t in time_slices]), - ) - print( - "Total Renewable Costs = ", - sum([pl.value(renewable_eletricity_costs[t]) for t in time_slices]), - ) + cls.problem += ( + BatteryElectricityFlowVariables.get_battery_electricity_to_grid(_t) + * OptimiserConstants.unit_price_battery_sale_price + ) - FuelCostVariables.get_battery_to_grid_sales( + _t + ) == 0, f"Battery to grid sales {_t}" - print("---- Energy Flows ----") - print( - f"Total Electricity Sent from Battery to Grid = " - f"{sum([pl.value(battery_to_grid_sales[t]) for t in time_slices])} kWh" - ) - print( - f"Total Renewable Generation = " - f"{sum([pl.value(total_renewable_generation[t]) for t in time_slices])} kWh" - ) - print( - f"Total Electricity Sent from Renewables to House = " - f"{sum([pl.value(renewable_electricity_to_house[t]) for t in time_slices])} kWh" - ) - print( - f"Total Electricity Sent from Renewables to Battery = " - f"{sum([pl.value(renewable_electricity_to_battery[t]) for t in time_slices])} kWh" - ) - print( - f"Total Electricity Sent from Battery to House = " - f"{sum([pl.value(battery_electricity_to_house[t]) for t in time_slices])} kWh" - ) - print( - f"Total Electricity Sent from Grid to House = " - f"{sum([pl.value(grid_electricity_to_house[t]) for t in time_slices])} kWh" - ) +def main(): + """ + Main function + :return: + """ -if __name__ == "__main__": - main() + # print(pl.LpStatus[problem.status]) + # + # print("---- Costs ----") + # print("Total Cost = £", pl.value(problem.objective)) + # print( + # "Total Battery Costs = ", + # sum([pl.value(battery_electricity_costs[t]) for t in time_slices]), + # ) + # print( + # "Total Grid Costs = ", + # sum([pl.value(grid_electricity_costs[t]) for t in time_slices]), + # ) + # print( + # "Total Renewable Costs = ", + # sum([pl.value(renewable_eletricity_costs[t]) for t in time_slices]), + # ) + # + # print("---- Energy Flows ----") + # print( + # f"Total Electricity Sent from Battery to Grid = " + # f"{sum([pl.value(battery_to_grid_sales[t]) for t in time_slices])} kWh" + # ) + # print( + # f"Total Renewable Generation = " + # f"{sum([pl.value(total_renewable_generation[t]) for t in time_slices])} kWh" + # ) + # print( + # f"Total Electricity Sent from Renewables to House = " + # f"{sum([pl.value(renewable_electricity_to_house[t]) for t in time_slices])} kWh" + # ) + # print( + # f"Total Electricity Sent from Renewables to Battery = " + # f"{sum([pl.value(renewable_electricity_to_battery[t]) for t in time_slices])} kWh" + # ) + # print( + # f"Total Electricity Sent from Battery to House = " + # f"{sum([pl.value(battery_electricity_to_house[t]) for t in time_slices])} kWh" + # ) + # print( + # f"Total Electricity Sent from Grid to House = " + # f"{sum([pl.value(grid_electricity_to_house[t]) for t in time_slices])} kWh" + # ) diff --git a/optimiser/run.py b/optimiser/run.py new file mode 100644 index 0000000..8d35320 --- /dev/null +++ b/optimiser/run.py @@ -0,0 +1,17 @@ +""" +Module serves as the entry point for running the optimiser. +""" + +from optimiser.utils import set_up_logging +from optimiser.optimisation_problem import Optimiser + + +def run_optimiser(): + """ + Function that runs the optimiser. + + Sets up logging and calls the + :return: + """ + set_up_logging() + Optimiser.build_optimiser() diff --git a/optimiser/utils.py b/optimiser/utils.py new file mode 100644 index 0000000..aa73b04 --- /dev/null +++ b/optimiser/utils.py @@ -0,0 +1,17 @@ +""" +Module for utility functions associated with the optimiser. +""" +import logging +import sys + + +def set_up_logging(): + """ + Function to set up logging. + :return: + """ + logging.basicConfig( + level=logging.INFO, + format="%(asctime)s %(levelname)s %(message)s", + handlers=[logging.StreamHandler(sys.stdout)], + ) From c4b23701e18edce84c837636ba4485fe1a87bed2 Mon Sep 17 00:00:00 2001 From: davidwickh <56641696+davidwickh@users.noreply.github.com> Date: Thu, 26 Oct 2023 00:49:16 +0100 Subject: [PATCH 06/10] Adding optimisation problem to README.md --- README.md | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/README.md b/README.md index 97c8568..9d313c9 100644 --- a/README.md +++ b/README.md @@ -1 +1,24 @@ # Home-Battery-Optimiser + +## Optimisation Model +The optimisation model is a linear program (LP) that minimises the cost of electricity consumption over +a given time horizon based on optimal dispatch of a battery. The model is formulated as follows: + +$$ \begin{aligned} & \underset{P_{b,t}^{+}, P_{b,t}^{-}, P_{g,t}, P_{l,t}}{\text{minimise}} +& & \sum_{t=1}^{T} \left( c_{b,t}^{+} P_{b,t}^{+} + c_{b,t}^{-} P_{b,t}^{-} + c_{g,t} P_{g,t} + c_{l,t} P_{l,t} \right) \\ +& \text{subject to} +& & P_{b,t}^{+} - P_{b,t}^{-} = P_{g,t} - P_{l,t} & \forall t \in \{1, \dots, T\} \\ +& & & P_{b,t}^{+} \leq P_{b,t}^{+,\text{max}} & \forall t \in \{1, \dots, T\} \\ +& & & P_{b,t}^{-} \leq P_{b,t}^{-,\text{max}} & \forall t \in \{1, \dots, T\} \\ +& & & P_{g,t} \leq P_{g,t}^{\text{max}} & \forall t \in \{1, \dots, T\} \\ +& & & P_{l,t} \leq P_{l,t}^{\text{max}} & \forall t \in \{1, \dots, T\} \\ +& & & P_{b,t}^{+}, P_{b,t}^{-}, P_{g,t}, P_{l,t} \geq 0 & \forall t \in \{1, \dots, T\} +\end{aligned}$$ + +where $P_{b,t}^{+}$ and $P_{b,t}^{-}$ are the charging and discharging power of the battery at time $t$, +$P_{g,t}$ and $P_{l,t}$ are the power consumption from the grid and the power consumption from the load at time $t$, +$c_{b,t}^{+}$ and $c_{b,t}^{-}$ are the charging and discharging cost of the battery at time $t$, +$c_{g,t}$ and $c_{l,t}$ are the cost of electricity from the grid and the cost of electricity from the load at time $t$, +$P_{b,t}^{+,\text{max}}$ and $P_{b,t}^{-,\text{max}}$ are the maximum charging and discharging power of the battery at time $t$, +$P_{g,t}^{\text{max}}$ and $P_{l,t}^{\text{max}}$ are the maximum power consumption from the grid and the maximum power consumption from the load at time $t$, +and $T$ is the number of time steps in the time horizon. From bc9b3c0bc58675493c4e6c75baab7f31c0830894 Mon Sep 17 00:00:00 2001 From: davidwickh <56641696+davidwickh@users.noreply.github.com> Date: Thu, 26 Oct 2023 00:50:17 +0100 Subject: [PATCH 07/10] Adding optimisation problem to README.md --- README.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 9d313c9..d9f876d 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,9 @@ The optimisation model is a linear program (LP) that minimises the cost of electricity consumption over a given time horizon based on optimal dispatch of a battery. The model is formulated as follows: -$$ \begin{aligned} & \underset{P_{b,t}^{+}, P_{b,t}^{-}, P_{g,t}, P_{l,t}}{\text{minimise}} +$$ +\begin{aligned} +& \underset{P_{b,t}^{+}, P_{b,t}^{-}, P_{g,t}, P_{l,t}}{\text{minimise}} & & \sum_{t=1}^{T} \left( c_{b,t}^{+} P_{b,t}^{+} + c_{b,t}^{-} P_{b,t}^{-} + c_{g,t} P_{g,t} + c_{l,t} P_{l,t} \right) \\ & \text{subject to} & & P_{b,t}^{+} - P_{b,t}^{-} = P_{g,t} - P_{l,t} & \forall t \in \{1, \dots, T\} \\ @@ -13,7 +15,8 @@ $$ \begin{aligned} & \underset{P_{b,t}^{+}, P_{b,t}^{-}, P_{g,t}, P_{l,t}}{\text & & & P_{g,t} \leq P_{g,t}^{\text{max}} & \forall t \in \{1, \dots, T\} \\ & & & P_{l,t} \leq P_{l,t}^{\text{max}} & \forall t \in \{1, \dots, T\} \\ & & & P_{b,t}^{+}, P_{b,t}^{-}, P_{g,t}, P_{l,t} \geq 0 & \forall t \in \{1, \dots, T\} -\end{aligned}$$ +\end{aligned} +$$ where $P_{b,t}^{+}$ and $P_{b,t}^{-}$ are the charging and discharging power of the battery at time $t$, $P_{g,t}$ and $P_{l,t}$ are the power consumption from the grid and the power consumption from the load at time $t$, From e378f66276b42240a4bf2681816930265538798a Mon Sep 17 00:00:00 2001 From: davidwickh <56641696+davidwickh@users.noreply.github.com> Date: Thu, 26 Oct 2023 00:59:53 +0100 Subject: [PATCH 08/10] Adding optimisation problem to README.md --- README.md | 24 +++++++++--------------- optimiser/optimisation_problem.py | 1 + 2 files changed, 10 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index d9f876d..4d98300 100644 --- a/README.md +++ b/README.md @@ -4,24 +4,18 @@ The optimisation model is a linear program (LP) that minimises the cost of electricity consumption over a given time horizon based on optimal dispatch of a battery. The model is formulated as follows: + +[//]: # (read the optimisation problem) $$ \begin{aligned} -& \underset{P_{b,t}^{+}, P_{b,t}^{-}, P_{g,t}, P_{l,t}}{\text{minimise}} -& & \sum_{t=1}^{T} \left( c_{b,t}^{+} P_{b,t}^{+} + c_{b,t}^{-} P_{b,t}^{-} + c_{g,t} P_{g,t} + c_{l,t} P_{l,t} \right) \\ +& \underset{Cost_{t}^{battery} + Cost_{t}^{grid} + Cost_{t}^{renewable} - Sales_{t}^{grid}}{\text{minimise}} +& & \sum_{t=1}^{T} \left(Cost_{t}^{battery} + Cost_{t}^{grid} + Cost_{t}^{renewable} - Sales_{t}^{grid} \right) \\ & \text{subject to} -& & P_{b,t}^{+} - P_{b,t}^{-} = P_{g,t} - P_{l,t} & \forall t \in \{1, \dots, T\} \\ -& & & P_{b,t}^{+} \leq P_{b,t}^{+,\text{max}} & \forall t \in \{1, \dots, T\} \\ -& & & P_{b,t}^{-} \leq P_{b,t}^{-,\text{max}} & \forall t \in \{1, \dots, T\} \\ -& & & P_{g,t} \leq P_{g,t}^{\text{max}} & \forall t \in \{1, \dots, T\} \\ -& & & P_{l,t} \leq P_{l,t}^{\text{max}} & \forall t \in \{1, \dots, T\} \\ -& & & P_{b,t}^{+}, P_{b,t}^{-}, P_{g,t}, P_{l,t} \geq 0 & \forall t \in \{1, \dots, T\} +& & & P_{t}^{renewable_to_house} + P_{t}^{battery_to_house} + P_{t}^{grid_to_house} - D_{t}^{house} = 0 & \forall t \in \{1, \dots, T\} \\ +& & & P_{t}^{renewable_to_battery} + P_{t}^{grid_to_battery} - P_{t}^{battery_to_grid} = 0 & \forall t \in \{1, \dots, T\} \\ \end{aligned} $$ -where $P_{b,t}^{+}$ and $P_{b,t}^{-}$ are the charging and discharging power of the battery at time $t$, -$P_{g,t}$ and $P_{l,t}$ are the power consumption from the grid and the power consumption from the load at time $t$, -$c_{b,t}^{+}$ and $c_{b,t}^{-}$ are the charging and discharging cost of the battery at time $t$, -$c_{g,t}$ and $c_{l,t}$ are the cost of electricity from the grid and the cost of electricity from the load at time $t$, -$P_{b,t}^{+,\text{max}}$ and $P_{b,t}^{-,\text{max}}$ are the maximum charging and discharging power of the battery at time $t$, -$P_{g,t}^{\text{max}}$ and $P_{l,t}^{\text{max}}$ are the maximum power consumption from the grid and the maximum power consumption from the load at time $t$, -and $T$ is the number of time steps in the time horizon. +Where $Cost_{t}^{battery}$ is the unit cost of electricity from the battery, $Cost_{t}^{grid}$ is the unit cost of +electricity from the grid, $Cost_{t}^{renewable}$ is the unit cost of electricity from renewable sources, +$Sales_{t}^{grid}$ is the unit revenue from selling electricity to the grid, diff --git a/optimiser/optimisation_problem.py b/optimiser/optimisation_problem.py index 55616ab..a9a2189 100644 --- a/optimiser/optimisation_problem.py +++ b/optimiser/optimisation_problem.py @@ -63,6 +63,7 @@ def _define_objective_function(cls): + pl.lpSum( FuelCostVariables.get_renewable_electricity_costs(cls.time_slices) ) + - pl.lpSum(FuelCostVariables.get_battery_to_grid_sales(cls.time_slices)) ), "Objective - minimise fuel costs" @classmethod From 78e15ff103aae8b5bd385b9dffd38d27512246d5 Mon Sep 17 00:00:00 2001 From: davidwickh <56641696+davidwickh@users.noreply.github.com> Date: Thu, 26 Oct 2023 01:01:41 +0100 Subject: [PATCH 09/10] Adding optimisation problem to README.md --- README.md | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 4d98300..532b2e6 100644 --- a/README.md +++ b/README.md @@ -11,10 +11,19 @@ $$ & \underset{Cost_{t}^{battery} + Cost_{t}^{grid} + Cost_{t}^{renewable} - Sales_{t}^{grid}}{\text{minimise}} & & \sum_{t=1}^{T} \left(Cost_{t}^{battery} + Cost_{t}^{grid} + Cost_{t}^{renewable} - Sales_{t}^{grid} \right) \\ & \text{subject to} -& & & P_{t}^{renewable_to_house} + P_{t}^{battery_to_house} + P_{t}^{grid_to_house} - D_{t}^{house} = 0 & \forall t \in \{1, \dots, T\} \\ -& & & P_{t}^{renewable_to_battery} + P_{t}^{grid_to_battery} - P_{t}^{battery_to_grid} = 0 & \forall t \in \{1, \dots, T\} \\ +& & E_{t}^{battery} = E_{t-1}^{battery} + \eta_{t}^{charge} P_{t}^{charge} - \frac{1}{\eta_{t}^{discharge}} P_{t}^{discharge} \\ +& & & E_{t}^{battery} \leq E_{t}^{battery, max} \\ +& & & E_{t}^{battery} \geq E_{t}^{battery, min} \\ +& & & P_{t}^{charge} \leq P_{t}^{charge, max} \\ +& & & P_{t}^{discharge} \leq P_{t}^{discharge, max} \\ +& & & P_{t}^{charge} \geq P_{t}^{charge, min} \\ +& & & P_{t}^{discharge} \geq P_{t}^{discharge, min} \\ +& & & \eta_{t}^{charge} \leq \eta_{t}^{charge, max} \\ +& & & \eta_{t}^{discharge} \leq \eta_{t}^{discharge, max} \\ +& & & \eta_{t}^{charge} \geq \eta_{t}^{charge, min} \\ +& & & \eta_{t}^{discharge} \geq \eta_{t}^{discharge, min} \\ +& & & \forall t \in \{1, \dots, T\} \\ \end{aligned} -$$ Where $Cost_{t}^{battery}$ is the unit cost of electricity from the battery, $Cost_{t}^{grid}$ is the unit cost of electricity from the grid, $Cost_{t}^{renewable}$ is the unit cost of electricity from renewable sources, From 3b83b99e46af04ad9630377b0013ca3a37eb2305 Mon Sep 17 00:00:00 2001 From: davidwickh <56641696+davidwickh@users.noreply.github.com> Date: Thu, 26 Oct 2023 01:02:25 +0100 Subject: [PATCH 10/10] Adding optimisation problem to README.md --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 532b2e6..f0f093f 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,7 @@ $$ & & & \eta_{t}^{discharge} \geq \eta_{t}^{discharge, min} \\ & & & \forall t \in \{1, \dots, T\} \\ \end{aligned} +$$ Where $Cost_{t}^{battery}$ is the unit cost of electricity from the battery, $Cost_{t}^{grid}$ is the unit cost of electricity from the grid, $Cost_{t}^{renewable}$ is the unit cost of electricity from renewable sources,