Skip to content

Commit

Permalink
Merge pull request #7 from thisistheplace/feature/test_heatpump
Browse files Browse the repository at this point in the history
Heat pump refactor and testing
  • Loading branch information
andrewlyden authored Jun 17, 2024
2 parents 9c25b75 + 80c4a1e commit 42b5cbe
Show file tree
Hide file tree
Showing 58 changed files with 1,739 additions and 2,019 deletions.
40 changes: 40 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
name: Tests

on:
pull_request:
branches:
- 'master'
- 'feature/*'
push:
branches:
- 'master'

jobs:
test:
runs-on: ubuntu-22.04

strategy:
matrix:
python-version: ["3.10"]

steps:
- uses: actions/checkout@v3
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}
- name: Install python environment
run: |
python -m pip install --upgrade pip
pip install flake8 pytest
if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
# - name: Lint with flake8
# run: |
# # stop the build if there are Python syntax errors or undefined names
# flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
# # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide
# flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
- name: Test with pytest
working-directory: .
run: |
python -m pytest --cov=pylesa --cov-report term-missing -svv
7 changes: 7 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"python.testing.pytestArgs": [
"."
],
"python.testing.unittestEnabled": false,
"python.testing.pytestEnabled": true
}
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
![Test status](https://github.com/thisistheplace/PyLESA/actions/workflows/test.yml/badge.svg?event=push)

# `PyLESA`
`PyLESA` stands for Python for Local Energy Systems Analysis and is pronounced "pai-lee-suh".

Expand Down Expand Up @@ -41,6 +43,16 @@ Running `python -m pylesa --help` will display the following help message:

A video discussing how to run `PyLESA` is available here: https://youtu.be/QsJut9ftCT4

## Testing
The `PyLESA` source code is tested using [pytest](https://docs.pytest.org/en/8.2.x/). The tests can be run locally by running the following command:

```python
source venv/bin/activate
python -m pytest -svv
# for test coverage reporting run:
python -m pytest --cov=pylesa -svv --cov-report term-missing
```

## References

PhD Thesis - Modelling and design of local energy systems incorporating heat pumps, thermal storage, future tariffs, and model predictive control (https://doi.org/10.48730/8nz5-xb46)
Expand Down
9 changes: 8 additions & 1 deletion pylesa/__main__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
import logging
import typer

from .main import main

LOG = logging.getLogger(__name__)

if __name__ == "__main__":
typer.run(main)
try:
typer.run(main)
except Exception as e:
LOG.error(e, exc_info=True)
raise e
3 changes: 2 additions & 1 deletion pylesa/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,5 @@
LOG_PATH = "pylesa.log"

INDIR = "inputs"
OUTDIR = "outputs"
OUTDIR = "outputs"
ANNUAL_HOURS = 8760
107 changes: 28 additions & 79 deletions pylesa/controllers/fixed_order.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@
from .. import initialise_classes
from ..io import inputs
from ..constants import OUTDIR
from ..heat.models import PerformanceValue
from ..heat.enums import Fuel

LOG = logging.getLogger(__name__)

Expand Down Expand Up @@ -164,59 +166,6 @@ def run_timesteps(self, first_hour, timesteps):
# stop progress bar
pbar.finish()

# ES_to_demand = []
# ES_to_HP_to_demand = []
# RES_to_ES = []
# import_for_ES = []
# soc = []

# # TSd = []
# IC = []
# surplus = []
# # hd = []
# export = []
# for i in range(timesteps):
# ES_to_demand.append(-1 * results[i]['ES']['discharging_to_demand'])
# ES_to_HP_to_demand.append(-1 * results[i]['ES']['discharging_to_HP'])
# RES_to_ES.append(results[i]['ES']['charging_from_RES'])
# import_for_ES.append(results[i]['ES']['charging_from_import'])
# soc.append(results[i]['ES']['final_soc'])
# # TSd.append(results[i]['TS']['discharging_total'])
# IC.append(results[i]['grid']['import_price'])
# surplus.append(results[i]['grid']['surplus'])
# # hd.append(results[i]['heat_demand']['heat_demand'])
# export.append(results[i]['grid']['total_export'])

# # Plot solution
# time = range(first_hour, final_hour)
# plt.figure()
# plt.subplot(4, 1, 1)
# plt.plot(time, ES_to_demand, 'r', linewidth=2)
# plt.plot(time, ES_to_HP_to_demand, 'y', linewidth=2)
# plt.plot(time, RES_to_ES, 'b', linewidth=2)
# plt.plot(time, import_for_ES, 'g', linewidth=2)
# plt.ylabel('ES charging/discharging')
# plt.legend(['ES_to_demand', 'ES_to_HP_to_demand', 'RES_to_ES', 'import_for_ES'], loc='best')
# plt.subplot(4, 1, 2)
# plt.plot(time, soc, 'r', linewidth=2)
# plt.legend(['SOC'], loc='best')
# plt.ylabel('SOC')
# plt.subplot(4, 1, 3)
# plt.plot(time, IC, 'g', linewidth=2)
# plt.legend(['Import cost'], loc='best')
# plt.ylabel('Import cost')
# plt.subplot(4, 1, 4)
# plt.plot(time, surplus, 'm', linewidth=2)
# plt.plot(time, export, 'b', linewidth=2)
# plt.legend(['surplus', 'export'], loc='best')
# plt.ylabel('Surplus, and export')
# # plt.plot(time, TSc, 'r', linewidth=2)
# # plt.plot(time, TSd, 'g', linewidth=2)
# # plt.legend(['TSc', 'TSd'], loc='best')
# # plt.ylabel('Charging/discharging')
# plt.xlabel('Time')
# plt.show()

# write the outputs to a pickle
file = self.root / OUTDIR / self.subname / 'outputs.pkl'
with open(file, 'wb') as output_file:
Expand All @@ -233,8 +182,8 @@ def above_setpoint(self, timestep, surplus, deficit, match,
results['grid']['deficit'] = deficit
results['grid']['match'] = surplus - deficit
results['grid']['import_price'] = self.import_price[timestep]
results['HP']['cop'] = hp_performance['cop']
results['HP']['duty'] = hp_performance['duty']
results['HP']['cop'] = hp_performance.cop
results['HP']['duty'] = hp_performance.duty

order_above_setpoint = self.fixed_order_info['order_above_setpoint']
key = self.process_key()['above']
Expand Down Expand Up @@ -305,7 +254,7 @@ def above_setpoint(self, timestep, surplus, deficit, match,
results['aux']['RES_to_demand'])
results['aux']['usage'] = self.myAux.fuel_usage(
results['aux']['demand'])
if self.myAux.fuel == 'Electric':
if self.myAux.fuel == Fuel.ELECTRIC:
aux_price = results['grid']['import_price']
density = 0.0
else:
Expand Down Expand Up @@ -362,7 +311,7 @@ def above_setpoint(self, timestep, surplus, deficit, match,
results['grid']['import_for_heat_pump_total'] = (
results['grid']['import_for_heat_pump_to_heat_demand'] +
results['grid']['import_for_heat_pump_to_TS'])
if self.myAux.fuel == 'Electric':
if self.myAux.fuel == Fuel.ELECTRIC:
results['grid']['total_import'] = (
results['grid']['import_for_elec_demand'] +
results['grid']['import_for_heat_pump_total'] +
Expand Down Expand Up @@ -395,7 +344,7 @@ def above_setpoint(self, timestep, surplus, deficit, match,
# IF NOT AVAILABLE CAP THEN DONT RUN HP
min_out = (self.myHeatPump.minimum_output *
self.myHeatPump.minimum_runtime / 60 /
100.0 * hp_performance['duty'])
100.0 * hp_performance.duty)
hp_usage = results['HP']['heat_total_output']

# need to change results if threshold not met
Expand Down Expand Up @@ -506,8 +455,8 @@ def below_setpoint(self, timestep, surplus, deficit, match,
results['grid']['match'] = surplus - deficit
results['grid']['import_price'] = self.import_price[timestep]
results['grid']['export_price'] = self.export_price
results['HP']['cop'] = hp_performance['cop']
results['HP']['duty'] = hp_performance['duty']
results['HP']['cop'] = hp_performance.cop
results['HP']['duty'] = hp_performance.duty

order_below_setpoint = self.fixed_order_info['order_below_setpoint']
key = self.process_key()['below']
Expand Down Expand Up @@ -569,7 +518,7 @@ def below_setpoint(self, timestep, surplus, deficit, match,
results['heat_demand']['aux'])
results['aux']['usage'] = self.myAux.fuel_usage(
results['aux']['demand'])
if self.myAux.fuel == 'Electric':
if self.myAux.fuel == Fuel.ELECTRIC:
aux_price = results['grid']['import_price']
density = 0.0
else:
Expand Down Expand Up @@ -633,7 +582,7 @@ def below_setpoint(self, timestep, surplus, deficit, match,
results['grid']['import_for_heat_pump_total'] = (
results['grid']['import_for_heat_pump_to_heat_demand'] +
results['grid']['import_for_heat_pump_to_TS'])
if self.myAux.fuel == 'Electric':
if self.myAux.fuel == Fuel.ELECTRIC:
results['grid']['total_import'] = (
results['grid']['import_for_elec_demand'] +
results['grid']['import_for_heat_pump_total'] +
Expand Down Expand Up @@ -670,7 +619,7 @@ def below_setpoint(self, timestep, surplus, deficit, match,
# IF NOT AVAILABLE CAP THEN DONT RUN HP
min_out = (self.myHeatPump.minimum_output *
self.myHeatPump.minimum_runtime / 60 /
100.0 * hp_performance['duty'])
100.0 * hp_performance.duty)
hp_usage = results['HP']['heat_total_output']

# need to change results if threshold not met
Expand Down Expand Up @@ -1102,28 +1051,28 @@ def import_to_demand(self, elec_unmet):

return import_elec_demand

def HP_RES_to_demand(self, hp_usage, RES_left, hp_performance, heat_unmet):
def HP_RES_to_demand(self, hp_usage, RES_left, hp_performance: PerformanceValue, heat_unmet):

# USE HEAT PUMP WITH SURPLUS TO MEET HEAT DEMAND

heat_pump_spare = (
hp_performance['duty'] -
hp_performance.duty -
hp_usage)

# hpr - heat pump renewable
hpr = self.myHeatPump.thermal_output(
RES_left, hp_performance, heat_unmet)
# thermal output from heat pump running on renewables
HPtrd = min(hpr['hp_demand'], heat_pump_spare)
HPtrd = min(hpr.demand, heat_pump_spare)
# elec usage
HPetd = self.myHeatPump.elec_usage(
HPtrd, hp_performance)['hp_elec']
HPtrd, hp_performance)

return {'h': HPtrd, 'e': HPetd}

def EAUX_RES_to_demand(self, aux_left, RES_left, heat_unmet):

if self.myAux.fuel == 'Electric':
if self.myAux.fuel == Fuel.ELECTRIC:

# use up surplus with electric heater if exisitng

Expand All @@ -1141,7 +1090,7 @@ def HP_import_to_demand(self, hp_usage, heat_unmet, hp_performance,
# TO MEET HEAT DEMAND

heat_pump_spare = (
hp_performance['duty'] -
hp_performance.duty -
hp_usage)

# min of dem unmet and spare hp capacity
Expand All @@ -1157,7 +1106,7 @@ def HP_import_to_demand(self, hp_usage, heat_unmet, hp_performance,

# elec usage
elec = self.myHeatPump.elec_usage(
heat_pump_import_demand, hp_performance)['hp_elec']
heat_pump_import_demand, hp_performance)

return {'h': heat_pump_import_demand, 'e': elec}

Expand Down Expand Up @@ -1189,14 +1138,14 @@ def ES_to_HP_to_demand(self, soc, hp_performance, heat_unmet, hp_usage):
es_discharging, hp_performance, heat_unmet)

heat_pump_spare = (
hp_performance['duty'] -
hp_performance.duty -
hp_usage)

# thermal output from heat pump running on renewables
HP_h_es = min(max_hp_ES['hp_demand'], heat_pump_spare)
HP_h_es = min(max_hp_ES.demand, heat_pump_spare)
# elec usage
HP_e_es = self.myHeatPump.elec_usage(
HP_h_es, hp_performance)['hp_elec']
HP_h_es, hp_performance)

return {'h': HP_h_es, 'e': HP_e_es}

Expand All @@ -1212,11 +1161,11 @@ def HP_RES_to_TS(self, RES_left, hp_performance, hp_usage,

# CHARGE THERMAL STORAGE WITH HEAT PUMP DRIVEN BY SURPLUS

duty = hp_performance['duty']
duty = hp_performance.duty
# capacity of heat pump leftover
hp_cap_left = duty - hp_usage
# max heat from heat pump running surplus
hp_max_RES = RES_left * hp_performance['cop']
hp_max_RES = RES_left * hp_performance.cop
hp_left = min(hp_cap_left, hp_max_RES)
# how much can be input into the hot water tank
state = 'charging'
Expand All @@ -1229,15 +1178,15 @@ def HP_RES_to_TS(self, RES_left, hp_performance, hp_usage,
HPtrs = min(hp_left, tank_charge_left)
# elec usage
elec = self.myHeatPump.elec_usage(
HPtrs, hp_performance)['hp_elec']
HPtrs, hp_performance)

return {'h': HPtrs, 'e': elec}

def EAUX_RES_to_TS(self, aux_left, RES_left, nodes_temp,
source_temp, flow_temp, timestep,
ts_discharge, ts_charge):

if self.myAux.fuel == 'Electric':
if self.myAux.fuel == Fuel.ELECTRIC:

# how much can be input into the hot water tank
state = 'charging'
Expand All @@ -1262,7 +1211,7 @@ def HP_import_to_TS(self, hp_performance, hp_usage, nodes_temp,
# spare heat pump capacity

heat_pump_spare = (
hp_performance['duty'] -
hp_performance.duty -
hp_usage)
# spare tank capacity
state = 'charging'
Expand All @@ -1277,7 +1226,7 @@ def HP_import_to_TS(self, hp_performance, hp_usage, nodes_temp,
ts_hp_charge_heat_imports = 0
# elec usage
elec = self.myHeatPump.elec_usage(
ts_hp_charge_heat_imports, hp_performance)['hp_elec']
ts_hp_charge_heat_imports, hp_performance)

return {'h': ts_hp_charge_heat_imports, 'e': elec}

Expand Down
Loading

0 comments on commit 42b5cbe

Please sign in to comment.