""" =============================================================================== BPEX - Part L 2026 Dwellings - opaque constructions for IES VE version 1.0 Building Performance Explained (BPEX) =============================================================================== What it does ------------ Part L 2026 limiting U-values for DWELLINGS (England) Approved Document L Volume 1, 2026 edition: Tables 3.1, 3.2 and 3.3. One construction per element and U-value: Standard same value for new and existing elements New new dwellings only Extension extensions and new or replacement elements in existing dwellings Existing Improved existing element being upgraded (Table 3.3 col b) Existing Threshold existing element at the upgrade threshold (Table 3.3 col a) Named in IES VE as e.g. "Part L 2026_DWL_External Wall Extension_0.18". Glazing: whole-window U-values (glass + frame). g-values are typical (0.63) - Part L sets no g-value limit for dwellings; see Approved Document O. External fire doorsets may be up to 1.8. Swimming pool basins not included. Each construction is built layer by layer (outside to inside) with typical materials, so it has the right U-value AND realistic thermal mass. How to use ---------- 1. Open your project in the VE and SAVE it first (so you can go back - the VE's Python interface cannot delete constructions, only you can). 2. Optionally change the SETTINGS below (by default it creates everything). 3. Run this file from the VE Python script editor. 4. Read the table printed at the end: it lists the VE ID of every construction next to its BPEX name. A copy is saved as a CSV next to this script (or in your current folder). 5. Save the VE project. Safe to run again: constructions already in the project with the same name are NOT created twice. BPEX materials are shared between all BPEX scripts and created only once. Nothing else in your project is changed. Disclaimer ---------- Typical build-ups sized to hit the stated limiting U-values. They are not verified build-ups of any specific building or product. Check them against your own information before relying on them. =============================================================================== """ # ============================================================================= # SETTINGS # ============================================================================= # Create only some constructions: list their codes, e.g. ['EW-EXT']. # Codes are listed in the DATA section below. Empty list = create all. IDS = [] # True = only print what would be created, change nothing in the project. DRY_RUN = False # ============================================================================= # Nothing below needs changing # ============================================================================= import os import csv import iesve SCRIPT_VERSION = '1.0' SET_LABEL = 'Part L 2026 Dwelling' LOG_NAME = 'bpex_partl_2026_dwl_log.csv' PREFIX = 'BPEX' U_TOLERANCE = 0.002 # W/m2K allowed between VE and BPEX calculation # ----------------------------------------------------------------------------- # DATA # ----------------------------------------------------------------------------- # 'u' = U-value calculated to BS EN ISO 6946 (as IES), 'target' = limiting value MATERIALS = { 'GL_CLR4': {'name': 'Clear float glass 4 mm', 'cat': 'Glass', 'k': 1.06, 'rho': 2500, 'cp': 840, 'mu': None, 'optical': {'transmittance': 0.82, 'outside_reflectance': 0.075, 'inside_reflectance': 0.075, 'outside_emissivity': 0.84, 'inside_emissivity': 0.84, 'refractive_index': 1.526, 'visible_transmittance': 0.9, 'outside_visible_reflectance': 0.08, 'inside_visible_reflectance': 0.08, 'thickness': 0.004}}, 'GL_LOWE6': {'name': 'Low-e glass 6 mm (soft coat, g approx. 0.63 in a double unit)', 'cat': 'Glass', 'k': 1.06, 'rho': 2500, 'cp': 840, 'mu': None, 'optical': {'transmittance': 0.6, 'outside_reflectance': 0.22, 'inside_reflectance': 0.24, 'outside_emissivity': 0.84, 'inside_emissivity': 0.04, 'refractive_index': 1.526, 'visible_transmittance': 0.82, 'outside_visible_reflectance': 0.1, 'inside_visible_reflectance': 0.11, 'thickness': 0.006}}, 'HL_ACB': {'name': 'Aircrete block incl. mortar (equivalent)', 'cat': 'Brick & Blockwork', 'k': 0.22, 'rho': 600, 'cp': 1000, 'mu': 40}, 'HL_BLKD': {'name': 'Dense concrete block', 'cat': 'Brick & Blockwork', 'k': 1.13, 'rho': 2000, 'cp': 1000, 'mu': 75}, 'HL_BRKO': {'name': 'Brickwork, outer leaf (exposed)', 'cat': 'Brick & Blockwork', 'k': 0.77, 'rho': 1700, 'cp': 800, 'mu': 50}, 'HL_DCORE': {'name': 'Sectional door panel core (effective, incl. joints)', 'cat': 'Insulating Materials', 'k': 0.08, 'rho': 40, 'cp': 1400, 'mu': None}, 'HL_E11MW37': {'name': 'Mineral wool batt between joists (11% joists, equivalent)', 'cat': 'Insulating Materials', 'k': 0.04723, 'rho': 81.7, 'cp': 1413.7, 'mu': 26.4}, 'HL_E9LW40': {'name': 'Glass mineral wool loft roll between joists (9% joists, equivalent)', 'cat': 'Insulating Materials', 'k': 0.0481, 'rho': 55.9, 'cp': 1488.7, 'mu': 22.6}, 'HL_LW40': {'name': 'Glass mineral wool loft roll', 'cat': 'Insulating Materials', 'k': 0.04, 'rho': 12, 'cp': 1030, 'mu': 5}, 'HL_MF44': {'name': 'Blown mineral fibre cavity fill', 'cat': 'Insulating Materials', 'k': 0.044, 'rho': 30, 'cp': 1030, 'mu': 5}, 'HL_MW32': {'name': 'Mineral wool cavity batt (high performance)', 'cat': 'Insulating Materials', 'k': 0.032, 'rho': 35, 'cp': 1030, 'mu': 5}, 'HL_PIR22': {'name': 'PIR foam board', 'cat': 'Insulating Materials', 'k': 0.022, 'rho': 32, 'cp': 1400, 'mu': 300}, 'HL_PLD': {'name': 'Dense gypsum plaster', 'cat': 'Plaster', 'k': 0.5, 'rho': 1300, 'cp': 1000, 'mu': 50}, 'HL_PLL': {'name': 'Lightweight gypsum plaster', 'cat': 'Plaster', 'k': 0.18, 'rho': 600, 'cp': 1000, 'mu': 50}, 'HL_PLY': {'name': 'Plywood sheathing', 'cat': 'Boards, Sheets & Decking', 'k': 0.13, 'rho': 500, 'cp': 1600, 'mu': 500}, 'HL_SCR': {'name': 'Sand-cement screed', 'cat': 'Screeds & Renders', 'k': 1.15, 'rho': 2000, 'cp': 1000, 'mu': 100}, 'HL_SOIL': {'name': 'Ground, clay/silt (equivalent layer)', 'cat': 'Soils', 'k': 1.5, 'rho': 1500, 'cp': 2000, 'mu': 50}, 'HL_SWD': {'name': 'Softwood', 'cat': 'Timber', 'k': 0.13, 'rho': 500, 'cp': 1600, 'mu': 200}, 'HL_TILE': {'name': 'Clay roof tiles', 'cat': 'Tiles', 'k': 1, 'rho': 2000, 'cp': 800, 'mu': 200}, 'STD_CC2': {'name': 'Reinforced Concrete', 'cat': 'Concretes', 'k': 2.3, 'rho': 2300, 'cp': 1000, 'mu': None}, 'STD_US1': {'name': 'Plasterboard', 'cat': 'Plaster', 'k': 0.21, 'rho': 700, 'cp': 1000, 'mu': 0}, } CONSTRUCTIONS = [ {'id': 'RF-STD', 'group': 'All', 'btype': 'Dwelling', 'name': 'Part L 2026_DWL_Roof Standard_0.16', 'cat': 'Roof', 'u': 0.16, 'target': 0.16, 'out': {'emis': 0.9, 'res': 0.04, 'abs': 0.7}, 'in': {'emis': 0.9, 'res': 0.1, 'abs': 0.55}, 'layers': [{'cavity': False, 'mat': 'HL_TILE', 't': 12}, {'cavity': True, 't': 150, 'R': 0.2}, {'cavity': False, 'mat': 'HL_LW40', 't': 150.4, 'solved': True}, {'cavity': False, 'mat': 'HL_E9LW40', 't': 100}, {'cavity': False, 'mat': 'STD_US1', 't': 12.5}]}, {'id': 'RF-EXT', 'group': 'All', 'btype': 'Dwelling', 'name': 'Part L 2026_DWL_Roof Extension_0.15', 'cat': 'Roof', 'u': 0.15, 'target': 0.15, 'out': {'emis': 0.9, 'res': 0.04, 'abs': 0.7}, 'in': {'emis': 0.9, 'res': 0.1, 'abs': 0.55}, 'layers': [{'cavity': False, 'mat': 'HL_TILE', 't': 12}, {'cavity': True, 't': 150, 'R': 0.2}, {'cavity': False, 'mat': 'HL_LW40', 't': 167.1, 'solved': True}, {'cavity': False, 'mat': 'HL_E9LW40', 't': 100}, {'cavity': False, 'mat': 'STD_US1', 't': 12.5}]}, {'id': 'RF-THR', 'group': 'All', 'btype': 'Dwelling', 'name': 'Part L 2026_DWL_Roof Existing Threshold_0.35', 'cat': 'Roof', 'u': 0.3498, 'target': 0.35, 'out': {'emis': 0.9, 'res': 0.04, 'abs': 0.7}, 'in': {'emis': 0.9, 'res': 0.1, 'abs': 0.55}, 'layers': [{'cavity': False, 'mat': 'HL_TILE', 't': 12}, {'cavity': True, 't': 150, 'R': 0.2}, {'cavity': False, 'mat': 'HL_E9LW40', 't': 117.7, 'solved': True}, {'cavity': False, 'mat': 'STD_US1', 't': 12.5}]}, {'id': 'EW-NEW', 'group': 'All', 'btype': 'Dwelling', 'name': 'Part L 2026_DWL_External Wall New_0.26', 'cat': 'External Wall', 'u': 0.2598, 'target': 0.26, 'out': {'emis': 0.9, 'res': 0.04, 'abs': 0.7}, 'in': {'emis': 0.9, 'res': 0.13, 'abs': 0.55}, 'layers': [{'cavity': False, 'mat': 'HL_BRKO', 't': 102.5}, {'cavity': False, 'mat': 'HL_MW32', 't': 96.6, 'solved': True}, {'cavity': False, 'mat': 'HL_ACB', 't': 100}, {'cavity': False, 'mat': 'HL_PLL', 't': 13}]}, {'id': 'EW-EXT', 'group': 'All', 'btype': 'Dwelling', 'name': 'Part L 2026_DWL_External Wall Extension_0.18', 'cat': 'External Wall', 'u': 0.1799, 'target': 0.18, 'out': {'emis': 0.9, 'res': 0.04, 'abs': 0.7}, 'in': {'emis': 0.9, 'res': 0.13, 'abs': 0.55}, 'layers': [{'cavity': False, 'mat': 'HL_BRKO', 't': 102.5}, {'cavity': False, 'mat': 'HL_MW32', 't': 151.3, 'solved': True}, {'cavity': False, 'mat': 'HL_ACB', 't': 100}, {'cavity': False, 'mat': 'HL_PLL', 't': 13}]}, {'id': 'EW-IMP-CAV', 'group': 'All', 'btype': 'Dwelling', 'name': 'Part L 2026_DWL_External Wall Existing Improved Cavity_0.55', 'cat': 'External Wall', 'u': 0.5495, 'target': 0.55, 'out': {'emis': 0.9, 'res': 0.04, 'abs': 0.7}, 'in': {'emis': 0.9, 'res': 0.13, 'abs': 0.55}, 'layers': [{'cavity': False, 'mat': 'HL_BRKO', 't': 102.5}, {'cavity': False, 'mat': 'HL_MF44', 't': 61.7, 'solved': True}, {'cavity': False, 'mat': 'HL_BLKD', 't': 100}, {'cavity': False, 'mat': 'HL_PLD', 't': 13}]}, {'id': 'EW-IMP-INS', 'group': 'All', 'btype': 'Dwelling', 'name': 'Part L 2026_DWL_External Wall Existing Improved Insulated_0.30', 'cat': 'External Wall', 'u': 0.2998, 'target': 0.3, 'out': {'emis': 0.9, 'res': 0.04, 'abs': 0.7}, 'in': {'emis': 0.9, 'res': 0.13, 'abs': 0.55}, 'layers': [{'cavity': False, 'mat': 'HL_BRKO', 't': 215}, {'cavity': True, 't': 15, 'R': 0.1}, {'cavity': False, 'mat': 'HL_PIR22', 't': 60.0, 'solved': True}, {'cavity': False, 'mat': 'STD_US1', 't': 12.5}]}, {'id': 'EW-THR', 'group': 'All', 'btype': 'Dwelling', 'name': 'Part L 2026_DWL_External Wall Existing Threshold_0.70', 'cat': 'External Wall', 'u': 0.6998, 'target': 0.7, 'out': {'emis': 0.9, 'res': 0.04, 'abs': 0.7}, 'in': {'emis': 0.9, 'res': 0.13, 'abs': 0.55}, 'layers': [{'cavity': False, 'mat': 'HL_BRKO', 't': 102.5}, {'cavity': False, 'mat': 'HL_MF44', 't': 44.5, 'solved': True}, {'cavity': False, 'mat': 'HL_BLKD', 't': 100}, {'cavity': False, 'mat': 'HL_PLD', 't': 13}]}, {'id': 'FL-STD', 'group': 'All', 'btype': 'Dwelling', 'name': 'Part L 2026_DWL_Floor Standard_0.18', 'cat': 'Ground/Exposed Floor', 'u': 0.18, 'target': 0.18, 'out': {'emis': 0.9, 'res': 0.04, 'abs': 0.7}, 'in': {'emis': 0.9, 'res': 0.17, 'abs': 0.55}, 'layers': [{'cavity': False, 'mat': 'HL_SOIL', 't': 2108.2}, {'cavity': False, 'mat': 'HL_PIR22', 't': 84.3, 'solved': True}, {'cavity': False, 'mat': 'STD_CC2', 't': 150}, {'cavity': False, 'mat': 'HL_SCR', 't': 50}]}, {'id': 'FL-IMP', 'group': 'All', 'btype': 'Dwelling', 'name': 'Part L 2026_DWL_Floor Existing Improved_0.25', 'cat': 'Ground/Exposed Floor', 'u': 0.25, 'target': 0.25, 'out': {'emis': 0.9, 'res': 0.04, 'abs': 0.7}, 'in': {'emis': 0.9, 'res': 0.17, 'abs': 0.55}, 'layers': [{'cavity': False, 'mat': 'HL_SOIL', 't': 1725.5}, {'cavity': True, 't': 300, 'R': 0.21}, {'cavity': False, 'mat': 'HL_E11MW37', 't': 107.5, 'solved': True}, {'cavity': False, 'mat': 'HL_SWD', 't': 20}]}, {'id': 'FL-THR', 'group': 'All', 'btype': 'Dwelling', 'name': 'Part L 2026_DWL_Floor Existing Threshold_0.70', 'cat': 'Ground/Exposed Floor', 'u': 0.7, 'target': 0.7, 'out': {'emis': 0.9, 'res': 0.04, 'abs': 0.7}, 'in': {'emis': 0.9, 'res': 0.17, 'abs': 0.55}, 'layers': [{'cavity': False, 'mat': 'HL_SOIL', 't': 1664.9, 'solved': True}, {'cavity': False, 'mat': 'STD_CC2', 't': 150}, {'cavity': False, 'mat': 'HL_SCR', 't': 50}]}, {'id': 'PW', 'group': 'All', 'btype': 'Dwelling', 'name': 'Part L 2026_DWL_Party Wall_0.20', 'cat': 'Internal Partition', 'u': 0.2, 'target': 0.2, 'out': {'emis': 0.9, 'res': 0.13, 'abs': 0.7}, 'in': {'emis': 0.9, 'res': 0.13, 'abs': 0.55}, 'layers': [{'cavity': False, 'mat': 'HL_PLL', 't': 13}, {'cavity': False, 'mat': 'HL_ACB', 't': 100}, {'cavity': False, 'mat': 'HL_MW32', 't': 118, 'solved': True}, {'cavity': False, 'mat': 'HL_ACB', 't': 100}, {'cavity': False, 'mat': 'HL_PLL', 't': 13}]}, {'id': 'DR-NEW', 'group': 'All', 'btype': 'Dwelling', 'name': 'Part L 2026_DWL_Door New_1.60', 'cat': 'Door', 'u': 1.5973, 'target': 1.6, 'out': {'emis': 0.9, 'res': 0.04, 'abs': 0.7}, 'in': {'emis': 0.9, 'res': 0.13, 'abs': 0.55}, 'layers': [{'cavity': False, 'mat': 'HL_PLY', 't': 6}, {'cavity': False, 'mat': 'HL_DCORE', 't': 29.1, 'solved': True}, {'cavity': False, 'mat': 'HL_PLY', 't': 6}]}, {'id': 'DR-EXT', 'group': 'All', 'btype': 'Dwelling', 'name': 'Part L 2026_DWL_Door Extension_1.40', 'cat': 'Door', 'u': 1.399, 'target': 1.4, 'out': {'emis': 0.9, 'res': 0.04, 'abs': 0.7}, 'in': {'emis': 0.9, 'res': 0.13, 'abs': 0.55}, 'layers': [{'cavity': False, 'mat': 'HL_PLY', 't': 6}, {'cavity': False, 'mat': 'HL_DCORE', 't': 36.2, 'solved': True}, {'cavity': False, 'mat': 'HL_PLY', 't': 6}]}, {'id': 'WN-NEW', 'group': 'All', 'btype': 'Dwelling', 'name': 'Part L 2026_DWL_Window New_1.60', 'cat': 'Window', 'u': 1.6, 'target': 1.6, 'out': {'emis': 0.84, 'res': 0.04, 'abs': 0.0}, 'in': {'emis': 0.84, 'res': 0.13, 'abs': 0.0}, 'layers': [{'cavity': False, 'mat': 'GL_LOWE6', 't': 6}, {'cavity': True, 't': 16, 'R': 0.4}, {'cavity': False, 'mat': 'GL_CLR4', 't': 4}], 'glazed': True, 'g': 0.63, 'frame_pct': 30, 'frame_u': 1.4, 'glazing_type': 'double_glazed_low_e_soft_coat', 'frame_material': 'pvc'}, {'id': 'WN-EXT', 'group': 'All', 'btype': 'Dwelling', 'name': 'Part L 2026_DWL_Window Extension_1.40', 'cat': 'Window', 'u': 1.4, 'target': 1.4, 'out': {'emis': 0.84, 'res': 0.04, 'abs': 0.0}, 'in': {'emis': 0.84, 'res': 0.13, 'abs': 0.0}, 'layers': [{'cavity': False, 'mat': 'GL_LOWE6', 't': 6}, {'cavity': True, 't': 16, 'R': 0.4}, {'cavity': False, 'mat': 'GL_CLR4', 't': 4}], 'glazed': True, 'g': 0.63, 'frame_pct': 30, 'frame_u': 1.4, 'glazing_type': 'double_glazed_low_e_soft_coat', 'frame_material': 'pvc'}, {'id': 'RL-STD', 'group': 'All', 'btype': 'Dwelling', 'name': 'Part L 2026_DWL_Rooflight Standard_2.20', 'cat': 'Rooflight', 'u': 2.2, 'target': 2.2, 'out': {'emis': 0.84, 'res': 0.04, 'abs': 0.0}, 'in': {'emis': 0.84, 'res': 0.13, 'abs': 0.0}, 'layers': [{'cavity': False, 'mat': 'GL_LOWE6', 't': 6}, {'cavity': True, 't': 16, 'R': 0.4}, {'cavity': False, 'mat': 'GL_CLR4', 't': 4}], 'glazed': True, 'g': 0.63, 'frame_pct': 25, 'frame_u': 2.0, 'glazing_type': 'double_glazed_low_e_soft_coat', 'frame_material': 'aluminium'}, {'id': 'GD-NEW', 'group': 'All', 'btype': 'Dwelling', 'name': 'Part L 2026_DWL_Glazed Door New_1.60', 'cat': 'Glazed Door', 'u': 1.6, 'target': 1.6, 'out': {'emis': 0.84, 'res': 0.04, 'abs': 0.0}, 'in': {'emis': 0.84, 'res': 0.13, 'abs': 0.0}, 'layers': [{'cavity': False, 'mat': 'GL_LOWE6', 't': 6}, {'cavity': True, 't': 16, 'R': 0.4}, {'cavity': False, 'mat': 'GL_CLR4', 't': 4}], 'glazed': True, 'g': 0.63, 'frame_pct': 30, 'frame_u': 1.4, 'glazing_type': 'double_glazed_low_e_soft_coat', 'frame_material': 'pvc'}, {'id': 'GD-EXT', 'group': 'All', 'btype': 'Dwelling', 'name': 'Part L 2026_DWL_Glazed Door Extension_1.40', 'cat': 'Glazed Door', 'u': 1.4, 'target': 1.4, 'out': {'emis': 0.84, 'res': 0.04, 'abs': 0.0}, 'in': {'emis': 0.84, 'res': 0.13, 'abs': 0.0}, 'layers': [{'cavity': False, 'mat': 'GL_LOWE6', 't': 6}, {'cavity': True, 't': 16, 'R': 0.4}, {'cavity': False, 'mat': 'GL_CLR4', 't': 4}], 'glazed': True, 'g': 0.63, 'frame_pct': 30, 'frame_u': 1.4, 'glazing_type': 'double_glazed_low_e_soft_coat', 'frame_material': 'pvc'}, ] # ----------------------------------------------------------------------------- # IES categories # ----------------------------------------------------------------------------- def _enum(enum_cls, *names): for n in names: if hasattr(enum_cls, n): return getattr(enum_cls, n) raise AttributeError('None of %s found in %s' % (names, enum_cls)) MC = iesve.material_categories MATERIAL_CATEGORY = { 'Asphalts & Bitumens': _enum(MC, 'asphalts'), 'Boards, Sheets & Decking': _enum(MC, 'boards'), 'Brick & Blockwork': _enum(MC, 'bricks'), 'Concretes': _enum(MC, 'concretes'), 'Insulating Materials': _enum(MC, 'insulating'), 'Metals': _enum(MC, 'metals'), 'Plaster': _enum(MC, 'plaster'), 'Screeds & Renders': _enum(MC, 'screeds'), 'Soils': _enum(MC, 'soils', 'other'), # IES has no soils category 'Tiles': _enum(MC, 'tiles', 'titles'), # IES spells it 'titles' 'Timber': _enum(MC, 'timber'), 'Glass': _enum(MC, 'glass'), } EC = iesve.element_categories CONSTRUCTION_CATEGORY = { 'External Wall': EC.wall, 'Roof': EC.roof, 'Ground/Exposed Floor': EC.ground_floor, 'Door': EC.door, 'Internal Partition': EC.partition, 'Window': EC.ext_glazing, 'Glazed Door': EC.ext_glazing, 'Rooflight': EC.roof_light, } # Property names confirmed in IES VE (Python 3.12 build, Oct 2026) NAME_KEYS = ['reference', 'description', 'name'] # ----------------------------------------------------------------------------- # Helpers # ----------------------------------------------------------------------------- WARNINGS = [] def say(*args): print(' '.join(str(a) for a in args)) def warn(msg): WARNINGS.append(msg) say(' WARNING:', msg) def get_props(obj): try: p = obj.get_properties() except TypeError: try: p = obj.get_properties(iesve.uvalue_types.iso) except Exception: p = {} except Exception: p = {} return p if isinstance(p, dict) else {} def set_props(obj, values): """Set properties; returns the list of keys that failed.""" failed = [] for key, value in values.items(): try: obj.set_properties({key: value}) except Exception: try: obj.set_properties(**{key: value}) except Exception: failed.append(key) return failed def material_desc(code): return '%s %s %s' % (PREFIX, code, MATERIALS[code]['name']) def bpex_code(description): """'BPEX HL_RND Sand-cement render' -> 'HL_RND' (else None).""" parts = str(description or '').split() if len(parts) >= 2 and parts[0] == PREFIX: return parts[1] return None def selected(c): if IDS: return c['id'] in IDS return c.get('group') in globals().get('GROUPS', [c.get('group')]) def get_project(): cdb = iesve.VECdbDatabase.get_current_database() return cdb.get_projects()[iesve.project_types.project][0] # ----------------------------------------------------------------------------- # Fingerprints: recognise a BPEX construction by its layers # ----------------------------------------------------------------------------- def fingerprint_from_data(c): fp = [int(CONSTRUCTION_CATEGORY[c['cat']])] for l in c['layers']: fp.append(('cavity' if l['cavity'] else l['mat'], round(l['t'], 1))) return tuple(fp) def fingerprint_from_ve(con): fp = [int(con.category)] for layer in con.get_layers(): mat = layer.get_material(True) t = round(layer.get_properties().get('thickness', 0) * 1000, 1) if mat: code = bpex_code(mat.get_properties().get('description')) if code is None: return None # contains a non-BPEX material fp.append((code, t)) else: fp.append(('cavity', t)) return tuple(fp) def existing_constructions_by_name(project): """{name: construction id} for every construction in the project.""" out = {} for cid in project.get_construction_ids(iesve.construction_class.none): try: name = str(project.get_construction(cid, iesve.construction_class.none).reference).strip() out.setdefault(name, cid) except Exception: pass return out def existing_bpex_materials(project): out = {} for mid in project.get_material_ids(MC.all): try: code = bpex_code(project.get_material(mid).get_properties().get('description')) if code and code not in out: out[code] = mid except Exception: pass return out # ----------------------------------------------------------------------------- # Build # ----------------------------------------------------------------------------- def create_material(project, code): m = MATERIALS[code] mat = project.create_material(MATERIAL_CATEGORY[m['cat']]) mid = mat.get_properties().get('id') values = {'description': material_desc(code), 'conductivity': m['k'], 'density': m['rho'], 'specific_heat_capacity': m['cp']} if m['mu'] is not None: values['vapour_resistivity'] = m['mu'] values.update(m.get('optical', {})) failed = set_props(mat, values) if failed: warn('material %s: could not set %s' % (code, ', '.join(failed))) return mid def try_name(con, name): """Set the construction name (shown as its description). True if it stuck.""" def stuck(): try: return str(con.reference).strip() == name except Exception: return False try: con.reference = name # this is the way that works in the VE except Exception: pass if stuck(): return True for key in NAME_KEYS: set_props(con, {key: name}) if stuck(): return True return False def layer_ids(con): return [l.get_id() for l in con.get_layers()] def add_layers(con, c, material_ids): for layer in c['layers']: before = set(layer_ids(con)) if layer['cavity']: con.add_layer('', True) else: con.add_layer(material_ids[layer['mat']], False) new = [l for l in con.get_layers() if l.get_id() not in before] if len(new) != 1: raise RuntimeError('could not identify the new layer') values = {'thickness': layer['t'] / 1000.0} if layer['cavity']: values['resistance'] = layer['R'] failed = set_props(new[0], values) if failed: warn('%s: could not set layer %s' % (c['id'], ', '.join(failed))) def build(project, c, material_ids): con = project.create_construction(CONSTRUCTION_CATEGORY[c['cat']]) try_name(con, c['name']) default_layers = layer_ids(con) failed = set_props(con, { 'outside_surface_resistance': c['out']['res'], 'inside_surface_resistance': c['in']['res'], 'outside_surface_emissivity': c['out']['emis'], 'inside_surface_emissivity': c['in']['emis'], 'outside_surface_solar_absorptivity': c['out']['abs'], 'inside_surface_solar_absorptivity': c['in']['abs']}) if failed: warn('%s: could not set %s' % (c['id'], ', '.join(failed))) add_layers(con, c, material_ids) for lid in default_layers: con.delete_layer(lid) if fingerprint_from_ve(con) != fingerprint_from_data(c): warn('%s: layers in the VE do not match the BPEX build-up - please check' % c['id']) return con # ----------------------------------------------------------------------------- # Glazing: built from panes + gas gap + frame, then calibrated against the # VE's own U-value and g-value calculation so the results match IES exactly. # ----------------------------------------------------------------------------- G_CALIBRATED = {} # glass material code -> True once its transmittance is set def _u(con): return con.get_u_factor(iesve.uvalue_types.iso) def _g(con): try: g = con.get_g_values() return g.get('bs_en_410') if g else None except Exception: return None def _bisect(setter, measure, target, lo, hi, increasing, iters=40, tol=0.0005): """Find x in [lo, hi] so measure() == target after setter(x).""" best = None for _ in range(iters): mid = (lo + hi) / 2.0 if not setter(mid): return None val = measure() if val is None: return None best = (mid, val) if abs(val - target) <= tol: break if (val < target) == increasing: lo = mid else: hi = mid return best def frame_scale(con): """IES may store frame percentage as 0-100 or 0-1.""" fp = get_props(con).get('frame_percent') return 1.0 if (fp is not None and fp <= 1.0) else 100.0 def build_glazed(project, c, material_ids): what = c['id'] con = project.create_construction(CONSTRUCTION_CATEGORY[c['cat']]) try_name(con, c['name']) try: con.set_const_class(iesve.construction_class.glazed) except Exception: pass default_layers = layer_ids(con) add_layers(con, c, material_ids) for lid in default_layers: try: con.delete_layer(lid) except Exception: warn('%s: could not delete a default layer' % what) scale = frame_scale(con) f = c['frame_pct'] / 100.0 rframe = max(0.0, 1.0 / c['frame_u'] - 0.17) extra = {'frame_resistance': rframe} gt = getattr(iesve, 'glazing_types', None) if gt is not None and hasattr(gt, c.get('glazing_type', '')): extra['glazing_type'] = getattr(gt, c['glazing_type']) fm = getattr(iesve, 'frame_materials', None) if fm is not None and hasattr(fm, c.get('frame_material', '')): extra['frame_material'] = getattr(fm, c['frame_material']) failed = set_props(con, extra) if failed: warn('%s: could not set %s' % (what, ', '.join(failed))) # Does the VE U-value include the frame? set_props(con, {'frame_percent': 0.0}) u0 = _u(con) if set_props(con, {'frame_percent': f * scale}): warn('%s: could not set frame_percent' % what) u1 = _u(con) includes_frame = abs(u1 - u0) > 1e-4 target_u = c['u'] if includes_frame else (c['u'] - f * c['frame_u']) / (1 - f) # Gas gap layer (the cavity) gap = [l for l in con.get_layers() if not l.get_material(False) and not l.get_material(True)] gap = gap[0] if gap else None # 1. g-value: calibrate the outer pane's solar transmittance once per glass type pane = c['layers'][0]['mat'] if c.get('g') and not G_CALIBRATED.get(pane): mat = project.get_material(material_ids[pane]) res = _bisect(lambda x: not set_props(mat, {'transmittance': x}), lambda: _g(con), c['g'], 0.05, 0.95, increasing=True) if res is None: warn('%s: g-value could not be calibrated (VE did not report a BS EN 410 g-value)' % what) G_CALIBRATED[pane] = True # 2. U-value: calibrate the gas gap resistance if gap is None: warn('%s: no gas gap layer found - U-value not calibrated' % what) else: res = _bisect(lambda x: not set_props(gap, {'resistance': x}), lambda: _u(con), target_u - 0.0006, 0.01, 3.0, increasing=False, tol=0.0004) if res is None: warn('%s: U-value could not be calibrated (gap resistance not accepted)' % what) u = _u(con) if not includes_frame: u = (1 - f) * u + f * c['frame_u'] return con, u, _g(con), includes_frame # ----------------------------------------------------------------------------- # Main # ----------------------------------------------------------------------------- def log_path(): try: folder = os.path.dirname(os.path.abspath(__file__)) except NameError: folder = os.getcwd() return os.path.join(folder, LOG_NAME) def main(): say('=' * 78) say('BPEX %s constructions for IES VE, v%s' % (SET_LABEL, SCRIPT_VERSION)) say('=' * 78) todo = [c for c in CONSTRUCTIONS if selected(c)] say('Selected %d of %d constructions.%s' % ( len(todo), len(CONSTRUCTIONS), ' DRY RUN - nothing will change.' if DRY_RUN else '')) if not todo: say('Nothing selected - check IDS.') return project = get_project() existing = existing_constructions_by_name(project) material_ids = existing_bpex_materials(project) rows = [] # [BPEX id, name, VE id, status, U sheet, U VE] names_ok = True to_build = [] for c in todo: ve_id = existing.get(c['name']) if ve_id: con = project.get_construction(ve_id, iesve.construction_class.none) u = con.get_u_factor(iesve.uvalue_types.iso) rows.append([c['id'], c['name'], ve_id, 'already in project (skipped)', c['u'], u]) else: to_build.append(c) needed = [] for c in to_build: for l in c['layers']: if not l['cavity'] and l['mat'] not in material_ids and l['mat'] not in needed: needed.append(l['mat']) if DRY_RUN: for c in to_build: say(' would create %-12s %s' % (c['id'], c['name'])) say('%d already in the project, %d would be created, %d new materials.' % (len(todo) - len(to_build), len(to_build), len(needed))) return made = 0 for code in needed: try: material_ids[code] = create_material(project, code) made += 1 except Exception as e: warn('material %s failed: %s' % (code, e)) say('Materials: %d created, %d BPEX materials already in project.' % (made, len(material_ids) - made)) for c in to_build: missing = [l['mat'] for l in c['layers'] if not l['cavity'] and l['mat'] not in material_ids] if missing: warn('%s skipped - missing materials %s' % (c['id'], missing)) rows.append([c['id'], c['name'], '', 'FAILED (missing materials)', c['u'], None]) continue try: if c.get('glazed'): con, u, g, incl = build_glazed(project, c, material_ids) ok = u <= c['u'] + U_TOLERANCE and abs(u - c['u']) <= 0.02 gtxt = '' if g is None else ' g %.2f' % g status = ('created' if ok else 'CHECK U-value') + gtxt + ('' if incl else ' (U incl. frame calc)') else: con = build(project, c, material_ids) u = con.get_u_factor(iesve.uvalue_types.iso) ok = abs(u - c['u']) <= U_TOLERANCE status = 'created' if ok else 'CHECK U-value' named = try_name(con, c['name']) names_ok = names_ok and named if not named: warn('%s: name not accepted by the VE' % c['id']) rows.append([c['id'], c['name'], con.id, status, c['u'], u]) except Exception as e: warn('%s failed: %s' % (c['id'], e)) rows.append([c['id'], c['name'], '', 'FAILED: %s' % e, c['u'], None]) # Results table say('') say('%-11s %-9s %-7s %-7s %-34s %s' % ('BPEX ID', 'VE ID', 'U BPEX', 'U VE', 'Status', 'Name')) say('-' * 78) for r in rows: say('%-11s %-9s %-7.4f %-7s %-34s %s' % ( r[0], r[2], r[4], '' if r[5] is None else '%.4f' % r[5], r[3], r[1])) say('-' * 78) if not names_ok: say('NOTE: some names were not accepted - use the VE IDs in the table above,') say(' and do not run this script again until they are renamed by hand') say(' (otherwise they will be created twice).') try: with open(log_path(), 'w', newline='', encoding='utf-8') as f: w = csv.writer(f) w.writerow(['BPEX ID', 'BPEX name', 'VE construction ID', 'Status', 'U-value BPEX (W/m2K)', 'U-value VE EN-ISO (W/m2K)']) w.writerows(rows) if WARNINGS: w.writerow([]) w.writerow(['Warnings']) for m in WARNINGS: w.writerow([m]) say('Table saved to', log_path()) except Exception as e: say('Could not save the CSV:', e) say('%d warnings.' % len(WARNINGS) if WARNINGS else 'No warnings.') say('Now save your VE project.') if __name__ == '__main__': main()