openstudio-ee 0.12.3 → 0.12.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. checksums.yaml +4 -4
  2. data/.coverage +0 -0
  3. data/.github/workflows/test-with-openstudio.yml +109 -74
  4. data/.gitignore +21 -0
  5. data/.rubocop.yml +15 -2
  6. data/CHANGELOG.md +6 -0
  7. data/Gemfile +7 -8
  8. data/README.md +3 -2
  9. data/WORKFLOW_CHANGES.md +74 -0
  10. data/lib/measures/AddDaylightSensors/measure.rb +79 -79
  11. data/lib/measures/AddDaylightSensors/measure.xml +4 -4
  12. data/lib/measures/AddOverhangsByProjectionFactor/measure.rb +38 -41
  13. data/lib/measures/AddOverhangsByProjectionFactor/measure.xml +4 -4
  14. data/lib/measures/EnableDemandControlledVentilation/measure.rb +37 -40
  15. data/lib/measures/EnableDemandControlledVentilation/measure.xml +4 -4
  16. data/lib/measures/EnableEconomizerControl/measure.rb +36 -37
  17. data/lib/measures/EnableEconomizerControl/measure.xml +4 -4
  18. data/lib/measures/GLHEProExportLoadsforGroundHeatExchangerSizing/measure.rb +27 -41
  19. data/lib/measures/GLHEProExportLoadsforGroundHeatExchangerSizing/measure.xml +4 -4
  20. data/lib/measures/GLHEProGFunctionImport/measure.rb +11 -15
  21. data/lib/measures/GLHEProGFunctionImport/measure.xml +4 -4
  22. data/lib/measures/GLHEProSetupExportLoadsforGroundHeatExchangerSizing/measure.rb +5 -9
  23. data/lib/measures/GLHEProSetupExportLoadsforGroundHeatExchangerSizing/measure.xml +3 -3
  24. data/lib/measures/ImproveFanBeltEfficiency/measure.rb +78 -95
  25. data/lib/measures/ImproveFanBeltEfficiency/measure.xml +6 -6
  26. data/lib/measures/ImproveMotorEfficiency/measure.rb +75 -100
  27. data/lib/measures/ImproveMotorEfficiency/measure.xml +6 -6
  28. data/lib/measures/IncreaseInsulationRValueForExteriorWalls/measure.rb +137 -130
  29. data/lib/measures/IncreaseInsulationRValueForExteriorWalls/measure.xml +4 -4
  30. data/lib/measures/IncreaseInsulationRValueForExteriorWallsByPercentage/measure.rb +114 -115
  31. data/lib/measures/IncreaseInsulationRValueForExteriorWallsByPercentage/measure.xml +3 -3
  32. data/lib/measures/IncreaseInsulationRValueForRoofs/measure.rb +137 -130
  33. data/lib/measures/IncreaseInsulationRValueForRoofs/measure.xml +4 -4
  34. data/lib/measures/IncreaseInsulationRValueForRoofsByPercentage/measure.rb +114 -115
  35. data/lib/measures/IncreaseInsulationRValueForRoofsByPercentage/measure.xml +3 -3
  36. data/lib/measures/ReduceElectricEquipmentLoadsByPercentage/measure.rb +69 -63
  37. data/lib/measures/ReduceElectricEquipmentLoadsByPercentage/measure.xml +6 -6
  38. data/lib/measures/ReduceLightingLoadsByPercentage/measure.rb +77 -66
  39. data/lib/measures/ReduceLightingLoadsByPercentage/measure.xml +6 -6
  40. data/lib/measures/ReduceNightTimeElectricEquipmentLoads/measure.rb +45 -43
  41. data/lib/measures/ReduceNightTimeElectricEquipmentLoads/measure.xml +4 -4
  42. data/lib/measures/ReduceNightTimeLightingLoads/measure.rb +45 -43
  43. data/lib/measures/ReduceNightTimeLightingLoads/measure.xml +4 -4
  44. data/lib/measures/ReduceSpaceInfiltrationByPercentage/measure.rb +58 -52
  45. data/lib/measures/ReduceSpaceInfiltrationByPercentage/measure.xml +6 -6
  46. data/lib/measures/ReduceVentilationByPercentage/measure.rb +49 -46
  47. data/lib/measures/ReduceVentilationByPercentage/measure.xml +6 -6
  48. data/lib/measures/add_variable_speed_rtu_control_logic/measure.rb +31 -23
  49. data/lib/measures/add_variable_speed_rtu_control_logic/measure.xml +4 -4
  50. data/lib/measures/create_variable_speed_rtu/measure.rb +166 -174
  51. data/lib/measures/create_variable_speed_rtu/measure.xml +6 -6
  52. data/lib/measures/fan_assist_night_ventilation/measure.rb +33 -32
  53. data/lib/measures/fan_assist_night_ventilation/measure.xml +4 -4
  54. data/lib/measures/nze_hvac/measure.rb +72 -62
  55. data/lib/measures/nze_hvac/measure.xml +4 -4
  56. data/lib/measures/replace_water_heater_mixed_with_thermal_storage_chilled_water/measure.rb +16 -19
  57. data/lib/measures/replace_water_heater_mixed_with_thermal_storage_chilled_water/measure.xml +4 -4
  58. data/lib/measures/window_enhancement/LICENSE.md +14 -0
  59. data/lib/measures/window_enhancement/README.md +112 -0
  60. data/lib/measures/window_enhancement/docs/.gitkeep +0 -0
  61. data/lib/measures/window_enhancement/measure.py +386 -0
  62. data/lib/measures/window_enhancement/measure.xml +271 -0
  63. data/lib/measures/window_enhancement/resources/EC3_lookup.py +321 -0
  64. data/lib/measures/window_enhancement/resources/Test_API.py +32 -0
  65. data/lib/measures/window_enhancement/resources/__pycache__/EC3_lookup.cpython-39.pyc +0 -0
  66. data/lib/measures/window_enhancement/resources/__pycache__/Original_EC3_lookup.py +322 -0
  67. data/lib/measures/window_enhancement/resources/__pycache__/Test_API.cpython-39.pyc +0 -0
  68. data/lib/measures/window_enhancement/resources/calculate_perimeter.py +39 -0
  69. data/lib/measures/window_enhancement/test_output.log +39 -0
  70. data/lib/openstudio/ee_measures/version.rb +1 -1
  71. data/openstudio-ee.gemspec +10 -8
  72. data/test-workflow-locally.sh +152 -0
  73. metadata +64 -35
  74. data/Jenkinsfile +0 -11
