openstudio-ee 0.12.0 → 0.12.4
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.
- checksums.yaml +4 -4
- data/.coverage +0 -0
- data/.github/workflows/test-with-openstudio.yml +109 -74
- data/.gitignore +21 -0
- data/.rubocop.yml +15 -2
- data/CHANGELOG.md +12 -0
- data/Gemfile +7 -8
- data/README.md +5 -1
- data/WORKFLOW_CHANGES.md +74 -0
- data/doc_templates/LICENSE.md +1 -1
- data/lib/measures/AddDaylightSensors/measure.rb +79 -79
- data/lib/measures/AddDaylightSensors/measure.xml +5 -5
- data/lib/measures/AddOverhangsByProjectionFactor/measure.rb +38 -41
- data/lib/measures/AddOverhangsByProjectionFactor/measure.xml +5 -5
- data/lib/measures/EnableDemandControlledVentilation/measure.rb +37 -40
- data/lib/measures/EnableDemandControlledVentilation/measure.xml +5 -5
- data/lib/measures/EnableEconomizerControl/measure.rb +36 -37
- data/lib/measures/EnableEconomizerControl/measure.xml +5 -5
- data/lib/measures/GLHEProExportLoadsforGroundHeatExchangerSizing/measure.rb +27 -41
- data/lib/measures/GLHEProExportLoadsforGroundHeatExchangerSizing/measure.xml +5 -5
- data/lib/measures/GLHEProGFunctionImport/measure.rb +11 -15
- data/lib/measures/GLHEProGFunctionImport/measure.xml +5 -5
- data/lib/measures/GLHEProSetupExportLoadsforGroundHeatExchangerSizing/measure.rb +5 -9
- data/lib/measures/GLHEProSetupExportLoadsforGroundHeatExchangerSizing/measure.xml +4 -4
- data/lib/measures/ImproveFanBeltEfficiency/measure.rb +78 -95
- data/lib/measures/ImproveFanBeltEfficiency/measure.xml +7 -7
- data/lib/measures/ImproveMotorEfficiency/measure.rb +75 -100
- data/lib/measures/ImproveMotorEfficiency/measure.xml +7 -7
- data/lib/measures/IncreaseInsulationRValueForExteriorWalls/measure.rb +137 -130
- data/lib/measures/IncreaseInsulationRValueForExteriorWalls/measure.xml +5 -5
- data/lib/measures/IncreaseInsulationRValueForExteriorWallsByPercentage/measure.rb +114 -115
- data/lib/measures/IncreaseInsulationRValueForExteriorWallsByPercentage/measure.xml +4 -4
- data/lib/measures/IncreaseInsulationRValueForRoofs/measure.rb +137 -130
- data/lib/measures/IncreaseInsulationRValueForRoofs/measure.xml +5 -5
- data/lib/measures/IncreaseInsulationRValueForRoofsByPercentage/measure.rb +114 -115
- data/lib/measures/IncreaseInsulationRValueForRoofsByPercentage/measure.xml +4 -4
- data/lib/measures/ReduceElectricEquipmentLoadsByPercentage/measure.rb +69 -63
- data/lib/measures/ReduceElectricEquipmentLoadsByPercentage/measure.xml +7 -7
- data/lib/measures/ReduceLightingLoadsByPercentage/measure.rb +77 -66
- data/lib/measures/ReduceLightingLoadsByPercentage/measure.xml +7 -7
- data/lib/measures/ReduceNightTimeElectricEquipmentLoads/measure.rb +45 -43
- data/lib/measures/ReduceNightTimeElectricEquipmentLoads/measure.xml +5 -5
- data/lib/measures/ReduceNightTimeLightingLoads/measure.rb +45 -43
- data/lib/measures/ReduceNightTimeLightingLoads/measure.xml +5 -5
- data/lib/measures/ReduceSpaceInfiltrationByPercentage/measure.rb +58 -52
- data/lib/measures/ReduceSpaceInfiltrationByPercentage/measure.xml +7 -7
- data/lib/measures/ReduceVentilationByPercentage/measure.rb +49 -46
- data/lib/measures/ReduceVentilationByPercentage/measure.xml +7 -7
- data/lib/measures/add_variable_speed_rtu_control_logic/measure.rb +31 -23
- data/lib/measures/add_variable_speed_rtu_control_logic/measure.xml +5 -5
- data/lib/measures/create_variable_speed_rtu/measure.rb +166 -174
- data/lib/measures/create_variable_speed_rtu/measure.xml +7 -7
- data/lib/measures/fan_assist_night_ventilation/measure.rb +33 -32
- data/lib/measures/fan_assist_night_ventilation/measure.xml +5 -5
- data/lib/measures/nze_hvac/measure.rb +72 -62
- data/lib/measures/nze_hvac/measure.xml +5 -5
- data/lib/measures/replace_water_heater_mixed_with_thermal_storage_chilled_water/measure.rb +16 -19
- data/lib/measures/replace_water_heater_mixed_with_thermal_storage_chilled_water/measure.xml +5 -5
- data/lib/measures/window_enhancement/LICENSE.md +14 -0
- data/lib/measures/window_enhancement/README.md +112 -0
- data/lib/measures/window_enhancement/docs/.gitkeep +0 -0
- data/lib/measures/window_enhancement/measure.py +386 -0
- data/lib/measures/window_enhancement/measure.xml +128 -0
- data/lib/measures/window_enhancement/resources/EC3_lookup.py +321 -0
- data/lib/measures/window_enhancement/resources/Test_API.py +32 -0
- data/lib/measures/window_enhancement/resources/__pycache__/EC3_lookup.cpython-39.pyc +0 -0
- data/lib/measures/window_enhancement/resources/__pycache__/Original_EC3_lookup.py +322 -0
- data/lib/measures/window_enhancement/resources/__pycache__/Test_API.cpython-39.pyc +0 -0
- data/lib/measures/window_enhancement/resources/calculate_perimeter.py +39 -0
- data/lib/measures/window_enhancement/test_output.log +39 -0
- data/lib/openstudio/ee_measures/version.rb +1 -1
- data/openstudio-ee.gemspec +11 -9
- data/test-workflow-locally.sh +152 -0
- metadata +66 -37
- data/Jenkinsfile +0 -11
@@ -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)}")
|
Binary file
|
@@ -0,0 +1,322 @@
|
|
1
|
+
# EC3 API Call
|
2
|
+
import requests
|
3
|
+
import json
|
4
|
+
import re
|
5
|
+
import pprint
|
6
|
+
import statistics
|
7
|
+
|
8
|
+
print("EC3 API Call:")
|
9
|
+
|
10
|
+
EC3_category_array = {
|
11
|
+
"ConstructionMaterials": {
|
12
|
+
"Concrete": [
|
13
|
+
"ReadyMix",
|
14
|
+
"Shotcrete",
|
15
|
+
"CementGrout",
|
16
|
+
"FlowableFill",
|
17
|
+
"Precast",
|
18
|
+
"Rebar",
|
19
|
+
"WireMeshSteel",
|
20
|
+
"SCM",
|
21
|
+
"Cement",
|
22
|
+
"Aggregates"
|
23
|
+
],
|
24
|
+
"Masonry": [
|
25
|
+
"Brick",
|
26
|
+
"ConcreteUnitMasonry",
|
27
|
+
"Mortar",
|
28
|
+
"Cementitious",
|
29
|
+
"Aggregates"
|
30
|
+
],
|
31
|
+
"Steel": [],
|
32
|
+
"Aluminum": [],
|
33
|
+
"Wood": [],
|
34
|
+
"Sheathing": [],
|
35
|
+
"ThermalMoistureProtection": [],
|
36
|
+
"Cladding": [],
|
37
|
+
"Openings": [],
|
38
|
+
"Finishes": [],
|
39
|
+
"ConveyingEquipment": [],
|
40
|
+
"NetworkInfrastrucure": [],
|
41
|
+
"Asphalt": [],
|
42
|
+
"Accessories": [],
|
43
|
+
"ManufacturingInputs": [],
|
44
|
+
"BulkMaterials": [],
|
45
|
+
"Placeholders": []
|
46
|
+
},
|
47
|
+
"BuildingAssemblies": {
|
48
|
+
"NewCoustomAssembly": [],
|
49
|
+
"ReinforcedConcrete": [],
|
50
|
+
"Walls": [],
|
51
|
+
"Floors": [],
|
52
|
+
"GlazingFenesration": [],
|
53
|
+
},
|
54
|
+
}
|
55
|
+
|
56
|
+
# Define the URL with query parameters (FIXME: Not including API key in public repo. Will need to be included in a gitignored config.ini file)
|
57
|
+
windows_url = (
|
58
|
+
"https://api.buildingtransparency.org/api/epds"
|
59
|
+
"?page_number=1&page_size=25&fields=id%2Copen_xpd_uuid%2Cis_failed%2Cfailures%2Cerrors%2Cwarnings"
|
60
|
+
"%2Cdate_validity_ends%2Ccqd_sync_unlocked%2Cmy_capabilities%2Coriginal_data_format%2Ccategory"
|
61
|
+
"%2Cdisplay_name%2Cmanufacturer%2Cplant_or_group%2Cname%2Cdescription%2Cprogram_operator"
|
62
|
+
"%2Cprogram_operator_fkey%2Cverifier%2Cdeveloper%2Cmatched_plants_count%2Cplant_geography%2Cpcr"
|
63
|
+
"%2Cshort_name%2Cversion%2Cdate_of_issue%2Clanguage%2Cgwp%2Cuncertainty_adjusted_gwp%2Cdeclared_unit"
|
64
|
+
"%2Cupdated_on%2Ccorrections_count%2Cdeclaration_type%2Cbox_id%2Cis_downloadable"
|
65
|
+
"&sort_by=-updated_on&name__like=window&description__like=window&q=windows&plant_geography=US&declaration_type=Product%20EPD"
|
66
|
+
)
|
67
|
+
|
68
|
+
igu_url = (
|
69
|
+
"https://api.buildingtransparency.org/api/materials"
|
70
|
+
"?page_number=1&page_size=100"
|
71
|
+
"&mf=!EC3%20search(%22InsulatingGlazingUnits%22)%20WHERE%20"
|
72
|
+
"%0A%20%20jurisdiction%3A%20IN(%22021%22)%20AND%0A%20%20"
|
73
|
+
"epd__date_validity_ends%3A%20%3E%20%222024-12-05%22%20AND%0A%20%20"
|
74
|
+
"epd_types%3A%20IN(%22Product%20EPDs%22)%20"
|
75
|
+
"!pragma%20eMF(%222.0%2F1%22)%2C%20lcia(%22TRACI%202.1%22)"
|
76
|
+
)
|
77
|
+
|
78
|
+
wframe_url = (
|
79
|
+
"https://api.buildingtransparency.org/api/materials"
|
80
|
+
"?page_number=1&page_size=25"
|
81
|
+
"&mf=!EC3%20search(%22AluminiumExtrusions%22)%20WHERE%20"
|
82
|
+
"%0A%20%20jurisdiction%3A%20IN(%22021%22)%20AND%0A%20%20"
|
83
|
+
"epd__date_validity_ends%3A%20%3E%20%222024-12-09%22%20AND%0A%20%20"
|
84
|
+
"epd_types%3A%20IN(%22Product%20EPDs%22)%20"
|
85
|
+
"!pragma%20eMF(%222.0%2F1%22)%2C%20lcia(%22TRACI%202.1%22)"
|
86
|
+
)
|
87
|
+
|
88
|
+
|
89
|
+
gypsum_url = (
|
90
|
+
"https://api.buildingtransparency.org/api/materials"
|
91
|
+
"?page_number=1&page_size=25"
|
92
|
+
"&mf=!EC3%20search(%22Gypsum%22)%20WHERE%20"
|
93
|
+
"%0A%20%20jurisdiction%3A%20IN(%22US%22%2C%20%22CA%22)%20AND%0A%20%20"
|
94
|
+
"epd__date_validity_ends%3A%20%3E%20%222024-12-24%22%20AND%0A%20%20"
|
95
|
+
"epd_types%3A%20IN(%22Product%20EPDs%22)%20AND%0A%20%20"
|
96
|
+
"gypsum_fire_rating%3A%20IN(%22X%22)%20AND%0A%20%20"
|
97
|
+
"gypsum_thickness%3A%20IN(%220.625%20in%22)%20AND%0A%20%20"
|
98
|
+
"gypsum_facing%3A%20IN(%22Paper%22)%20"
|
99
|
+
"!pragma%20eMF(%222.0%2F1%22)%2C%20lcia(%22TRACI%202.1%22)"
|
100
|
+
)
|
101
|
+
|
102
|
+
# Headers for the request
|
103
|
+
headers = {
|
104
|
+
"Accept": "application/json",
|
105
|
+
"Authorization": "Bearer 5fk7wP4cJg6pcmx6ncZN0ftMdoVR8u"
|
106
|
+
}
|
107
|
+
|
108
|
+
# Execute the GET request
|
109
|
+
igu_response = requests.get(igu_url, headers=headers, verify=False)
|
110
|
+
wframe_response = requests.get(wframe_url, headers=headers, verify=False)
|
111
|
+
|
112
|
+
# Parse the JSON response
|
113
|
+
igu_response = igu_response.json()
|
114
|
+
wframe_response = wframe_response.json()
|
115
|
+
#print(igu_response)
|
116
|
+
# Print the response and the number of EPDs
|
117
|
+
print(f"Number of EPDs for IGU: {len(igu_response)}")
|
118
|
+
print(f"Number of EPDs for window frame: {len(wframe_response)}")
|
119
|
+
|
120
|
+
# Exception for EPD: 0
|
121
|
+
if len(igu_response) == 0:
|
122
|
+
print("No EPD returned for IGU, please change the search terms")
|
123
|
+
if len(wframe_response) == 0:
|
124
|
+
print("No EPD returned for window frame, please change the search terms")
|
125
|
+
|
126
|
+
# List of keys to exclude
|
127
|
+
exclude_keys = [
|
128
|
+
'manufacturer',
|
129
|
+
'plant_or_group',
|
130
|
+
'category',
|
131
|
+
'created_on',
|
132
|
+
'updated_on',
|
133
|
+
'gwp',
|
134
|
+
'gwp_z',
|
135
|
+
'pct80_gwp_per_category_declared_unit',
|
136
|
+
'conservative_estimate',
|
137
|
+
'best_practice',
|
138
|
+
'standard_deviation',
|
139
|
+
'uncertainty_factor',
|
140
|
+
'uncertainty_adjusted_gwp',
|
141
|
+
'lowest_plausible_gwp',
|
142
|
+
]
|
143
|
+
|
144
|
+
# Create an empty list to store EPDs of IGU
|
145
|
+
igu_epd_data = {}
|
146
|
+
igu_gwp_per_volume = []
|
147
|
+
igu_gwp_per_mass = []
|
148
|
+
igu_gwp_per_area = []
|
149
|
+
wframe_epd_data = {}
|
150
|
+
wframe_gwp_per_volume = []
|
151
|
+
wframe_gwp_per_mass = []
|
152
|
+
wframe_gwp_per_area = []
|
153
|
+
|
154
|
+
for igu_epd_no, igu_epd in enumerate(igu_response, start=1):
|
155
|
+
print(f"========================================EPD No. {igu_epd_no}:==========================================")
|
156
|
+
# Initialize gwp per unit volume of IGU
|
157
|
+
gwp_per_unit_volume = "Not specified"
|
158
|
+
gwp_per_unit_area = "Not specified"
|
159
|
+
gwp_per_unit_mass = "Not specified"
|
160
|
+
# Create an empty list to store each key,value pair in an individual EPD object
|
161
|
+
igu_object = {}
|
162
|
+
|
163
|
+
for key, value in igu_epd.items():
|
164
|
+
if value is not None and key not in exclude_keys:
|
165
|
+
# Append the key-value pair to the list
|
166
|
+
igu_object[key] = value
|
167
|
+
# Extract the relevant keys
|
168
|
+
declared_unit = igu_epd.get("declared_unit")
|
169
|
+
thickness_unit = igu_epd.get("thickness")
|
170
|
+
gwp_per_category_declared_unit = igu_epd.get("gwp_per_category_declared_unit")
|
171
|
+
warnings = igu_epd.get("warnings")
|
172
|
+
mass_per_declared_unit = igu_epd.get("mass_per_declared_unit")
|
173
|
+
density_unit = igu_epd.get("density")
|
174
|
+
gwp_per_unit_mass = igu_epd.get("gwp_per_kg")
|
175
|
+
|
176
|
+
# Per volume
|
177
|
+
if declared_unit and "m3" in declared_unit:
|
178
|
+
gwp_per_category_declared_unit_value = float(re.search(r"[-+]?\d*\.?\d+", str(gwp_per_category_declared_unit)).group())
|
179
|
+
declared_unit_value = float(re.search(r"[-+]?\d*\.?\d+", str(declared_unit)).group())
|
180
|
+
gwp_per_unit_volume = gwp_per_category_declared_unit_value/declared_unit_value
|
181
|
+
|
182
|
+
elif declared_unit and "m2" in declared_unit and thickness_unit and "mm" in str(thickness_unit):
|
183
|
+
thickness = float(re.search(r"[-+]?\d*\.?\d+", thickness_unit).group()) # Extract numeric part
|
184
|
+
gwp_per_category_declared_unit_value = float(re.search(r"[-+]?\d*\.?\d+", str(gwp_per_category_declared_unit)).group())
|
185
|
+
thickness = thickness * 1e-3 # Convert thickness to meters
|
186
|
+
gwp_per_unit_volume = round(gwp_per_category_declared_unit_value / thickness,2)
|
187
|
+
|
188
|
+
elif density_unit and gwp_per_unit_mass:
|
189
|
+
density = float(re.search(r"[-+]?\d*\.?\d+", density_unit).group()) # Extract numeric part
|
190
|
+
gwp_per_unit_mass = float(re.search(r"[-+]?\d*\.?\d+", str(gwp_per_unit_mass)).group())
|
191
|
+
gwp_per_unit_volume = gwp_per_unit_mass*density
|
192
|
+
gwp_per_unit_volume = round(gwp_per_unit_volume,2)
|
193
|
+
|
194
|
+
elif declared_unit and "t" in declared_unit and density_unit:
|
195
|
+
density = float(re.search(r"[-+]?\d*\.?\d+", density_unit).group()) # Extract numeric part
|
196
|
+
gwp_per_category_declared_unit_value = float(re.search(r"[-+]?\d*\.?\d+", str(gwp_per_category_declared_unit)).group())
|
197
|
+
gwp_per_unit_volume = (gwp_per_category_declared_unit_value / 1000) * density
|
198
|
+
gwp_per_unit_volume = round(gwp_per_unit_volume,2)
|
199
|
+
|
200
|
+
# Per area
|
201
|
+
if declared_unit and "sf" in declared_unit:
|
202
|
+
gwp_per_category_declared_unit_value = float(re.search(r"[-+]?\d*\.?\d+", str(gwp_per_category_declared_unit)).group())
|
203
|
+
declared_unit_value = float(re.search(r"[-+]?\d*\.?\d+", str(declared_unit)).group())
|
204
|
+
gwp_per_unit_area = (gwp_per_category_declared_unit_value/declared_unit_value)/0.092903
|
205
|
+
gwp_per_unit_area = round(gwp_per_unit_area,2)
|
206
|
+
|
207
|
+
igu_object["gwp_per_unit_volume"] = f"{gwp_per_unit_volume} kg CO2 e/m3"
|
208
|
+
|
209
|
+
if declared_unit and "m2" in declared_unit:
|
210
|
+
gwp_per_category_declared_unit_value = float(re.search(r"[-+]?\d*\.?\d+", str(gwp_per_category_declared_unit)).group())
|
211
|
+
igu_object["gwp_per_unit_area"] = f"{gwp_per_category_declared_unit_value} kg CO2 e/m2"
|
212
|
+
else:
|
213
|
+
igu_object["gwp_per_unit_area"] = f"{gwp_per_unit_area} kg CO2 e/m2"
|
214
|
+
|
215
|
+
if gwp_per_unit_mass:
|
216
|
+
gwp_per_unit_mass = float(re.search(r"[-+]?\d*\.?\d+", str(gwp_per_unit_mass)).group())
|
217
|
+
gwp_per_unit_mass = round(gwp_per_unit_mass,2)
|
218
|
+
igu_object["gwp_per_unit_mass"] = f"{gwp_per_unit_mass} kg CO2 e/kg"
|
219
|
+
else:
|
220
|
+
igu_object["gwp_per_unit_mass"] = "Not avaialble in this EPD"
|
221
|
+
object_key = "object" + str(igu_epd_no)
|
222
|
+
igu_epd_data[object_key] = igu_object
|
223
|
+
igu_gwp_per_volume.append(gwp_per_unit_volume)
|
224
|
+
igu_gwp_per_area.append(gwp_per_unit_area)
|
225
|
+
igu_gwp_per_mass.append(gwp_per_unit_mass)
|
226
|
+
pprint.pp(igu_object)
|
227
|
+
#print all gwp per volume
|
228
|
+
igu_gwp_per_volume = [float(item) for item in igu_gwp_per_volume if isinstance(item, (int, float)) or item.replace('.', '', 1).isdigit()]
|
229
|
+
# Calculate the mean
|
230
|
+
if igu_gwp_per_volume: # Check if the list is not empty
|
231
|
+
mean_value = statistics.mean(igu_gwp_per_volume)
|
232
|
+
print("Mean of GWP per volume:", mean_value)
|
233
|
+
else:
|
234
|
+
print("No numeric values to calculate mean.")
|
235
|
+
print(igu_gwp_per_volume)
|
236
|
+
|
237
|
+
for wframe_epd_no, wframe_epd in enumerate(wframe_response, start=1):
|
238
|
+
print(f"========================================EPD No. {wframe_epd_no}:==========================================")
|
239
|
+
# Initialize gwp per unit volume of window frame
|
240
|
+
gwp_per_unit_volume = "Not specified"
|
241
|
+
gwp_per_unit_area = "Not specified"
|
242
|
+
gwp_per_unit_mass = "Not specified"
|
243
|
+
# Create an empty list to store each key,value pair in an individual EPD object
|
244
|
+
wframe_object = {}
|
245
|
+
|
246
|
+
for key, value in wframe_epd.items():
|
247
|
+
if value is not None and key not in exclude_keys:
|
248
|
+
# Append the key-value pair to the list
|
249
|
+
wframe_object[key] = value
|
250
|
+
# Extract the relevant keys
|
251
|
+
declared_unit = wframe_epd.get("declared_unit")
|
252
|
+
thickness_unit = wframe_epd.get("thickness")
|
253
|
+
gwp_per_category_declared_unit = wframe_epd.get("gwp_per_category_declared_unit")
|
254
|
+
warnings = wframe_epd.get("warnings")
|
255
|
+
mass_per_declared_unit = wframe_epd.get("mass_per_declared_unit")
|
256
|
+
density_unit = wframe_epd.get("density")
|
257
|
+
gwp_per_unit_mass = wframe_epd.get("gwp_per_kg")
|
258
|
+
|
259
|
+
# Per volume
|
260
|
+
if declared_unit and "m3" in declared_unit:
|
261
|
+
gwp_per_category_declared_unit_value = float(re.search(r"[-+]?\d*\.?\d+", str(gwp_per_category_declared_unit)).group())
|
262
|
+
declared_unit_value = float(re.search(r"[-+]?\d*\.?\d+", str(declared_unit)).group())
|
263
|
+
gwp_per_unit_volume = gwp_per_category_declared_unit_value/declared_unit_value
|
264
|
+
|
265
|
+
elif declared_unit and "m2" in declared_unit and thickness_unit and "mm" in str(thickness_unit):
|
266
|
+
thickness = float(re.search(r"[-+]?\d*\.?\d+", thickness_unit).group()) # Extract numeric part
|
267
|
+
gwp_per_category_declared_unit_value = float(re.search(r"[-+]?\d*\.?\d+", str(gwp_per_category_declared_unit)).group())
|
268
|
+
thickness = thickness * 1e-3 # Convert thickness to meters
|
269
|
+
gwp_per_unit_volume = round(gwp_per_category_declared_unit_value / thickness,2)
|
270
|
+
|
271
|
+
elif density_unit and gwp_per_unit_mass:
|
272
|
+
density = float(re.search(r"[-+]?\d*\.?\d+", density_unit).group()) # Extract numeric part
|
273
|
+
gwp_per_unit_mass = float(re.search(r"[-+]?\d*\.?\d+", str(gwp_per_unit_mass)).group())
|
274
|
+
gwp_per_unit_volume = gwp_per_unit_mass*density
|
275
|
+
gwp_per_unit_volume = round(gwp_per_unit_volume,2)
|
276
|
+
|
277
|
+
elif declared_unit and "t" in declared_unit and density_unit:
|
278
|
+
density = float(re.search(r"[-+]?\d*\.?\d+", density_unit).group()) # Extract numeric part
|
279
|
+
gwp_per_category_declared_unit_value = float(re.search(r"[-+]?\d*\.?\d+", str(gwp_per_category_declared_unit)).group())
|
280
|
+
gwp_per_unit_volume = (gwp_per_category_declared_unit_value / 1000) * density
|
281
|
+
gwp_per_unit_volume = round(gwp_per_unit_volume,2)
|
282
|
+
|
283
|
+
# Per area
|
284
|
+
if declared_unit and "sf" in declared_unit:
|
285
|
+
gwp_per_category_declared_unit_value = float(re.search(r"[-+]?\d*\.?\d+", str(gwp_per_category_declared_unit)).group())
|
286
|
+
declared_unit_value = float(re.search(r"[-+]?\d*\.?\d+", str(declared_unit)).group())
|
287
|
+
gwp_per_unit_area = (gwp_per_category_declared_unit_value/declared_unit_value)/0.092903
|
288
|
+
gwp_per_unit_area = round(gwp_per_unit_area,2)
|
289
|
+
|
290
|
+
wframe_object["gwp_per_unit_volume"] = f"{gwp_per_unit_volume} kg CO2 e/m3"
|
291
|
+
|
292
|
+
if declared_unit and "m2" in declared_unit:
|
293
|
+
gwp_per_category_declared_unit_value = float(re.search(r"[-+]?\d*\.?\d+", str(gwp_per_category_declared_unit)).group())
|
294
|
+
wframe_object["gwp_per_unit_area"] = f"{gwp_per_category_declared_unit_value} kg CO2 e/m2"
|
295
|
+
else:
|
296
|
+
wframe_object["gwp_per_unit_area"] = f"{gwp_per_unit_area} kg CO2 e/m2"
|
297
|
+
|
298
|
+
if gwp_per_unit_mass:
|
299
|
+
gwp_per_unit_mass = float(re.search(r"[-+]?\d*\.?\d+", str(gwp_per_unit_mass)).group())
|
300
|
+
gwp_per_unit_mass = round(gwp_per_unit_mass,2)
|
301
|
+
wframe_object["gwp_per_unit_mass"] = f"{gwp_per_unit_mass} kg CO2 e/kg"
|
302
|
+
else:
|
303
|
+
wframe_object["gwp_per_unit_mass"] = "Not avaialble in this EPD"
|
304
|
+
object_key = "object" + str(wframe_epd_no)
|
305
|
+
wframe_epd_data[object_key] = wframe_object
|
306
|
+
wframe_gwp_per_volume.append(gwp_per_unit_volume)
|
307
|
+
wframe_gwp_per_area.append(gwp_per_unit_area)
|
308
|
+
wframe_gwp_per_mass.append(gwp_per_unit_mass)
|
309
|
+
pprint.pp(wframe_object)
|
310
|
+
|
311
|
+
#print all gwp per volume
|
312
|
+
wframe_gwp_per_volume = [float(item) for item in wframe_gwp_per_volume if isinstance(item, (int, float)) or item.replace('.', '', 1).isdigit()]
|
313
|
+
# Calculate the mean
|
314
|
+
if wframe_gwp_per_volume: # Check if the list is not empty
|
315
|
+
mean_value = statistics.mean(wframe_gwp_per_volume)
|
316
|
+
min_value = min(wframe_gwp_per_volume)
|
317
|
+
max_value = max(wframe_gwp_per_volume)
|
318
|
+
print("Mean of GWP per m3:", mean_value)
|
319
|
+
print("Minimum GWP per m3:", min_value)
|
320
|
+
print("Maximum GWP per m3:", max_value)
|
321
|
+
else:
|
322
|
+
print("No numeric values to calculate statistical parameters.")
|