Coverage for bim2sim/tasks/bps/enrich_use_cond.py: 93%
135 statements
« prev ^ index » next coverage.py v7.6.12, created at 2025-03-12 17:09 +0000
« prev ^ index » next coverage.py v7.6.12, created at 2025-03-12 17:09 +0000
1from typing import Union, Dict
3from bim2sim.kernel.decision import ListDecision, DecisionBunch
4from bim2sim.elements.bps_elements import ThermalZone
5from bim2sim.tasks.base import ITask
6from bim2sim.utilities.common_functions import get_use_conditions_dict, \
7 get_pattern_usage, wildcard_match, filter_elements
8from bim2sim.tasks.base import Playground
9from bim2sim.sim_settings import BuildingSimSettings
10from bim2sim.utilities.types import AttributeDataSource
13class EnrichUseConditions(ITask):
14 """Enriches Use Conditions of thermal zones
15 based on decisions and translation of zone names"""
17 reads = ('elements',)
19 def __init__(self, playground: Playground):
20 super().__init__(playground)
21 self.enriched_tz: list = []
22 self.use_conditions: dict = {}
24 def run(self, elements: dict):
25 """Enriches Use Conditions of thermal zones and central AHU settings.
27 Enrichment data in the files commonUsages.json and UseConditions.json
28 is taken from TEASER. The underlying data comes from DIN 18599-10 and
29 SIA 2024.
30 """
31 tz_elements = filter_elements(elements, 'ThermalZone', True)
32 # case no thermal zones found
33 if len(tz_elements) == 0:
34 self.logger.warning("Found no spaces to enrich")
35 else:
36 custom_use_cond_path = (
37 self.playground.sim_settings.prj_use_conditions)
38 custom_usage_path = \
39 self.playground.sim_settings.prj_custom_usages
41 self.logger.info("enriches thermal zones usage")
42 self.use_conditions = get_use_conditions_dict(custom_use_cond_path)
43 pattern_usage = get_pattern_usage(self.use_conditions,
44 custom_usage_path)
45 final_usages = yield from self.enrich_usages(
46 pattern_usage, tz_elements)
47 for tz, usage in final_usages.items():
48 orig_usage = tz.usage
49 tz.usage = usage
50 self.load_usage(tz)
51 # overwrite loaded heating and cooling profiles with
52 # template values if setpoints_from_template == True
53 if self.playground.sim_settings.setpoints_from_template:
54 tz.heating_profile = \
55 self.use_conditions[usage]['heating_profile']
56 tz.cooling_profile = \
57 self.use_conditions[usage]['cooling_profile']
58 # set maintained illuminance from sim_setting
59 tz.use_maintained_illuminance = (
60 self.playground.sim_settings.use_maintained_illuminance,
61 AttributeDataSource.enrichment)
62 # reset lighting_power if it was calculated before
63 tz.reset('lighting_power')
64 self.enriched_tz.append(tz)
65 self.logger.info('Enrich ThermalZone from IfcSpace with '
66 'original usage "%s" with usage "%s"',
67 orig_usage, usage)
68 # set heating and cooling based on sim settings configuration
69 building_elements = filter_elements(elements, 'Building')
70 self.overwrite_heating_cooling_ahu_by_settings(
71 tz_elements, building_elements, self.playground.sim_settings)
75 @staticmethod
76 def overwrite_heating_cooling_ahu_by_settings(
77 tz_elements: dict,
78 bldg_elements: list,
79 sim_settings: BuildingSimSettings) -> None:
80 """Set HVAC settings for thermal zones based on simulation settings.
82 Updates heating, cooling, and AHU usage for all thermal zones
83 according to the provided simulation settings.
85 Args:
86 tz_elements: Dictionary of thermal zone elements
87 bldg_elements: List of building elements
88 sim_settings: Building simulation settings
89 """
90 # Apply settings to all thermal zones
91 for tz in tz_elements.values():
92 if sim_settings.heating_tz_overwrite is not None:
93 tz.with_heating = (sim_settings.heating_tz_overwrite,
94 AttributeDataSource.enrichment)
95 if sim_settings.cooling_tz_overwrite is not None:
96 tz.with_cooling = (sim_settings.cooling_tz_overwrite,
97 AttributeDataSource.enrichment)
98 if sim_settings.ahu_tz_overwrite is not None:
99 tz.with_ahu = (sim_settings.ahu_tz_overwrite,
100 AttributeDataSource.enrichment)
101 if sim_settings.base_infiltration_rate_overwrite is not None:
102 tz.base_infiltration = (
103 sim_settings.base_infiltration_rate_overwrite,
104 AttributeDataSource.enrichment)
105 if sim_settings.use_constant_infiltration_overwrite is not None:
106 tz.use_constant_infiltration = (
107 sim_settings.use_constant_infiltration_overwrite,
108 AttributeDataSource.enrichment)
110 # overwrite building AHU settings if sim_settings are used
111 for building in bldg_elements:
112 if sim_settings.ahu_heating_overwrite is not None:
113 building.ahu_heating = (
114 sim_settings.ahu_heating_overwrite,
115 AttributeDataSource.enrichment)
116 if sim_settings.ahu_cooling_overwrite is not None:
117 building.ahu_cooling = (
118 sim_settings.ahu_cooling_overwrite,
119 AttributeDataSource.enrichment)
120 if sim_settings.ahu_humidification_overwrite is not None:
121 building.ahu_humidification = (
122 sim_settings.ahu_humidification_overwrite,
123 AttributeDataSource.enrichment)
124 if sim_settings.ahu_dehumidification_overwrite is not None:
125 building.ahu_dehumidification = (
126 sim_settings.ahu_dehumidification_overwrite,
127 AttributeDataSource.enrichment)
128 if sim_settings.ahu_heat_recovery_overwrite is not None:
129 building.ahu_heat_recovery = (
130 sim_settings.ahu_heat_recovery_overwrite,
131 AttributeDataSource.enrichment)
132 if sim_settings.ahu_heat_recovery_efficiency_overwrite is not None:
133 building.ahu_heat_recovery_efficiency = (
134 sim_settings.ahu_heat_recovery_efficiency_overwrite,
135 AttributeDataSource.enrichment)
137 # reset with_ahu on building level to make sure that _check_tz_ahu
138 # is performed again
139 building.reset('with_ahu')
143 @staticmethod
144 def list_decision_usage(tz: ThermalZone, choices: list) -> ListDecision:
145 """decision to select an usage that matches the zone name
147 Args:
148 tz: bim2sim ThermalZone element
149 choices: list of possible answers
150 Returns:
151 usage_decision: ListDecision to find the correct usage
152 """
153 usage_decision = ListDecision("Which usage does the Space %s have?" %
154 (str(tz.usage)),
155 choices=choices,
156 key='usage_'+str(tz.usage),
157 related=tz,
158 global_key="%s_%s.BpsUsage" %
159 (type(tz).__name__, tz.guid),
160 allow_skip=False,
161 live_search=True)
162 return usage_decision
164 @staticmethod
165 def office_usage(tz: ThermalZone) -> Union[str, list]:
166 """function to determine which office usage is best fitting"
168 The used enrichment for usage conditions come from DIN 18599-10. This
169 standard offers 3 types of office usages:
170 * Single office (1 workplace)
171 * Group office (2 - 6 workplaces)
172 * Open open offices (> 6 workplaces)
174 Based on the standards given medium occupancy density the following area
175 sections are defined.
176 * Single office < 14 m2
177 * Group office [14m2; 70 m2]
178 (70 m² is lower bound from open plan office)
179 * Open plan office > 70 m²
181 Args:
182 tz: bim2sim thermalzone element
183 Returns
184 matching usage as string or a list of str of no fitting
185 usage could be found
186 """
188 default_matches = ["Single office",
189 "Group Office (between 2 and 6 employees)",
190 "Open-plan Office (7 or more employees)"]
191 if tz.gross_area:
192 area = tz.gross_area.m
193 if area < 14:
194 return default_matches[0]
195 elif 14 <= area <= 70:
196 return default_matches[1]
197 else:
198 return default_matches[2]
199 # case area not available
200 else:
201 return default_matches
203 @classmethod
204 def enrich_usages(
205 cls,
206 pattern_usage: dict,
207 thermal_zones: Dict[str, ThermalZone]) -> Dict[str, ThermalZone]:
208 """Sets the usage of the given thermal_zones and enriches them.
210 Looks for fitting usages in assets/enrichment/usage based on the given
211 usage of a zone in the IFC. The way the usage is obtained is described
212 in the ThermalZone classes attribute "usage".
213 The following data is taken into account:
214 commonUsages.json: typical translations for the existing usage data
215 customUsages<prj_name>.json: project specific translations that can
216 be stored for easier simulation.
218 Args:
219 pattern_usage: Dict with custom and common pattern
220 thermal_zones: dict with tz elements guid as key and the element
221 itself as value
222 Returns:
223 final_usages: key: str of usage type, value: ThermalZone element
225 """
226 # selected_usage = {}
227 final_usages = {}
228 for tz in list(thermal_zones.values()):
229 orig_usage = str(tz.usage)
230 if orig_usage in pattern_usage:
231 final_usages[tz] = orig_usage
232 else:
233 matches = []
234 list_org = tz.usage.replace(' (', ' ').replace(')', ' '). \
235 replace(' -', ' ').replace(', ', ' ').split()
236 for usage in pattern_usage.keys():
237 # check custom first
238 if "custom" in pattern_usage[usage]:
239 for cus_usage in pattern_usage[usage]["custom"]:
240 # if cus_usage == tz.usage:
241 if wildcard_match(cus_usage, tz.usage):
242 if usage not in matches:
243 matches.append(usage)
244 # if not found in custom, continue with common
245 if len(matches) == 0:
246 for i in pattern_usage[usage]["common"]:
247 for i_name in list_org:
248 if i.match(i_name):
249 if usage not in matches:
250 matches.append(usage)
251 # if just one match
252 if len(matches) == 1:
253 # case its an office
254 if 'office_function' == matches[0]:
255 office_use = cls.office_usage(tz)
256 if isinstance(office_use, list):
257 final_usages[tz] = cls.list_decision_usage(
258 tz, office_use)
259 else:
260 final_usages[tz] = office_use
261 # other zone usage
262 else:
263 final_usages[tz] = matches[0]
264 # if no matches given forward all (for decision)
265 elif len(matches) == 0:
266 matches = list(pattern_usage.keys())
267 if len(matches) > 1:
268 final_usages[tz] = cls.list_decision_usage(
269 tz, matches)
270 # selected_usage[orig_usage] = tz.usage
271 # collect decisions
272 usage_dec_bunch = DecisionBunch()
273 for tz, use_or_dec in final_usages.items():
274 if isinstance(use_or_dec, ListDecision):
275 usage_dec_bunch.append(use_or_dec)
276 # remove duplicate decisions
277 unique_decisions, doubled_decisions = usage_dec_bunch.get_reduced_bunch(
278 criteria='key')
279 yield unique_decisions
280 answers = unique_decisions.to_answer_dict()
281 # combine answers and not answered decision
282 for dec in doubled_decisions:
283 final_usages[dec.related] = answers[dec.key]
284 for dec in unique_decisions:
285 final_usages[dec.related] = dec.value
286 # set usages
287 return final_usages
289 # def one_zone_usage(self, thermal_zones: dict):
290 # """defines an usage to all the building - since its a singular zone"""
291 # usage_decision = ListDecision("Which usage does the one_zone_building"
292 # " %s have?",
293 # choices=list(UseConditions.keys()),
294 # global_key="one_zone_usage",
295 # allow_skip=False,
296 # allow_load=True,
297 # allow_save=True,
298 # quick_decide=not True)
299 # usage_decision.decide()
300 # for tz in list(thermal_zones.values()):
301 # tz.usage = usage_decision.value
302 # self.enriched_tz.append(tz)
304 def load_usage(self, tz: ThermalZone):
305 """loads the usage of the corresponding ThermalZone.
307 Loads the usage from the statistical data in assets/enrichment/usage.
309 Args:
310 tz: bim2sim ThermalZone element
311 """
312 use_condition = self.use_conditions[tz.usage]
313 for attr, value in use_condition.items():
314 # avoid to overwrite attrs present on the element
315 if getattr(tz, attr) is None:
316 value = self.value_processing(value)
317 setattr(tz, attr, value)
319 @staticmethod
320 def value_processing(value: float):
321 """"""
322 if isinstance(value, dict):
323 values = next(iter(value.values()))
324 return values[0]/values[1]
325 else:
326 return value