Coverage for teaser/logic/buildingobjects/building.py: 80%
354 statements
« prev ^ index » next coverage.py v7.4.4, created at 2025-04-29 16:01 +0000
« prev ^ index » next coverage.py v7.4.4, created at 2025-04-29 16:01 +0000
1# created June 2015
2# by TEASER4 Development Team
4"""This module includes the Building class
5"""
6import inspect
7import random
8import re
9import warnings
10from teaser.logic.buildingobjects.calculation.aixlib import AixLib
11from teaser.logic.buildingobjects.calculation.ibpsa import IBPSA
14from teaser.logic.buildingobjects.buildingsystems.buildingahu import BuildingAHU
17class Building(object):
18 """Building Class
20 This class is used to manage information and parameter calculation for
21 Buildings. It is the base class for all archetype buildings and holds a
22 list containing all ThermalZones instances. In addition it can hold an
23 optional attribute for CentralAHU instance, that is e.g. needed for
24 AixLib Simulation models.
26 Parameters
27 ----------
29 parent: Project()
30 The parent class of this object, the Project the Building belongs to.
31 Allows for better control of hierarchical structures. If not None it
32 adds this Building instance to Project.buildings.
33 (default: None)
34 name : str
35 Individual name (default: None)
36 year_of_construction : int
37 Year of first construction (default: None)
38 net_leased_area : float [m2]
39 Total net leased area of building. This is area is NOT the footprint
40 of a building (default: None)
41 with_ahu : Boolean
42 If set to True, an empty instance of BuildingAHU is instantiated and
43 assigned to attribute central_ahu. This instance holds information for
44 central Air Handling units. Default is False.
45 internal_gains_mode: int [1, 2, 3]
46 mode for the internal gains calculation done in AixLib:
48 1. Temperature and activity degree dependent heat flux calculation for persons. The
49 calculation is based on SIA 2024 (default)
50 2. Temperature and activity degree independent heat flux calculation for persons, the max.
51 heatflowrate is prescribed by the parameter
52 fixed_heat_flow_rate_persons.
53 3. Temperature and activity degree dependent calculation with
54 consideration of moisture and co2. The moisture calculation is
55 based on SIA 2024 (2015) and regards persons and non-persons, the co2 calculation is based on
56 Engineering ToolBox (2004) and regards only persons.
57 inner_wall_approximation_approach : str
58 'teaser_default' (default) assumes area of inner wall per room as equal
59 to one typical length (defined in use conditions) * height of
60 floors, where number of rooms is
61 zone area/(typical length + typical width)
62 'typical_minus_outer' sets length of inner walls per room = 2 * typical
63 length * height of floors + 2 * typical width * height of floors
64 - length of outer or interzonal walls. When calculating the number
65 of rooms, considers the square root of the share for non-complete
66 rooms in comparison to 'teaser_default'.
67 'typical_minus_outer_extended' like 'typical_minus_outer', but also
68 considers that rooftops, windows and ground floors (= walls with
69 border to soil) may have vertical and horizontal shares
70 area of floors and ceilings is not affected by this and always equal to
71 (zone number of floors - 1) * zone area
73 Attributes
74 ----------
75 central_ahu : instance of BuildingAHU
76 Teaser Instance of BuildingAHU if a central AHU is embedded into the
77 building (currently mostly needed for AixLib simulation).
78 number_of_floors : int
79 number of floors above ground (default: None)
80 height_of_floors : float [m]
81 Average height of the floors (default: None)
82 internal_id : float
83 Random id for the distinction between different buildings.
84 year_of_retrofit : int
85 Year of last retrofit.
86 type_of_building : string
87 Type of a Building (e.g. Building (unspecified), Office etc.).
88 building_id : None
89 ID of building, can be set by the user to keep track of a building
90 even outside of TEASER, e.g. in a simulation or in post-processing.
91 This is not the same as internal_id, as internal_id is e.g. not
92 exported to Modelica models!
93 street_name : string
94 Name of the street the building is located at. (optional)
95 city : string
96 Name of the city the building is located at. (optional)
97 longitude : float [degree]
98 Longitude of building location.
99 latitude : float [degree]
100 Latitude of building location.
101 thermal_zones : list
102 List with instances of ThermalZone(), that are located in this building.
103 outer_area : dict [degree: m2]
104 Dictionary with orientation as key and sum of outer wall areas of
105 that direction as value.
106 window_area : dict [degree: m2]
107 Dictionary with orientation as key and sum of window areas of
108 that direction as value.
109 bldg_height : float [m]
110 Total building height.
111 area_rt : float [m2]
112 Total roof area of all thermal zones.
113 area_gf : float [m2]
114 Total ground floor area of all thermal zones.
115 volume : float [m3]
116 Total volume of all thermal zones.
117 sum_heat_load : float [W]
118 Total heating load of all thermal zones.
119 sum_cooling_load : float [W]
120 Total heating load of all thermal zones. (currently not supported)
121 number_of_elements_calc : int
122 Number of elements that are used for thermal zone calculation in this
123 building.
125 1. OneElement
126 2. TwoElement
127 3. ThreeElement
128 4. FourElement
130 merge_windows_calc : boolean
131 True for merging the windows into the outer wall's RC-combination,
132 False for separate resistance for window, default is False. (Only
133 supported for IBPSA)
134 used_library_calc : str
135 'AixLib' for https://github.com/RWTH-EBC/AixLib
136 'IBPSA' for https://github.com/ibpsa/modelica
137 library_attr : Annex() or AixLib() instance
138 Classes with specific functions and attributes for building models in
139 IBPSA and AixLib. Python classes can be found in calculation package.
140 t_bt : float
141 Time constant according to VDI 6007.
142 Default 5 d, only change if you know what you are doing.
143 See https://publications.rwth-aachen.de/record/749705/files/749705.pdf for more
144 information (Section 4.1.2)
145 t_bt_layer: float
146 Time constant according to VDI 6007 for aggragation of layers.
147 Default 7 d, only change if you know what you are doing
148 See https://publications.rwth-aachen.de/record/749705/files/749705.pdf for more
149 information (Section 4.1.2)
150 """
152 def __init__(
153 self,
154 parent=None,
155 name=None,
156 year_of_construction=None,
157 net_leased_area=None,
158 with_ahu=False,
159 internal_gains_mode=1,
160 inner_wall_approximation_approach='teaser_default'
161 ):
162 """Constructor of Building Class
163 """
165 self.parent = parent
166 self.name = name
167 self.year_of_construction = year_of_construction
168 self.net_leased_area = net_leased_area
169 self._with_ahu = with_ahu
171 if with_ahu is True:
172 self.central_ahu = BuildingAHU(self)
173 else:
174 self._central_ahu = None
176 self.internal_gains_mode = internal_gains_mode
177 self.number_of_floors = None
178 self.height_of_floors = None
179 self.inner_wall_approximation_approach \
180 = inner_wall_approximation_approach
181 self.internal_id = random.random()
182 self._year_of_retrofit = None
183 self.type_of_building = type(self).__name__
184 self.building_id = None
185 self.street_name = ""
186 self.city = ""
187 self.longitude = 6.05
188 self.latitude = 50.79
190 self._thermal_zones = []
191 self._outer_area = {}
192 self._window_area = {}
194 self.bldg_height = None
195 self.area_rt = None
196 self.area_gf = None
197 self.volume = 0
198 self.sum_heat_load = 0
199 self.sum_cooling_load = 0
200 self._number_of_elements_calc = 2
201 self._merge_windows_calc = False
202 self._used_library_calc = "AixLib"
204 self.library_attr = None
206 self.t_bt = 5
207 self.t_bt_layer = 7
209 def set_outer_wall_area(self, new_area, orientation):
210 """Outer area wall setter
212 sets the outer wall area of all walls of one direction and weights
213 them according to zone size. This function covers OuterWalls,
214 Rooftops, GroundFloors.
216 Parameters
217 ----------
218 new_area : float
219 new_area of all outer walls of one orientation
220 orientation : float
221 orientation of the obtained walls
222 """
224 for zone in self.thermal_zones:
225 for wall in zone.outer_walls:
226 if wall.orientation == orientation:
227 wall.area = ((new_area / self.net_leased_area) * zone.area) / sum(
228 count.orientation == orientation for count in zone.outer_walls
229 )
231 for roof in zone.rooftops:
232 if roof.orientation == orientation:
233 roof.area = ((new_area / self.net_leased_area) * zone.area) / sum(
234 count.orientation == orientation for count in zone.rooftops
235 )
237 for ground in zone.ground_floors:
238 if ground.orientation == orientation:
239 ground.area = ((new_area / self.net_leased_area) * zone.area) / sum(
240 count.orientation == orientation for count in zone.ground_floors
241 )
243 for door in zone.doors:
244 if door.orientation == orientation:
245 door.area = ((new_area / self.net_leased_area) * zone.area) / sum(
246 count.orientation == orientation for count in zone.doors
247 )
249 def set_window_area(self, new_area, orientation):
250 """Window area setter
252 sets the window area of all windows of one direction and weights
253 them according to zone size
255 Parameters
256 ----------
257 new_area : float
258 new_area of all window of one orientation
259 orientation : float
260 orientation of the obtained windows
261 """
263 for zone in self.thermal_zones:
264 for win in zone.windows:
265 if win.orientation == orientation:
266 win.area = ((new_area / self.net_leased_area) * zone.area) / sum(
267 count.orientation == orientation for count in zone.windows
268 )
270 def get_outer_wall_area(self, orientation):
271 """Get aggregated wall area of one orientation
273 Returns the area of all outer walls of one direction. This function
274 covers OuterWalls, GroundFloors and Rooftops.
276 Parameters
277 ----------
278 orientation : float
279 orientation of the obtained wall
280 Returns
281 -------
282 sum_area : float
283 area of all walls of one direction
284 """
286 sum_area = 0.0
287 for zone_count in self.thermal_zones:
288 for wall_count in zone_count.outer_walls:
289 if (
290 wall_count.orientation == orientation
291 and wall_count.area is not None
292 ):
293 sum_area += wall_count.area
294 for roof_count in zone_count.rooftops:
295 if (
296 roof_count.orientation == orientation
297 and roof_count.area is not None
298 ):
299 sum_area += roof_count.area
300 for ground_count in zone_count.ground_floors:
301 if (
302 ground_count.orientation == orientation
303 and ground_count.area is not None
304 ):
305 sum_area += ground_count.area
306 return sum_area
308 def get_window_area(self, orientation):
309 """Get aggregated window area of one orientation
311 returns the area of all windows of one direction
313 Parameters
314 ----------
315 orientation : float
316 orientation of the obtained windows
317 Returns
318 -------
319 sum_area : float
320 area of all windows of one direction
321 """
323 sum_area = 0.0
324 for zone_count in self.thermal_zones:
325 for win_count in zone_count.windows:
326 if win_count.orientation == orientation and win_count.area is not None:
327 sum_area += win_count.area
328 return sum_area
330 def get_inner_wall_area(self):
331 """Get aggregated inner wall area
333 Returns the area of all inner walls. This function covers InnerWalls,
334 Ceilings and Floors.
336 Returns
337 -------
338 sum_area : float
339 area of all inner walls
341 """
343 sum_area = 0.0
344 for zone_count in self.thermal_zones:
345 for wall_count in zone_count.inner_walls:
346 sum_area += wall_count.area
347 for floor in zone_count.floors:
348 sum_area += floor.area
349 for ceiling in zone_count.ceilings:
350 sum_area += ceiling.area
351 return sum_area
353 def fill_outer_area_dict(self):
354 """Fills the attribute outer_area
356 Fills the dictionary outer_area with the sum of outer wall area
357 corresponding to the orientations of the building. This function
358 covers OuterWalls, GroundFloors and Rooftops.
360 """
361 self.outer_area = {}
362 for zone_count in self.thermal_zones:
363 for wall_count in zone_count.outer_walls:
364 self.outer_area[wall_count.orientation] = None
365 for roof in zone_count.rooftops:
366 self.outer_area[roof.orientation] = None
367 for ground in zone_count.ground_floors:
368 self.outer_area[ground.orientation] = None
370 for key in self.outer_area:
371 self.outer_area[key] = self.get_outer_wall_area(key)
373 def fill_window_area_dict(self):
374 """Fills the attribute
376 Fills the dictionary window_area with the sum of window area
377 corresponding to the orientations of the building.
379 """
380 self.window_area = {}
381 for zone_count in self.thermal_zones:
382 for win_count in zone_count.windows:
383 self.window_area[win_count.orientation] = None
385 for key in self.window_area:
386 self.window_area[key] = self.get_window_area(key)
388 def calc_building_parameter(
389 self,
390 number_of_elements=None,
391 merge_windows=None,
392 used_library=None
393 ):
394 """calc all building parameters
396 This functions calculates the parameters of all zones in a building
397 sums norm heat load of all zones
398 sums volume of all zones
400 Parameters
401 ----------
402 number_of_elements : int, optional
403 defines the number of elements, that area aggregated, between 1
404 and 5. Default is 2. If None, uses existing class property
405 merge_windows : bool, optional
406 True for merging the windows into the outer walls, False for
407 separate resistance for window. If None, uses existing class
408 property
409 used_library : str, optional
410 used library (AixLib and IBPSA are supported). If None, uses
411 existing class property
412 """
413 # Use provided values or fall back to existing class properties
414 number_of_elements = (
415 number_of_elements if number_of_elements is not None
416 else self._number_of_elements_calc)
417 merge_windows = (merge_windows if merge_windows is not None
418 else self._merge_windows_calc)
419 used_library = used_library if used_library is not None else (
420 self._used_library_calc)
422 # Update class properties with the values being used
423 self.number_of_elements_calc = number_of_elements
424 self.merge_windows_calc = merge_windows
425 self.used_library_calc = used_library
427 self.area_rt = 0
428 self.area_gf = 0
429 for zone in self.thermal_zones:
430 zone.calc_zone_parameters(
431 number_of_elements=number_of_elements,
432 merge_windows=merge_windows,
433 t_bt=self.t_bt,
434 t_bt_layer=self.t_bt_layer
435 )
436 self.sum_heat_load += zone.model_attr.heat_load
437 self.area_rt += sum(rf.area for rf in zone.rooftops)
438 self.area_gf += sum(gf.area for gf in zone.ground_floors)
440 if self.used_library_calc == self.library_attr.__class__.__name__:
441 if self.used_library_calc == "AixLib":
442 self.library_attr.calc_auxiliary_attr()
443 else:
444 pass
445 elif self.library_attr is None:
446 if self.used_library_calc == "AixLib":
447 self.library_attr = AixLib(parent=self)
448 self.library_attr.calc_auxiliary_attr()
449 elif self.used_library_calc == "IBPSA":
450 self.library_attr = IBPSA(parent=self)
451 else:
452 warnings.warn(
453 "You set conflicting options for the used library "
454 "in Building or Project class and "
455 "calculation function of building. Your library "
456 "attributes are set to default using the library "
457 "you indicated in the function call, which is: "
458 + self.used_library_calc
459 )
461 if self.used_library_calc == "AixLib":
462 self.library_attr = AixLib(parent=self)
463 self.library_attr.calc_auxiliary_attr()
464 elif self.used_library_calc == "IBPSA":
465 self.library_attr = IBPSA(parent=self)
467 def retrofit_building(
468 self,
469 year_of_retrofit=None,
470 type_of_retrofit=None,
471 window_type=None,
472 material=None,
473 ):
474 """Retrofits all zones in the building
476 Function call for each zone.
478 After retrofit, all parameters are calculated directly.
480 Parameters
481 ----------
482 year_of_retrofit : float
483 Year of last retrofit.
484 type_of_retrofit : str
485 The classification of retrofit, if the archetype building
486 approach of TABULA is used.
487 window_type : str
488 Default: EnEv 2014
489 material : str
490 Default: EPS035
491 """
493 # Set self.sum_heat_load to zero to prevent summing up of old and new
494 # design heat load calculation values (see #518)
495 self.sum_heat_load = 0
497 if year_of_retrofit is not None:
498 self.year_of_retrofit = year_of_retrofit
500 for zone in self.thermal_zones:
501 zone.retrofit_zone(type_of_retrofit, window_type, material)
503 self.calc_building_parameter(
504 number_of_elements=self.number_of_elements_calc,
505 merge_windows=self.merge_windows_calc,
506 used_library=self.used_library_calc,
507 )
509 def rotate_building(self, angle):
510 """Rotates the building to a given angle
512 This function covers OuterWall, Rooftop (if not flat roof) and Windows.
514 Parameters
515 ----------
517 angle: float
518 rotation of the building clockwise, between 0 and 360 degrees
519 """
521 for zone_count in self.thermal_zones:
522 new_angle = None
523 for wall_count in zone_count.outer_walls:
524 new_angle = wall_count.orientation + angle
525 if new_angle > 360.0:
526 wall_count.orientation = new_angle - 360.0
527 else:
528 wall_count.orientation = new_angle
529 for roof_count in zone_count.rooftops:
530 if roof_count.orientation != -1:
531 new_angle = roof_count.orientation + angle
532 if new_angle > 360.0:
533 roof_count.orientation = new_angle - 360.0
534 else:
535 roof_count.orientation = new_angle
536 else:
537 pass
538 for win_count in zone_count.windows:
539 new_angle = win_count.orientation + angle
540 if new_angle > 360.0:
541 win_count.orientation = new_angle - 360.0
542 else:
543 win_count.orientation = new_angle
545 def add_zone(self, thermal_zone):
546 """Adds a thermal zone to the corresponding list
548 This function adds a ThermalZone instance to the the thermal_zones list
550 Parameters
551 ----------
552 thermal_zone : ThermalZone()
553 ThermalZone() instance of TEASER
554 """
556 ass_error_1 = "Zone has to be an instance of ThermalZone()"
558 assert type(thermal_zone).__name__ == "ThermalZone", ass_error_1
560 self._thermal_zones.append(thermal_zone)
562 @property
563 def parent(self):
564 return self.__parent
566 @parent.setter
567 def parent(self, value):
569 if value is not None:
571 ass_error_1 = "Parent has to be an instance of Project()"
573 assert type(value).__name__ == "Project", ass_error_1
575 self.__parent = value
577 if inspect.isclass(Building):
578 if self in self.__parent.buildings:
579 pass
580 else:
581 self.__parent.buildings.append(self)
583 else:
585 self.__parent = None
587 @property
588 def name(self):
589 return self.__name
591 @name.setter
592 def name(self, value):
593 if isinstance(value, str):
594 regex = re.compile("[^a-zA-z0-9]")
595 self.__name = regex.sub("", value)
596 else:
597 try:
598 value = str(value)
599 regex = re.compile("[^a-zA-z0-9]")
600 self.__name = regex.sub("", value)
601 except ValueError:
602 print("Can't convert name to string")
604 if self.__name[0].isdigit():
605 self.__name = "B" + self.__name
607 @property
608 def year_of_construction(self):
609 return self.__year_of_construction
611 @year_of_construction.setter
612 def year_of_construction(self, value):
614 if isinstance(value, int) or value is None:
616 self.__year_of_construction = value
617 else:
618 try:
619 value = int(value)
620 self.__year_of_construction = value
622 except:
623 raise ValueError("Can't convert year of construction to int")
625 @property
626 def number_of_floors(self):
627 return self.__number_of_floors
629 @number_of_floors.setter
630 def number_of_floors(self, value):
632 if isinstance(value, int) or value is None:
634 self.__number_of_floors = value
635 else:
636 try:
637 value = int(value)
638 self.__number_of_floors = value
640 except:
641 raise ValueError("Can't convert number of floors to int")
643 @property
644 def height_of_floors(self):
645 return self.__height_of_floors
647 @height_of_floors.setter
648 def height_of_floors(self, value):
650 if isinstance(value, float) or value is None:
652 self.__height_of_floors = value
653 else:
654 try:
655 value = float(value)
656 self.__height_of_floors = value
658 except:
659 raise ValueError("Can't convert height of floors to float")
661 @property
662 def net_leased_area(self):
663 return self.__net_leased_area
665 @net_leased_area.setter
666 def net_leased_area(self, value):
668 if isinstance(value, float):
669 self.__net_leased_area = value
670 elif value is None:
671 self.__net_leased_area = value
672 else:
673 try:
674 value = float(value)
675 self.__net_leased_area = value
676 except:
677 raise ValueError("Can't convert net leased area to float")
679 @property
680 def thermal_zones(self):
681 return self._thermal_zones
683 @thermal_zones.setter
684 def thermal_zones(self, value):
686 if value is None:
687 self._thermal_zones = []
689 @property
690 def outer_area(self):
691 return self._outer_area
693 @outer_area.setter
694 def outer_area(self, value):
695 self._outer_area = value
697 @property
698 def window_area(self):
699 return self._window_area
701 @window_area.setter
702 def window_area(self, value):
703 self._window_area = value
705 @property
706 def year_of_retrofit(self):
707 return self._year_of_retrofit
709 @year_of_retrofit.setter
710 def year_of_retrofit(self, value):
711 if self.year_of_construction is not None:
712 self._year_of_retrofit = value
713 else:
714 raise ValueError("Specify year of construction first")
716 @property
717 def with_ahu(self):
718 return self._with_ahu
720 @with_ahu.setter
721 def with_ahu(self, value):
723 if value is True and self.central_ahu is None:
724 self.central_ahu = BuildingAHU(self)
725 self._with_ahu = True
726 elif value and self.central_ahu and self._with_ahu is False:
727 self._with_ahu = True
728 elif value is False and self.central_ahu:
729 self.central_ahu = None
730 self._with_ahu = False
732 @property
733 def central_ahu(self):
734 return self._central_ahu
736 @central_ahu.setter
737 def central_ahu(self, value):
739 if value is None:
740 self._central_ahu = value
741 else:
743 ass_error_1 = "central AHU has to be an instance of BuildingAHU()"
745 assert type(value).__name__ == "BuildingAHU", ass_error_1
747 self._central_ahu = value
749 @property
750 def number_of_elements_calc(self):
752 return self._number_of_elements_calc
754 @number_of_elements_calc.setter
755 def number_of_elements_calc(self, value):
757 ass_error_1 = "calculation_method has to be 1, 2, 3, 4, or 5"
759 assert value != [1, 2, 3, 4, 5], ass_error_1
761 if self.parent is None and value is None:
762 self._number_of_elements_calc = 2
763 elif self.parent is not None and value is None:
764 self._number_of_elements_calc = self.parent.number_of_elements_calc
765 elif value is not None:
766 self._number_of_elements_calc = value
768 @property
769 def merge_windows_calc(self):
771 return self._merge_windows_calc
773 @merge_windows_calc.setter
774 def merge_windows_calc(self, value):
776 ass_error_1 = "merge windows needs to be True or False"
778 assert value != [True, False], ass_error_1
780 if self.parent is None and value is None:
781 self._merge_windows_calc = 2
782 elif self.parent is not None and value is None:
783 self._merge_windows_calc = self.parent.merge_windows_calc
784 elif value is not None:
785 self._merge_windows_calc = value
787 @property
788 def used_library_calc(self):
790 return self._used_library_calc
792 @used_library_calc.setter
793 def used_library_calc(self, value):
795 ass_error_1 = "used library needs to be AixLib or IBPSA"
797 assert value != ["AixLib", "IBPSA"], ass_error_1
799 if self.parent is None and value is None:
800 self._used_library_calc = "AixLib"
801 elif self.parent is not None and value is None:
802 self._used_library_calc = self.parent.used_library_calc
803 elif value is not None:
804 self._used_library_calc = value
806 if self.used_library_calc == "AixLib":
807 self.library_attr = AixLib(parent=self)
808 elif self.used_library_calc == "IBPSA":
809 self.library_attr = IBPSA(parent=self)
811 @property
812 def inner_wall_approximation_approach(self):
813 return self._inner_wall_approximation_approach
815 @inner_wall_approximation_approach.setter
816 def inner_wall_approximation_approach(self, value):
817 ass_error_1 = "inner wall approximation approach needs to be one of " \
818 "'teaser_default', 'typical_minus_outer', "\
819 "'typical_minus_outer_extended'"
821 assert value in (
822 'teaser_default',
823 'typical_minus_outer',
824 'typical_minus_outer_extended'
825 ), ass_error_1
827 self._inner_wall_approximation_approach = value