├── .gitattributes ├── .gitignore ├── LICENSE ├── README.md ├── docs └── simple-simple.jpg ├── setup.py ├── simplesimple ├── __init__.py └── building.py └── tests └── test_building.py /.gitattributes: -------------------------------------------------------------------------------- 1 | # Set default behaviour, in case users don't have core.autocrlf set. 2 | * text=auto 3 | 4 | *.py text 5 | *.idf text 6 | *.md text 7 | *.txt text 8 | *.csv text 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Python 2 | *.pyc 3 | build/ 4 | dist/ 5 | __pycache__ 6 | *.egg-info 7 | .cache/ 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016-2018 Tim Tröndle 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Simple Simple 2 | 3 | > Your dream house come true! 4 | 5 | A simple Building Energy Model written in Python. 6 | 7 | ## Conceptual Model 8 | 9 | The model is derived from the hourly dynamic model in ISO 13790. It has only one capacity and 10 | one resistance. 11 | 12 | Compared to the ISO 13790 there is 13 | 14 | * no internal heat gain, 15 | * full shading of the building, no direct or indirect sun light, 16 | * no windows or doors, 17 | * no ventilation, 18 | * immediate heat transfer between air and surface. 19 | 20 | Simple simple model 21 | 22 | θm,t = θm,t-1 × (1 - Δt / Cm × Htr, em) + Δt / Cm × (ΦHC, nd, t-1 + Htr, em × θe, t-1) 23 | 24 | ### Output Variables 25 | 26 | * ΦHC, nd, t: cooling or heating power at time t 27 | 28 | ### State Variables 29 | 30 | * θm, t: building temperature [℃] at time t 31 | 32 | ### Parameters 33 | 34 | * θe, t: outside temperature [℃] at time t 35 | * Af: conditioned floor area [m2] 36 | * Cm: capacity of the building's heat mass [J/K] 37 | * Δt: time step size [s] 38 | * Htr, em: heat transmission to the outside [W/K] 39 | * θint, C, set: cooling set point temperature [℃] 40 | * θint, H, set: heating set point temperature [℃] 41 | * ΦC, max: maximum cooling power [W] 42 | * ΦH, max: maximum heating power [W] 43 | 44 | ## User Guide 45 | 46 | ### Installation 47 | 48 | You need Python 3.6 and pip installed. 49 | 50 | Install from GitHub (needs Git): 51 | 52 | $ pip install git+git://github.com/timtroendle/simple-simple 53 | 54 | If you don't have Git installed, download and extract the repository and then execute: 55 | 56 | $ cd 57 | $ pip install -e . 58 | 59 | ### Usage Example 60 | 61 | ```Python 62 | from datetime import timedelta 63 | from simplesimple import Building 64 | 65 | conditioned_floor_area = 100 66 | building = Building( 67 | heat_mass_capacity=165000 * conditioned_floor_area, 68 | heat_transmission=200, 69 | maximum_cooling_power=-10000, 70 | maximum_heating_power=10000, 71 | initial_building_temperature=16, 72 | time_step_size=timedelta(minutes=10), 73 | conditioned_floor_area=conditioned_floor_area 74 | ) 75 | 76 | # simulate one time step 77 | print(building.current_temperature) # returns 16 78 | print(building.thermal_power) # returns 0 79 | building.step(outside_temperature=20, heating_setpoint=18, cooling_setpoint=26) 80 | print(building.current_temperature) # returns ~16.4 81 | print(building.thermal_power) # returns 10000 82 | ``` 83 | 84 | ## Developer Guide 85 | 86 | ### Installation 87 | 88 | Best install simplesimple in editable mode: 89 | 90 | $ pip install -e . 91 | 92 | ### Run the test suite 93 | 94 | Run the test suite with py.test: 95 | 96 | $ py.test 97 | -------------------------------------------------------------------------------- /docs/simple-simple.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/timtroendle/simple-simple/0b594fc012fcb03161d847a00e892b36439a5d08/docs/simple-simple.jpg -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from setuptools import setup, find_packages 4 | 5 | exec(open('simplesimple/__init__.py').read()) 6 | 7 | setup( 8 | name='simplesimple', 9 | version=__version__, 10 | description='A simple Building Energy Model.', 11 | maintainer='Tim Tröndle', 12 | maintainer_email='tt397@cam.ac.uk', 13 | url='https://www.github.com/timtroendle/simple-simple', 14 | packages=find_packages(exclude=['tests*']), 15 | include_package_data=True, 16 | install_requires=[], 17 | classifiers=[ 18 | 'Environment :: Console', 19 | 'Intended Audience :: Science/Research', 20 | 'Programming Language :: Python', 21 | 'Programming Language :: Python :: 3 :: Only', 22 | 'Programming Language :: Python :: 3.6', 23 | 'Topic :: Scientific/Engineering' 24 | ] 25 | ) 26 | -------------------------------------------------------------------------------- /simplesimple/__init__.py: -------------------------------------------------------------------------------- 1 | from simplesimple.building import Building 2 | 3 | __version__ = '0.1.0.dev' 4 | -------------------------------------------------------------------------------- /simplesimple/building.py: -------------------------------------------------------------------------------- 1 | class Building(): 2 | """A simple Building Energy Model. 3 | 4 | Consisting of one thermal capacity and one resistance, this model is derived from the 5 | hourly dynamic model of the ISO 13790. It models heating and cooling energy demand only. 6 | 7 | Parameters: 8 | * heat_mass_capacity: capacity of the building's heat mass [J/K] 9 | * heat_transmission: heat transmission to the outside [W/K] 10 | * maximum_cooling_power: [W] (<= 0) 11 | * maximum_heating_power: [W] (>= 0) 12 | * initial_building_temperature: building temperature at start time [℃] 13 | * time_step_size: [s] 14 | * conditioned_floor_area: [m**2] 15 | """ 16 | 17 | def __init__(self, heat_mass_capacity, heat_transmission, 18 | maximum_cooling_power, maximum_heating_power, 19 | initial_building_temperature, time_step_size, 20 | conditioned_floor_area): 21 | if maximum_heating_power < 0: 22 | raise ValueError("Maximum heating power [W] must not be negative.") 23 | if maximum_cooling_power > 0: 24 | raise ValueError("Maximum cooling power [W] must not be positive.") 25 | self.__heat_mass_capacity = heat_mass_capacity 26 | self.__heat_transmission = heat_transmission 27 | self.__maximum_cooling_power = maximum_cooling_power 28 | self.__maximum_heating_power = maximum_heating_power 29 | self.current_temperature = initial_building_temperature 30 | self.thermal_power = 0 31 | self.__time_step_size = time_step_size 32 | self.__conditioned_floor_area = conditioned_floor_area 33 | 34 | def step(self, outside_temperature, heating_setpoint, cooling_setpoint): 35 | """Performs building simulation for the next time step. 36 | 37 | Parameters: 38 | * outside_temperature: [℃] 39 | * heating_setpoint: heating setpoint of the HVAC system [℃] 40 | * cooling_setpoint: cooling setpoint of the HVAC system [℃] 41 | """ 42 | def next_temperature(heating_cooling_power): 43 | return self._next_temperature( 44 | outside_temperature=outside_temperature, 45 | heating_setpoint=heating_setpoint, 46 | cooling_setpoint=cooling_setpoint, 47 | heating_cooling_power=heating_cooling_power 48 | ) 49 | next_temperature_no_power = next_temperature(0) 50 | if (next_temperature_no_power >= heating_setpoint and 51 | next_temperature_no_power <= cooling_setpoint): 52 | self.current_temperature = next_temperature_no_power 53 | else: 54 | if next_temperature_no_power < heating_setpoint: 55 | setpoint = heating_setpoint 56 | max_power = self.__maximum_heating_power 57 | else: 58 | setpoint = cooling_setpoint 59 | max_power = self.__maximum_cooling_power 60 | ten_watt_per_square_meter_power = 10 * self.__conditioned_floor_area 61 | next_temperature_power_10 = next_temperature(ten_watt_per_square_meter_power) 62 | unrestricted_power = (ten_watt_per_square_meter_power * 63 | (setpoint - next_temperature_no_power) / 64 | (next_temperature_power_10 - next_temperature_no_power)) 65 | if abs(unrestricted_power) <= abs(max_power): 66 | self.thermal_power = unrestricted_power 67 | else: 68 | self.thermal_power = max_power 69 | next_temperature_heating_cooling = next_temperature(self.thermal_power) 70 | self.current_temperature = next_temperature_heating_cooling 71 | 72 | def _next_temperature(self, outside_temperature, heating_setpoint, cooling_setpoint, 73 | heating_cooling_power): 74 | dt_by_cm = self.__time_step_size.total_seconds() / self.__heat_mass_capacity 75 | return (self.current_temperature * (1 - dt_by_cm * self.__heat_transmission) + 76 | dt_by_cm * (heating_cooling_power + self.__heat_transmission * outside_temperature)) 77 | -------------------------------------------------------------------------------- /tests/test_building.py: -------------------------------------------------------------------------------- 1 | from datetime import timedelta 2 | 3 | import pytest 4 | 5 | from simplesimple import Building 6 | 7 | 8 | @pytest.fixture 9 | def building(): 10 | conditioned_floor_area = 100 11 | return Building( 12 | heat_mass_capacity=165000 * conditioned_floor_area, 13 | heat_transmission=200, 14 | maximum_cooling_power=float("-inf"), 15 | maximum_heating_power=float("inf"), 16 | initial_building_temperature=22, 17 | time_step_size=timedelta(hours=1), 18 | conditioned_floor_area=conditioned_floor_area) 19 | 20 | 21 | @pytest.fixture 22 | def fully_damped_building(): 23 | conditioned_floor_area = 1 24 | return Building( 25 | heat_mass_capacity=3600 * conditioned_floor_area, 26 | heat_transmission=0, 27 | maximum_cooling_power=-1, 28 | maximum_heating_power=1, 29 | initial_building_temperature=22, 30 | time_step_size=timedelta(hours=1), 31 | conditioned_floor_area=conditioned_floor_area) 32 | 33 | 34 | def test_maximum_heating_power_cannot_be_negative(): 35 | with pytest.raises(ValueError): 36 | conditioned_floor_area = 100 37 | Building( 38 | heat_mass_capacity=165000 * conditioned_floor_area, 39 | heat_transmission=200, 40 | maximum_cooling_power=float("-inf"), 41 | maximum_heating_power=-0.01, 42 | initial_building_temperature=22, 43 | time_step_size=timedelta(hours=1), 44 | conditioned_floor_area=conditioned_floor_area 45 | ) 46 | 47 | 48 | def test_maximum_cooling_power_cannot_be_positive(): 49 | with pytest.raises(ValueError): 50 | conditioned_floor_area = 100 51 | Building( 52 | heat_mass_capacity=165000 * conditioned_floor_area, 53 | heat_transmission=200, 54 | maximum_cooling_power=0.01, 55 | maximum_heating_power=float("inf"), 56 | initial_building_temperature=22, 57 | time_step_size=timedelta(hours=1), 58 | conditioned_floor_area=conditioned_floor_area 59 | ) 60 | 61 | 62 | def test_thermal_power_zero_at_init(building): 63 | assert building.thermal_power == 0 64 | 65 | 66 | def test_building_temperature_remains_constant_when_same_temperature_outside(building): 67 | building.step(outside_temperature=22, heating_setpoint=21.9, cooling_setpoint=26) 68 | assert building.current_temperature == 22 69 | assert building.thermal_power == 0 70 | 71 | 72 | def test_building_temperature_raises_when_warmer_outside(building): 73 | building.step(outside_temperature=23, heating_setpoint=21.9, cooling_setpoint=26) 74 | assert building.current_temperature > 22 75 | assert building.thermal_power == 0 76 | 77 | 78 | def test_building_temperature_sinks_when_colder_outside(building): 79 | building.step(outside_temperature=21, heating_setpoint=21.9, cooling_setpoint=26) 80 | assert building.current_temperature < 22 81 | assert building.thermal_power == 0 82 | 83 | 84 | def test_building_gets_heated_when_below_heating_setpoint(building): 85 | building.step(outside_temperature=22, heating_setpoint=23, cooling_setpoint=26) 86 | assert building.current_temperature > 22 87 | assert building.current_temperature <= 23 88 | assert building.thermal_power > 0 89 | 90 | 91 | def test_building_gets_cooled_when_above_cooling_setpoint(building): 92 | building.step(outside_temperature=22, heating_setpoint=20, cooling_setpoint=21) 93 | assert building.current_temperature < 22 94 | assert building.current_temperature >= 21 95 | assert building.thermal_power < 0 96 | 97 | 98 | def test_building_gets_heated_with_max_power_when_too_cold(fully_damped_building): 99 | fully_damped_building.step(outside_temperature=22, heating_setpoint=23, cooling_setpoint=26) 100 | assert fully_damped_building.current_temperature == 23 101 | assert fully_damped_building.thermal_power == 1 102 | 103 | 104 | def test_building_does_not_exceed_max_heating_power(fully_damped_building): 105 | fully_damped_building.step(outside_temperature=22, heating_setpoint=24, cooling_setpoint=26) 106 | assert fully_damped_building.current_temperature == 23 107 | assert fully_damped_building.thermal_power == 1 108 | 109 | 110 | def test_building_gets_cooled_with_max_power_when_too_warm(fully_damped_building): 111 | fully_damped_building.step(outside_temperature=22, heating_setpoint=18, cooling_setpoint=21) 112 | assert fully_damped_building.current_temperature == 21 113 | assert fully_damped_building.thermal_power == -1 114 | 115 | 116 | def test_building_does_not_exceed_max_cooling_power(fully_damped_building): 117 | fully_damped_building.step(outside_temperature=22, heating_setpoint=18, cooling_setpoint=20) 118 | assert fully_damped_building.current_temperature == 21 119 | assert fully_damped_building.thermal_power == -1 120 | --------------------------------------------------------------------------------