Refactor BatteryModel and market handling for improved time tracking, logging, and market rate calculations
Something is slow, I suspect its the logging, but time is tight, so I'm going to wrap up to make it at least useable
This commit is contained in:
@@ -0,0 +1,47 @@
|
|||||||
|
import argparse
|
||||||
|
import datetime
|
||||||
|
import sys
|
||||||
|
from copy import copy
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from batterymodel import BatteryModel
|
||||||
|
from batterymodel.models.market import MarketState, MarketDataIncrement
|
||||||
|
from batterymodel.models.battery import BatteryState
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser()
|
||||||
|
parser.add_argument("--market1", type=Path, help="Market state file")
|
||||||
|
parser.add_argument("--market2", type=Path, help="Market state file")
|
||||||
|
|
||||||
|
args = parser.parse_args()
|
||||||
|
market1 = MarketState().load_market_data_from_csv(args.market1)
|
||||||
|
market2 = MarketState().load_market_data_from_csv(args.market2)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
increment = datetime.timedelta(minutes=10)
|
||||||
|
market_1_start = market1.get_start_time()
|
||||||
|
market2_start = market2.get_start_time()
|
||||||
|
|
||||||
|
start_time = min(market_1_start, market2_start)
|
||||||
|
print(f"Start Time: {start_time}")
|
||||||
|
end_time = max(market1.get_end_time(), market2.get_end_time())
|
||||||
|
print(f"End Time: {end_time}")
|
||||||
|
|
||||||
|
battery = BatteryModel(start_time)
|
||||||
|
|
||||||
|
current_time = copy(start_time)
|
||||||
|
while True:
|
||||||
|
this_market_info_1 = market1.get_market_rate_for_timestamp(current_time)
|
||||||
|
this_market_info_2 = market2.get_market_rate_for_timestamp(current_time)
|
||||||
|
if current_time > end_time:
|
||||||
|
sys.exit()
|
||||||
|
|
||||||
|
market_increment = MarketDataIncrement(current_time, [this_market_info_1, this_market_info_2])
|
||||||
|
battery.update(increment, market_increment)
|
||||||
|
current_time = current_time + increment
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
import argparse
|
|
||||||
import datetime
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
from .market import MarketState
|
|
||||||
from .battery import BatteryState
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
parser = argparse.ArgumentParser()
|
|
||||||
parser.add_argument("--market1", type=Path, help="Market state file")
|
|
||||||
parser.add_argument("--market2", type=Path, help="Market state file")
|
|
||||||
|
|
||||||
args = parser.parse_args()
|
|
||||||
market1 = MarketState().load_market_data_from_csv(args.market1)
|
|
||||||
market2 = MarketState().load_market_data_from_csv(args.market2)
|
|
||||||
|
|
||||||
battery = BatteryState()
|
|
||||||
|
|
||||||
increment = datetime.timedelta(minutes=10)
|
|
||||||
market_1_start = market1.get_start_time()
|
|
||||||
market2_start = market2.get_start_time()
|
|
||||||
start_time = min(market_1_start, market2_start)
|
|
||||||
|
|
||||||
for item in market_1_start:
|
|
||||||
print(item)
|
|
||||||
battery.update(increment, None, market1)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import datetime
|
import datetime
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
|
|
||||||
from batterymodel.models.market import MarketDataIncrement
|
from ..models.market import MarketDataIncrement
|
||||||
|
|
||||||
|
|
||||||
class BatteryState(Enum):
|
class BatteryState(Enum):
|
||||||
@@ -10,7 +10,7 @@ class BatteryState(Enum):
|
|||||||
DISCHARGING = 2
|
DISCHARGING = 2
|
||||||
|
|
||||||
class BatteryModel:
|
class BatteryModel:
|
||||||
def __init__(self):
|
def __init__(self, start_time: datetime.datetime):
|
||||||
self._max_charge_rate = 2000000 # Watts
|
self._max_charge_rate = 2000000 # Watts
|
||||||
self._max_discharge_rate = 2000000 # Watts
|
self._max_discharge_rate = 2000000 # Watts
|
||||||
self._max_storage_volume = 4000000 # Wh
|
self._max_storage_volume = 4000000 # Wh
|
||||||
@@ -22,29 +22,61 @@ class BatteryModel:
|
|||||||
self._fixed_operational_costs = 50000 # £/year
|
self._fixed_operational_costs = 50000 # £/year
|
||||||
self._runtime = datetime.timedelta(0)
|
self._runtime = datetime.timedelta(0)
|
||||||
self._charge_discharge_cycles = 0
|
self._charge_discharge_cycles = 0
|
||||||
self._decision_log_file_handle = open("battery_log.txt", "w+")
|
self._decision_log_file = "battery.log"
|
||||||
|
self._decision_log_handle = open(self._decision_log_file, "w+")
|
||||||
|
|
||||||
self._battery_state = BatteryState.IDLE
|
self._battery_state = BatteryState.IDLE
|
||||||
self._current_charge_rate = 0.0
|
self._current_charge_rate = 0.0
|
||||||
self._current_discharge_rate = 0.0
|
self._current_discharge_rate = 0.0
|
||||||
self._current_market_rate = 0.0
|
self._current_market_rate = ()
|
||||||
self._current_charge_value = 0.0
|
self._current_charge_value = 0.0
|
||||||
self._current_charge = 0.0
|
self._current_charge = 0.0
|
||||||
self._total_profit = 0.0
|
self._total_profit = 0.0
|
||||||
|
|
||||||
def update(self, increment: datetime.timedelta, previous_state: "BatteryModel", market_data_increment: MarketDataIncrement):
|
self.start_time = start_time
|
||||||
self.market_decision(increment, previous_state, market_data_increment)
|
self.current_time = start_time
|
||||||
|
|
||||||
|
def update(self, increment: datetime.timedelta, market_data_increment: MarketDataIncrement):
|
||||||
|
self.current_time += increment
|
||||||
|
self.market_decision(increment, market_data_increment)
|
||||||
|
if not self._battery_state == BatteryState.IDLE:
|
||||||
|
self._runtime += increment
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def market_decision(self, increment: datetime.timedelta, previous_state: "BatteryModel", market_data_increment: MarketDataIncrement):
|
def market_decision(self, increment: datetime.timedelta, market_data_increment: MarketDataIncrement):
|
||||||
|
self.log_state_change("market_decision", None, None)
|
||||||
if self._battery_state == BatteryState.IDLE and self._current_charge > 0:
|
if self._battery_state == BatteryState.IDLE and self._current_charge > 0:
|
||||||
# Sell, I guess, to whoever offers more
|
# Sell, I guess, to whoever offers more
|
||||||
for market_data_increment in market_data_increment.markets:
|
self._battery_state = BatteryState.DISCHARGING
|
||||||
self._battery_state = BatteryState.DISCHARGING
|
self._current_charge_rate = self._max_discharge_rate
|
||||||
self._current_charge_rate = self._max_discharge_rate
|
self._current_market_rate = market_data_increment.get_max_market_rate()
|
||||||
self._current_market_rate = market_data_increment.get_max_market_rate()
|
self._current_charge_value = self._current_charge_rate * self._current_charge
|
||||||
self._current_charge_value = self._current_charge_rate * self._current_charge
|
self.log_state_change("market_decision_battery", BatteryState.IDLE, self._battery_state)
|
||||||
return
|
elif self._battery_state == BatteryState.DISCHARGING and self._current_market_rate[0] < self.current_time:
|
||||||
|
# We've committed to discharging, so do figure out what we've earned this period, do the discharge calculation
|
||||||
|
# and continue
|
||||||
|
discharge_time = (increment.total_seconds() / 60.0)
|
||||||
|
self._total_profit = self._current_market_rate[1] * discharge_time
|
||||||
|
elif self._battery_state == BatteryState.DISCHARGING and self._current_market_rate[0] > self.current_time:
|
||||||
|
# We've finished discharging for this time period, calculate profit assuming we only discharged for this increment
|
||||||
|
# and remember to chop any time we wouldn't have been charging if our increments have overlapped.
|
||||||
|
discharge_time = (increment.total_seconds()/60.0) - ((self.current_time-self._current_market_rate[0])/60.0)
|
||||||
|
|
||||||
|
# Could we actually discharge for this time
|
||||||
|
remaining_capacity = self._current_charge - (self._max_discharge_rate * discharge_time)
|
||||||
|
if remaining_capacity > 0:
|
||||||
|
self._total_profit += (self._current_market_rate[1] * discharge_time)
|
||||||
|
else:
|
||||||
|
discharge_time = remaining_capacity / self._max_discharge_rate
|
||||||
|
self._total_profit += (self._current_market_rate[1] * discharge_time)
|
||||||
|
self._battery_state = BatteryState.IDLE
|
||||||
|
self._current_charge = self._max_discharge_rate * discharge_time
|
||||||
|
elif self._battery_state == BatteryState.IDLE and self._current_charge == 0.0:
|
||||||
|
# No charge at all, normally I'd assume some kind of settable boundary here so we don't buy power too high
|
||||||
|
# but I'll just assume we buy to make life easy
|
||||||
|
self._battery_state = BatteryState.CHARGING
|
||||||
|
selected_market = market_data_increment.get_min_market_rate()
|
||||||
|
self._current_market_rate = selected_market
|
||||||
|
|
||||||
|
|
||||||
def handle_charge_or_discharge(self, increment: datetime.timedelta, previous_state: "BatteryModel"):
|
def handle_charge_or_discharge(self, increment: datetime.timedelta, previous_state: "BatteryModel"):
|
||||||
@@ -53,9 +85,9 @@ class BatteryModel:
|
|||||||
self._current_charge = self._current_charge + total_charge_this_increment
|
self._current_charge = self._current_charge + total_charge_this_increment
|
||||||
|
|
||||||
# We filled the battery this increment
|
# We filled the battery this increment
|
||||||
if self._current_charge > self._battery_capacity:
|
if self._current_charge > self._max_storage_volume:
|
||||||
self._current_charge = self._battery_capacity
|
self._current_charge = self._max_storage_volume
|
||||||
total_charge_this_increment = self._battery_capacity - previous_state._current_charge
|
total_charge_this_increment = self._max_storage_volume - previous_state._current_charge
|
||||||
self._battery_state = BatteryState.IDLE
|
self._battery_state = BatteryState.IDLE
|
||||||
time_of_charge = datetime.timedelta(seconds=total_charge_this_increment / self._current_charge_rate)
|
time_of_charge = datetime.timedelta(seconds=total_charge_this_increment / self._current_charge_rate)
|
||||||
|
|
||||||
@@ -78,7 +110,7 @@ class BatteryModel:
|
|||||||
self._current_charge_value)
|
self._current_charge_value)
|
||||||
|
|
||||||
def log_state_change(self, fn, original_state, new_state):
|
def log_state_change(self, fn, original_state, new_state):
|
||||||
self._decision_log_file_handle.write(f"{self._runtime},{fn},{original_state},{new_state}\n")
|
self._decision_log_handle.write(f"{self.current_time},{self._runtime},{fn},{original_state},{new_state}\n")
|
||||||
|
|
||||||
def handle_storage_capacity_check(self, previous_state: "BatteryModel"):
|
def handle_storage_capacity_check(self, previous_state: "BatteryModel"):
|
||||||
if self._charge_discharge_cycles > previous_state._charge_discharge_cycles and self._charge_discharge_cycles / int(self._charge_discharge_cycles) == 1:
|
if self._charge_discharge_cycles > previous_state._charge_discharge_cycles and self._charge_discharge_cycles / int(self._charge_discharge_cycles) == 1:
|
||||||
|
|||||||
@@ -8,29 +8,41 @@ from xmlrpc.client import DateTime
|
|||||||
class MarketState:
|
class MarketState:
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self._market_data = OrderedDict()
|
self._market_data = OrderedDict()
|
||||||
|
self._last_key = None
|
||||||
|
|
||||||
def load_market_data_from_csv(self, csv_file):
|
def load_market_data_from_csv(self, csv_file):
|
||||||
with open(csv_file, 'r') as file:
|
with open(csv_file, 'r') as file:
|
||||||
|
|
||||||
reader = csv.reader(file)
|
reader = csv.reader(file)
|
||||||
|
next(reader, None) # skip the headers
|
||||||
for row in reader:
|
for row in reader:
|
||||||
self._market_data[datetime.datetime.strptime(row[0], '%d-%m-%Y %H:%M')] = float(row[1])
|
self._market_data[datetime.datetime.strptime(row[0], '%d/%m/%Y %H:%M')] = float(row[1])
|
||||||
|
self._last_key = datetime.datetime.strptime(row[0], '%d/%m/%Y %H:%M')
|
||||||
|
return self
|
||||||
|
|
||||||
def get_start_time(self):
|
def get_start_time(self):
|
||||||
return next(iter(self._market_data))
|
return next(iter(self._market_data.items()))[0]
|
||||||
|
|
||||||
|
def get_end_time(self):
|
||||||
|
end = None
|
||||||
|
for i in iter(self._market_data.items()):
|
||||||
|
end = i[0]
|
||||||
|
return end
|
||||||
|
|
||||||
def get_market_rate_for_timestamp(self, timestamp: datetime.datetime):
|
def get_market_rate_for_timestamp(self, timestamp: datetime.datetime):
|
||||||
last_timestamp = None
|
last_timestamp = None
|
||||||
|
if timestamp >= self._last_key:
|
||||||
|
return None
|
||||||
for key in self._market_data:
|
for key in self._market_data:
|
||||||
if last_timestamp is not None:
|
if last_timestamp is not None:
|
||||||
delta = timestamp - last_timestamp
|
delta = timestamp - last_timestamp
|
||||||
key_delta = key - last_timestamp
|
key_delta = key - last_timestamp
|
||||||
if key == timestamp:
|
if key == timestamp:
|
||||||
return self._market_data[key]
|
return timestamp, self._market_data[key]
|
||||||
elif last_timestamp is not None and last_timestamp < key and key_delta > delta:
|
elif last_timestamp is not None and last_timestamp < key and key_delta > delta:
|
||||||
|
return last_timestamp, self._market_data[last_timestamp]
|
||||||
return self._market_data[last_timestamp]
|
|
||||||
else:
|
else:
|
||||||
last_timestamp = key
|
last_timestamp = key
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
@@ -46,4 +58,13 @@ class MarketDataIncrement:
|
|||||||
if i[1] > rate:
|
if i[1] > rate:
|
||||||
rate = i[1]
|
rate = i[1]
|
||||||
highest = i
|
highest = i
|
||||||
return highest
|
return highest
|
||||||
|
|
||||||
|
def get_min_market_rate(self):
|
||||||
|
rate = 99999999999999
|
||||||
|
lowest = ()
|
||||||
|
for i in self.markets:
|
||||||
|
if i[1] < rate:
|
||||||
|
rate = i[1]
|
||||||
|
lowest = i
|
||||||
|
return lowest
|
||||||
@@ -1,5 +1,8 @@
|
|||||||
|
import datetime
|
||||||
|
import copy
|
||||||
from batterymodel import BatteryModel
|
from batterymodel import BatteryModel
|
||||||
from batterymodel.models.battery import BatteryState
|
from batterymodel.models.battery import BatteryState
|
||||||
|
from batterymodel.models.market import MarketDataIncrement
|
||||||
|
|
||||||
|
|
||||||
def test_battery_stats():
|
def test_battery_stats():
|
||||||
@@ -20,3 +23,24 @@ def test_battery_update():
|
|||||||
|
|
||||||
def test_market_decision_idle():
|
def test_market_decision_idle():
|
||||||
battery = BatteryModel()
|
battery = BatteryModel()
|
||||||
|
battery._current_charge=100000
|
||||||
|
time_increment = datetime.timedelta(minutes=30)
|
||||||
|
market_increment = MarketDataIncrement(
|
||||||
|
datetime.datetime.strptime('01-01-2022 00:00', '%d-%m-%Y %H:%M'),
|
||||||
|
[
|
||||||
|
(datetime.datetime.strptime('01-01-2022 00:00', '%d-%m-%Y %H:%M'),43.0),
|
||||||
|
(datetime.datetime.strptime('01-01-2022 00:00', '%d-%m-%Y %H:%M'),14.0)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
market_increment_current = MarketDataIncrement(
|
||||||
|
datetime.datetime.strptime('01-01-2022 00:30', '%d-%m-%Y %H:%M'),
|
||||||
|
[
|
||||||
|
(datetime.datetime.strptime('01-01-2022 00:00', '%d-%m-%Y %H:%M'),43.0),
|
||||||
|
(datetime.datetime.strptime('01-01-2022 00:30', '%d-%m-%Y %H:%M'),13.0)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
current_battery = copy.deepcopy(battery)
|
||||||
|
battery.market_decision(time_increment, current_battery, market_increment_current)
|
||||||
|
assert current_battery._battery_state == BatteryState.IDLE
|
||||||
|
assert battery._battery_state == BatteryState.DISCHARGING
|
||||||
@@ -10,7 +10,7 @@ def test_market():
|
|||||||
this_market._market_data[datetime.datetime.strptime('01-01-2022 00:02', '%d-%m-%Y %H:%M')] = 13
|
this_market._market_data[datetime.datetime.strptime('01-01-2022 00:02', '%d-%m-%Y %H:%M')] = 13
|
||||||
this_market._market_data[datetime.datetime.strptime('01-01-2022 00:05', '%d-%m-%Y %H:%M')] = 14
|
this_market._market_data[datetime.datetime.strptime('01-01-2022 00:05', '%d-%m-%Y %H:%M')] = 14
|
||||||
|
|
||||||
assert this_market.get_market_rate_for_timestamp(datetime.datetime.strptime('01-01-2022 00:02', '%d-%m-%Y %H:%M')) == 13
|
assert this_market.get_market_rate_for_timestamp(datetime.datetime.strptime('01-01-2022 00:02', '%d-%m-%Y %H:%M')) == (datetime.datetime.strptime('01-01-2022 00:02', '%d-%m-%Y %H:%M'),13)
|
||||||
assert this_market.get_market_rate_for_timestamp(datetime.datetime.strptime('01-01-2022 00:03', '%d-%m-%Y %H:%M')) == 13
|
assert this_market.get_market_rate_for_timestamp(datetime.datetime.strptime('01-01-2022 00:03', '%d-%m-%Y %H:%M')) == (datetime.datetime.strptime('01-01-2022 00:02', '%d-%m-%Y %H:%M'),13)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user