├── .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 |
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 |
--------------------------------------------------------------------------------