@@ -0,0 +1,271 @@
1
+ <?xml version="1.0"?>
2
+ <measure>
3
+ <schema_version>3.1</schema_version>
4
+ <name>window_enhancement</name>
5
+ <uid>0ad8e761-8624-4d9d-a7d6-ebf30cce243b</uid>
6
+ <version_id>6e806249-5eab-444e-b724-f00dd42c13fb</version_id>
7
+ <version_modified>2025-09-25T04:50:20Z</version_modified>
8
+ <xml_checksum>7DADF7C5</xml_checksum>
9
+ <class_name>WindowEnhancement</class_name>
10
+ <display_name>Window Enhancement</display_name>
11
+ <description>Make existing window better by adding film, storm window, or something else.</description>
12
+ <modeler_description>Using layered construction and not simple glazing to do this. We have to think about how to address simple glazing with this.</modeler_description>
13
+ <arguments>
14
+ <argument>
15
+ <name>analysis_period</name>
16
+ <display_name>Analysis Period</display_name>
17
+ <description>Analysis period of embodied carbon of building/building assembly</description>
18
+ <type>Integer</type>
19
+ <required>true</required>
20
+ <model_dependent>false</model_dependent>
21
+ <default_value>30</default_value>
22
+ </argument>
23
+ <argument>
24
+ <name>igu_option</name>
25
+ <display_name>IGU Option</display_name>
26
+ <description>Type of insulating glazing unit</description>
27
+ <type>Choice</type>
28
+ <required>true</required>
29
+ <model_dependent>false</model_dependent>
30
+ <choices>
31
+ <choice>
32
+ <value>electrochromic</value>
33
+ <display_name>electrochromic</display_name>
34
+ </choice>
35
+ <choice>
36
+ <value>fire_resistant</value>
37
+ <display_name>fire_resistant</display_name>
38
+ </choice>
39
+ <choice>
40
+ <value>laminated</value>
41
+ <display_name>laminated</display_name>
42
+ </choice>
43
+ <choice>
44
+ <value>low_emissivity</value>
45
+ <display_name>low_emissivity</display_name>
46
+ </choice>
47
+ <choice>
48
+ <value>tempered</value>
49
+ <display_name>tempered</display_name>
50
+ </choice>
51
+ </choices>
52
+ </argument>
53
+ <argument>
54
+ <name>igu_lifetime</name>
55
+ <display_name>IGU Lifetime</display_name>
56
+ <description>Lifetime of the insulating glazing unit</description>
57
+ <type>Integer</type>
58
+ <required>true</required>
59
+ <model_dependent>false</model_dependent>
60
+ <default_value>15</default_value>
61
+ </argument>
62
+ <argument>
63
+ <name>wf_lifetime</name>
64
+ <display_name>Product Lifetime of Window Frame</display_name>
65
+ <description>Life expectancy of window frame</description>
66
+ <type>Integer</type>
67
+ <required>true</required>
68
+ <model_dependent>false</model_dependent>
69
+ <default_value>15</default_value>
70
+ </argument>
71
+ <argument>
72
+ <name>wf_option</name>
73
+ <display_name>Window frame option</display_name>
74
+ <description>Type of aluminum extrusion</description>
75
+ <type>Choice</type>
76
+ <required>true</required>
77
+ <model_dependent>false</model_dependent>
78
+ <choices>
79
+ <choice>
80
+ <value>anodized</value>
81
+ <display_name>anodized</display_name>
82
+ </choice>
83
+ <choice>
84
+ <value>painted</value>
85
+ <display_name>painted</display_name>
86
+ </choice>
87
+ <choice>
88
+ <value>thermally_improved</value>
89
+ <display_name>thermally_improved</display_name>
90
+ </choice>
91
+ </choices>
92
+ </argument>
93
+ <argument>
94
+ <name>frame_cross_section_area</name>
95
+ <display_name>Frame Cross Section Area (m²)</display_name>
96
+ <description>Cross-sectional area of the IGU frame in square meters.</description>
97
+ <type>Double</type>
98
+ <required>true</required>
99
+ <model_dependent>false</model_dependent>
100
+ <default_value>0.0025</default_value>
101
+ </argument>
102
+ <argument>
103
+ <name>epd_type</name>
104
+ <display_name>EPD Type</display_name>
105
+ <description>Type of EPD for searching GWP values, Product EPDs refer to specific products from a manufacturer, while industrial EPDs represent average data across an entire industry sector.</description>
106
+ <type>Choice</type>
107
+ <required>true</required>
108
+ <model_dependent>false</model_dependent>
109
+ <choices>
110
+ <choice>
111
+ <value>Product</value>
112
+ <display_name>Product</display_name>
113
+ </choice>
114
+ <choice>
115
+ <value>Industry</value>
116
+ <display_name>Industry</display_name>
117
+ </choice>
118
+ </choices>
119
+ </argument>
120
+ <argument>
121
+ <name>gwp_statistic</name>
122
+ <display_name>GWP Statistic</display_name>
123
+ <description>Type of GWP statistic for searching GWP values, Product GWP statistics refer to specific products from a manufacturer, while industrial GWP statistics represent average data across an entire industry sector.</description>
124
+ <type>Choice</type>
125
+ <required>true</required>
126
+ <model_dependent>false</model_dependent>
127
+ <choices>
128
+ <choice>
129
+ <value>minimum</value>
130
+ <display_name>minimum</display_name>
131
+ </choice>
132
+ <choice>
133
+ <value>maximum</value>
134
+ <display_name>maximum</display_name>
135
+ </choice>
136
+ <choice>
137
+ <value>mean</value>
138
+ <display_name>mean</display_name>
139
+ </choice>
140
+ <choice>
141
+ <value>median</value>
142
+ <display_name>median</display_name>
143
+ </choice>
144
+ </choices>
145
+ </argument>
146
+ <argument>
147
+ <name>total_embodied_carbon</name>
148
+ <display_name>Total Embodied Carbon (kgCO2e)</display_name>
149
+ <description>Total embodied carbon of the window frame in kilograms of CO2 equivalent.</description>
150
+ <type>Double</type>
151
+ <required>true</required>
152
+ <model_dependent>false</model_dependent>
153
+ <default_value>0.0</default_value>
154
+ </argument>
155
+ <argument>
156
+ <name>total_embodied_carbon</name>
157
+ <display_name>Total Embodied Carbon (kgCO2e)</display_name>
158
+ <description>Total embodied carbon of the window frame in kilograms of CO2 equivalent.</description>
159
+ <type>Double</type>
160
+ <required>true</required>
161
+ <model_dependent>false</model_dependent>
162
+ <default_value>0.0</default_value>
163
+ </argument>
164
+ <argument>
165
+ <name>api_key</name>
166
+ <display_name>API Key</display_name>
167
+ <description>API key for accessing external services.</description>
168
+ <type>String</type>
169
+ <required>true</required>
170
+ <model_dependent>false</model_dependent>
171
+ </argument>
172
+ </arguments>
173
+ <outputs />
174
+ <provenances />
175
+ <tags>
176
+ <tag>Envelope.Fenestration</tag>
177
+ </tags>
178
+ <attributes>
179
+ <attribute>
180
+ <name>Measure Type</name>
181
+ <value>ModelMeasure</value>
182
+ <datatype>string</datatype>
183
+ </attribute>
184
+ <attribute>
185
+ <name>Measure Language</name>
186
+ <value>Python</value>
187
+ <datatype>string</datatype>
188
+ </attribute>
189
+ <attribute>
190
+ <name>Intended Software Tool</name>
191
+ <value>Apply Measure Now</value>
192
+ <datatype>string</datatype>
193
+ </attribute>
194
+ <attribute>
195
+ <name>Intended Software Tool</name>
196
+ <value>OpenStudio Application</value>
197
+ <datatype>string</datatype>
198
+ </attribute>
199
+ <attribute>
200
+ <name>Intended Software Tool</name>
201
+ <value>Parametric Analysis Tool</value>
202
+ <datatype>string</datatype>
203
+ </attribute>
204
+ <attribute>
205
+ <name>Intended Use Case</name>
206
+ <value>Retrofit EE</value>
207
+ <datatype>string</datatype>
208
+ </attribute>
209
+ </attributes>
210
+ <files>
211
+ <file>
212
+ <filename>LICENSE.md</filename>
213
+ <filetype>md</filetype>
214
+ <usage_type>license</usage_type>
215
+ <checksum>FFCBFF29</checksum>
216
+ </file>
217
+ <file>
218
+ <filename>README.md</filename>
219
+ <filetype>md</filetype>
220
+ <usage_type>readme</usage_type>
221
+ <checksum>DE9B0464</checksum>
222
+ </file>
223
+ <file>
224
+ <version>
225
+ <software_program>OpenStudio</software_program>
226
+ <identifier>3.8.0</identifier>
227
+ <min_compatible>3.8.0</min_compatible>
228
+ </version>
229
+ <filename>measure.py</filename>
230
+ <filetype>py</filetype>
231
+ <usage_type>script</usage_type>
232
+ <checksum>3D0A891D</checksum>
233
+ </file>
234
+ <file>
235
+ <filename>EC3_lookup.py</filename>
236
+ <filetype>py</filetype>
237
+ <usage_type>resource</usage_type>
238
+ <checksum>2832F008</checksum>
239
+ </file>
240
+ <file>
241
+ <filename>Test_API.py</filename>
242
+ <filetype>py</filetype>
243
+ <usage_type>resource</usage_type>
244
+ <checksum>26458C20</checksum>
245
+ </file>
246
+ <file>
247
+ <filename>calculate_perimeter.py</filename>
248
+ <filetype>py</filetype>
249
+ <usage_type>resource</usage_type>
250
+ <checksum>D7980CA8</checksum>
251
+ </file>
252
+ <file>
253
+ <filename>example_model.osm</filename>
254
+ <filetype>osm</filetype>
255
+ <usage_type>test</usage_type>
256
+ <checksum>E08CA027</checksum>
257
+ </file>
258
+ <file>
259
+ <filename>example_model_2.osm</filename>
260
+ <filetype>osm</filetype>
261
+ <usage_type>test</usage_type>
262
+ <checksum>E01ECAD1</checksum>
263
+ </file>
264
+ <file>
265
+ <filename>test_window_enhancement.py</filename>
266
+ <filetype>py</filetype>
267
+ <usage_type>test</usage_type>
268
+ <checksum>061487A7</checksum>
269
+ </file>
270
+ </files>
271
+ </measure>
@@ -0,0 +1,321 @@
1
+ # EC3 API Lookup Script
2
+ import json
3
+ import re
4
+ from typing import Dict, Any, List
5
+ from pathlib import Path
6
+ import configparser
7
+ from datetime import datetime
8
+ import numpy as np
9
+ import os
10
+
11
+ script_dir = os.path.dirname(os.path.abspath(__file__))
12
+ repo_root = os.path.abspath(os.path.join(script_dir, "../../../.."))
13
+ config_path = os.path.join(repo_root, "config.ini")
14
+
15
+ # this measure doesn't function without EC3 token and required Python libraries installed
16
+ if not os.path.exists(config_path):
17
+ raise FileNotFoundError(f"Config file not found: {config_path}. Please setup your EC3 token before attempting to run this measure.")
18
+
19
+ # load custom libraries after confirming if config.ini
20
+ import requests
21
+
22
+ config = configparser.ConfigParser()
23
+ config.read(config_path)
24
+ API_TOKEN= config["EC3_API_TOKEN"]["API_TOKEN"]
25
+
26
+ # #find material_name by category
27
+ # material_category = {"concrete":{"ReadyMix","PrecastConcrete","CementGrout","FlowableFill"},
28
+ # "masonry":{"Brick", "CMU"},
29
+ # "steel":{"RebarSteel","WireMeshSteel","ColdFormedSteel","StructuralSteel"},
30
+ # "aluminum":{"AluminiumExtrusions"},
31
+ # "wood":{"PrefabricatedWood","DimensionLumbe","SheathingPanels","CompositeLumber","MassTimber","NonStructuralWood"},
32
+ # "sheathing":{"GypsumSheathingBoard","CementitiousSheathingBoard"},
33
+ # "thermal_moisture_protection":{"Insulation","MembraneRoofing"},
34
+ # "cladding":{"RoofPanels","InsulatedRoofPanels","WallPanels","InsulatedWallPanels"},
35
+ # "openings":{"glazing":["InsulatingGlazingUnits","FlatGlassPanes","ProcessedNonInsulatingGlassPanes"],
36
+ # "extrusions":["AluminiumExtrusions"]},
37
+ # "finishes":{"CementBoard","Gypsum","Tiling","CeilingPanel","Flooring","PaintingAndCoating"}
38
+ # }
39
+ # for testing use, do not delete
40
+ material_category = {
41
+ "test":["InsulatingGlazingUnits"]
42
+ }
43
+
44
+ def generate_url(material_name, endpoint ="materials", page_number=1, page_size=250, jurisdiction="021", date=None, option=None, boolean="yes",
45
+ glass_panes=None, epd_type="Product"):
46
+ '''
47
+ jurisdiction = "021" means Northern America region
48
+ '''
49
+ if date is None:
50
+ date = datetime.today().strftime("%Y-%m-%d") # use today's date as default
51
+ url = (
52
+ f"https://api.buildingtransparency.org/api/{endpoint}"
53
+ f"?page_number={page_number}&page_size={page_size}"
54
+ f"&mf=!EC3%20search(%22{material_name}%22)%20WHERE%20"
55
+ f"%0A%20%20jurisdiction%3A%20IN(%22{jurisdiction}%22)%20AND%0A%20%20"
56
+ f"epd__date_validity_ends%3A%20%3E%20%22{date}%22%20AND%0A%20%20"
57
+ f"epd_types%3A%20IN(%22{epd_type}%20EPDs%22)%20"
58
+ )
59
+ conditions = []
60
+ if option and boolean:
61
+ conditions.append(f"{option}%3A%20{boolean}")
62
+
63
+ if glass_panes:
64
+ conditions.append(f"glass_panes%3A%20%3E~%20{glass_panes}")
65
+
66
+ if conditions:
67
+ url += "AND%0A%20%20" + "%20AND%0A%20%20".join(conditions) + "%20%0A"
68
+
69
+ url += "!pragma%20eMF(%222.0%2F1%22)%2C%20lcia(%22TRACI%202.1%22)"
70
+
71
+ return url
72
+
73
+ def fetch_epd_data(url,api_token):
74
+ """
75
+ input url address generted by generate_url()
76
+ Fetch EPD data from the EC3 API.
77
+ return: Parsed JSON response or empty list on failure.
78
+ """
79
+ try:
80
+ print(f"Fetching data from URL: {url}") # Log the URL being fetched
81
+ # API configuration
82
+ HEADERS = {"Accept": "application/json", "Authorization": "Bearer " + api_token}
83
+ response = requests.get(url, headers=HEADERS, verify=False)
84
+ response.raise_for_status() # HTTPError if failure
85
+ return response.json()
86
+ except requests.exceptions.RequestException as e:
87
+ print(f"Error fetching data from {url}: {e}")
88
+ if 'response' in locals(): # Check if response was defined
89
+ print(f"Response content: {response.text}")
90
+ else:
91
+ print("No response content available.")
92
+ return []
93
+
94
+ def parse_product_epd(epd: Dict[str, Any]) -> Dict[str, Any]:
95
+ """
96
+ Parse GWP data for a given EPD.
97
+ :param epd: EPD dictionary
98
+ :return: Parsed GWP data
99
+ """
100
+ # initialize parameters and dictionary
101
+ parsed_data = {}
102
+ gwp_per_m3 = 0.0
103
+ gwp_per_m2 = 0.0
104
+ gwp_per_kg = 0.0
105
+
106
+ # extract information from EPD's json repsonse
107
+ declared_unit = epd.get("declared_unit")
108
+ thickness = epd.get("thickness")
109
+ gwp_per_declared_unit = epd.get("gwp")
110
+ mass_per_declared_unit = epd.get("mass_per_declared_unit")
111
+ density = epd.get("density")
112
+ gwp_per_kg = epd.get("gwp_per_kg")
113
+ epd_name = epd.get('name')
114
+ description = epd.get('description')
115
+ original_ec3_link = epd['manufacturer']['original_ec3_link']
116
+
117
+ # Per mass
118
+ if gwp_per_kg != None:
119
+ gwp_per_kg = extract_numeric_value(gwp_per_kg)
120
+
121
+ elif gwp_per_kg == None and "t" in declared_unit:
122
+ gwp_per_kg = divide(gwp_per_declared_unit, declared_unit)
123
+ gwp_per_kg = gwp_per_kg/1000 # convert from t to kg
124
+
125
+ elif gwp_per_kg == None and "kg" in declared_unit:
126
+ gwp_per_kg = divide(gwp_per_declared_unit, declared_unit)
127
+
128
+ elif mass_per_declared_unit != None and gwp_per_kg == None and not any(unit in declared_unit for unit in ["kg", "t"]):
129
+ gwp_per_kg = divide(gwp_per_declared_unit, mass_per_declared_unit)
130
+
131
+ # Per volume
132
+ if "m3" in declared_unit:
133
+ gwp_per_m3 = divide(gwp_per_declared_unit, declared_unit)
134
+
135
+ elif "cf" in declared_unit:
136
+ gwp_per_m3 = divide(gwp_per_declared_unit, declared_unit)
137
+ gwp_per_m3 = gwp_per_m3 * 35.3147 # convert from cubic feet to m3
138
+
139
+ elif "m2" in declared_unit and thickness and "mm" in thickness:
140
+ gwp_per_m2 = divide(gwp_per_declared_unit, declared_unit)
141
+ gwp_per_m3 = gwp_per_m2/(extract_numeric_value(thickness)/1000)
142
+
143
+ elif density and "kg / m3" in density and gwp_per_kg:
144
+ gwp_per_m3 = multiply(gwp_per_kg, density)
145
+
146
+ # Per area
147
+ if "m2" in declared_unit:
148
+ gwp_per_m2 = divide(gwp_per_declared_unit, declared_unit)
149
+
150
+ elif "sf" in declared_unit:
151
+ gwp_per_m2 = divide(gwp_per_declared_unit, declared_unit)
152
+ gwp_per_m2 = gwp_per_m2 * 10.7639 # convert from square feet to m2
153
+
154
+ elif "m3" in declared_unit and thickness and "mm" in thickness:
155
+ gwp_per_m3 = divide(gwp_per_declared_unit, declared_unit)
156
+ gwp_per_m2 = gwp_per_m3 * (extract_numeric_value(thickness)/1000)
157
+
158
+ parsed_data["epd_name"] = epd_name
159
+ parsed_data["declared_unit"] = declared_unit
160
+ parsed_data["gwp_per_declared_unit"] = gwp_per_declared_unit
161
+ parsed_data["mass_per_declared_unit"] = mass_per_declared_unit
162
+ parsed_data["thickness"] = thickness
163
+ parsed_data["density"] = density
164
+ parsed_data["gwp_per_m3 (kg CO2 eq/m3)"] = gwp_per_m3
165
+ parsed_data["gwp_per_m2 (kg CO2 eq/m2)"] = gwp_per_m2
166
+ parsed_data["gwp_per_kg (kg CO2 eq/kg)"] = gwp_per_kg
167
+ parsed_data["original_ec3_link"] = original_ec3_link
168
+ parsed_data["description"] = description
169
+
170
+ return parsed_data
171
+
172
+ def parse_industrial_epd(epd: Dict[str, Any]) -> Dict[str, Any]:
173
+ """
174
+ Parse GWP data for a given EPD.
175
+ :param epd: EPD dictionary
176
+ :return: Parsed GWP data
177
+ """
178
+
179
+ parsed_data = {}
180
+ gwp_per_m3 = 0.0
181
+ gwp_per_m2 = 0.0
182
+ gwp_per_kg = 0.0
183
+
184
+ declared_unit = epd.get("declared_unit")
185
+ gwp_per_kg = epd.get("gwp_per_kg")
186
+ gwp_per_declared_unit = epd.get("gwp")
187
+ epd_name = epd.get('name')
188
+ original_ec3_link = epd.get('original_ec3_link')
189
+ description = epd.get('description')
190
+ density_min = epd.get('density_min')
191
+ density_max = epd.get('density_max')
192
+ area = epd.get('area')
193
+
194
+ if density_min is not None and density_max is not None:
195
+ density_avg = (extract_numeric_value(density_max) + extract_numeric_value(density_min))/2
196
+ elif density_min is not None:
197
+ density_avg = extract_numeric_value(density_min)
198
+ elif density_max is not None:
199
+ density_avg = extract_numeric_value(density_max)
200
+ else:
201
+ density_avg = None
202
+
203
+ # Per area (stop using this becasue the area value is not sensible)
204
+ # if "m^2" in area:
205
+ # gwp_per_m2 = extract_numeric_value(gwp_per_declared_unit)/extract_numeric_value(area)
206
+
207
+ # Per mass
208
+ if "t" in declared_unit:
209
+ gwp_per_kg = divide(gwp_per_declared_unit, declared_unit)
210
+ gwp_per_kg = gwp_per_kg / 1000 # convert to kg
211
+ elif "kg" in declared_unit:
212
+ gwp_per_kg = divide(gwp_per_declared_unit, declared_unit)
213
+
214
+ # Per volume
215
+ if gwp_per_kg != None and density_avg != None:
216
+ gwp_per_m3 = gwp_per_kg * density_avg
217
+
218
+ parsed_data["epd_name"] = epd_name
219
+ parsed_data["declared_unit"] = declared_unit
220
+ parsed_data["gwp_per_declared_unit"] = gwp_per_declared_unit
221
+ parsed_data["density_min"] = density_min
222
+ parsed_data["density_max"] = density_max
223
+ parsed_data["area"] = area
224
+ parsed_data["gwp_per_m3 (kg CO2 eq/m3)"] = gwp_per_m3
225
+ parsed_data["gwp_per_m2 (kg CO2 eq/m2)"] = gwp_per_m2
226
+ parsed_data["gwp_per_kg (kg CO2 eq/kg)"] = gwp_per_kg
227
+ parsed_data["original_ec3_link"] = original_ec3_link
228
+ parsed_data["description"] = description
229
+
230
+ return parsed_data
231
+
232
+ def extract_numeric_value(value: Any) -> float:
233
+ """
234
+ Extract numeric value from a string or number.
235
+ :param value: Value to process
236
+ :return: Extracted numeric value
237
+ """
238
+ match = re.search(r"[-+]?\d*\.?\d+", str(value))
239
+ return float(match.group()) if match else 0.0
240
+
241
+ # extract numeric values then divide
242
+ def divide(member: Any, denominator: Any) -> float:
243
+ try:
244
+ member_value = extract_numeric_value(member)
245
+ denominator_value = extract_numeric_value(denominator)
246
+ return round(member_value/denominator_value, 2)
247
+ except ZeroDivisionError:
248
+ return 0.0
249
+
250
+ # extract numeric values then multiply
251
+ def multiply(multiplicand: Any, multiplier: Any) -> float:
252
+
253
+ multiplicand_value = extract_numeric_value(multiplicand)
254
+ multiplier_value = extract_numeric_value(multiplier)
255
+ return round(multiplicand_value * multiplier_value, 2)
256
+
257
+ def calculate_geometry(self, sub_surface):
258
+ """
259
+ Calculate the length, width, perimeter, and area of the window from its vertices.
260
+ Assumes the window is a quadrilateral (typically a rectangle).
261
+ """
262
+ vertices = sub_surface.vertices()
263
+ if len(vertices) != 4:
264
+ return {
265
+ "length": 0.0,
266
+ "width": 0.0,
267
+ "perimeter": 0.0,
268
+ "area": 0.0
269
+ }
270
+
271
+ # Calculate all edge lengths
272
+ edge_lengths = []
273
+ perimeter = 0.0
274
+ for i in range(4):
275
+ v1 = vertices[i]
276
+ v2 = vertices[(i + 1) % 4]
277
+ edge = v2 - v1
278
+ length = edge.length()
279
+ edge_lengths.append(length)
280
+ perimeter += length
281
+
282
+ # Assume opposite edges are equal, so we can group into two unique lengths
283
+ length = max(edge_lengths)
284
+ width = min(edge_lengths)
285
+
286
+ # Area = length × width
287
+ area = length * width
288
+
289
+ return {
290
+ "length": length,
291
+ "width": width,
292
+ "perimeter": perimeter,
293
+ "area": area
294
+ }
295
+
296
+ def main():
297
+ """
298
+ Main function to execute the script.
299
+ """
300
+ print("Fetching EC3 EPD data...")
301
+
302
+ for category, list in material_category.items():
303
+ print(material_category[category])
304
+ for name in list:
305
+ print(name)
306
+ product_url = generate_url(name, endpoint="materials", epd_type="Product")
307
+ industry_url = generate_url(name, endpoint="industry_epds", epd_type="Industry")
308
+
309
+ product_epd_data = fetch_epd_data(product_url,API_TOKEN)
310
+ industrial_epd_data = fetch_epd_data(industry_url,API_TOKEN)
311
+ print(f"Number of product EPDs for {name}: {len(product_epd_data)}")
312
+ print(f"Number of industrial EPDs for {name}: {len(industrial_epd_data)}")
313
+ for idx, epd in enumerate(product_epd_data, start=1):
314
+ parsed_data = parse_product_epd(epd)
315
+ print(f"Product EPD #{idx}: {json.dumps(parsed_data, indent=4)}")
316
+ for idx, epd in enumerate(industrial_epd_data, start=1):
317
+ parsed_data = parse_industrial_epd(epd)
318
+ print(f"Industrial EPD #{idx}: {json.dumps(parsed_data, indent=4)}")
319
+
320
+ if __name__ == "__main__":
321
+ main()
@@ -0,0 +1,32 @@
1
+ # Test EC3 API Call
2
+ import requests
3
+ import pprint
4
+ import os
5
+ import configparser
6
+
7
+ script_dir = os.path.dirname(os.path.abspath(__file__))
8
+ repo_root = os.path.abspath(os.path.join(script_dir, "../../../.."))
9
+ config_path = os.path.join(repo_root, "config.ini")
10
+
11
+ if not os.path.exists(config_path):
12
+ raise FileNotFoundError(f"Config file not found: {config_path}")
13
+
14
+ config = configparser.ConfigParser()
15
+ config.read(config_path)
16
+ API_TOKEN= config["EC3_API_TOKEN"]["API_TOKEN"]
17
+
18
+ test_url = ("https://api.buildingtransparency.org/api/materials?page_number=1&page_size=25&mf=!EC3%20search(%22InsulatingGlazingUnits%22)%20WHERE%20%0A%20%20jurisdiction%3A%20IN(%22021%22)%20AND%0A%20%20epd__date_validity_ends%3A%20%3E%20%222025-04-18%22%20AND%0A%20%20epd_types%3A%20IN(%22Product%20EPDs%22)%20%0A!pragma%20eMF(%222.0%2F1%22)%2C%20lcia(%22TRACI%202.1%22)")
19
+ # Headers for the request
20
+ headers = {
21
+ "Accept": "application/json",
22
+ "Authorization": "Bearer "+ API_TOKEN
23
+ }
24
+
25
+ # Execute the GET request
26
+ test_response = requests.get(test_url, headers=headers, verify=False)
27
+
28
+ # Parse the JSON response
29
+ test_response = test_response.json()
30
+ pprint.pp(test_response)
31
+ # Print the response and the number of EPDs
32
+ print(f"Number of EPDs: {len(test_response)}")