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.
- 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 +6 -0
- data/Gemfile +7 -8
- data/README.md +3 -2
- data/WORKFLOW_CHANGES.md +74 -0
- data/lib/measures/AddDaylightSensors/measure.rb +79 -79
- data/lib/measures/AddDaylightSensors/measure.xml +4 -4
- data/lib/measures/AddOverhangsByProjectionFactor/measure.rb +38 -41
- data/lib/measures/AddOverhangsByProjectionFactor/measure.xml +4 -4
- data/lib/measures/EnableDemandControlledVentilation/measure.rb +37 -40
- data/lib/measures/EnableDemandControlledVentilation/measure.xml +4 -4
- data/lib/measures/EnableEconomizerControl/measure.rb +36 -37
- data/lib/measures/EnableEconomizerControl/measure.xml +4 -4
- data/lib/measures/GLHEProExportLoadsforGroundHeatExchangerSizing/measure.rb +27 -41
- data/lib/measures/GLHEProExportLoadsforGroundHeatExchangerSizing/measure.xml +4 -4
- data/lib/measures/GLHEProGFunctionImport/measure.rb +11 -15
- data/lib/measures/GLHEProGFunctionImport/measure.xml +4 -4
- data/lib/measures/GLHEProSetupExportLoadsforGroundHeatExchangerSizing/measure.rb +5 -9
- data/lib/measures/GLHEProSetupExportLoadsforGroundHeatExchangerSizing/measure.xml +3 -3
- data/lib/measures/ImproveFanBeltEfficiency/measure.rb +78 -95
- data/lib/measures/ImproveFanBeltEfficiency/measure.xml +6 -6
- data/lib/measures/ImproveMotorEfficiency/measure.rb +75 -100
- data/lib/measures/ImproveMotorEfficiency/measure.xml +6 -6
- data/lib/measures/IncreaseInsulationRValueForExteriorWalls/measure.rb +137 -130
- data/lib/measures/IncreaseInsulationRValueForExteriorWalls/measure.xml +4 -4
- data/lib/measures/IncreaseInsulationRValueForExteriorWallsByPercentage/measure.rb +114 -115
- data/lib/measures/IncreaseInsulationRValueForExteriorWallsByPercentage/measure.xml +3 -3
- data/lib/measures/IncreaseInsulationRValueForRoofs/measure.rb +137 -130
- data/lib/measures/IncreaseInsulationRValueForRoofs/measure.xml +4 -4
- data/lib/measures/IncreaseInsulationRValueForRoofsByPercentage/measure.rb +114 -115
- data/lib/measures/IncreaseInsulationRValueForRoofsByPercentage/measure.xml +3 -3
- data/lib/measures/ReduceElectricEquipmentLoadsByPercentage/measure.rb +69 -63
- data/lib/measures/ReduceElectricEquipmentLoadsByPercentage/measure.xml +6 -6
- data/lib/measures/ReduceLightingLoadsByPercentage/measure.rb +77 -66
- data/lib/measures/ReduceLightingLoadsByPercentage/measure.xml +6 -6
- data/lib/measures/ReduceNightTimeElectricEquipmentLoads/measure.rb +45 -43
- data/lib/measures/ReduceNightTimeElectricEquipmentLoads/measure.xml +4 -4
- data/lib/measures/ReduceNightTimeLightingLoads/measure.rb +45 -43
- data/lib/measures/ReduceNightTimeLightingLoads/measure.xml +4 -4
- data/lib/measures/ReduceSpaceInfiltrationByPercentage/measure.rb +58 -52
- data/lib/measures/ReduceSpaceInfiltrationByPercentage/measure.xml +6 -6
- data/lib/measures/ReduceVentilationByPercentage/measure.rb +49 -46
- data/lib/measures/ReduceVentilationByPercentage/measure.xml +6 -6
- data/lib/measures/add_variable_speed_rtu_control_logic/measure.rb +31 -23
- data/lib/measures/add_variable_speed_rtu_control_logic/measure.xml +4 -4
- data/lib/measures/create_variable_speed_rtu/measure.rb +166 -174
- data/lib/measures/create_variable_speed_rtu/measure.xml +6 -6
- data/lib/measures/fan_assist_night_ventilation/measure.rb +33 -32
- data/lib/measures/fan_assist_night_ventilation/measure.xml +4 -4
- data/lib/measures/nze_hvac/measure.rb +72 -62
- data/lib/measures/nze_hvac/measure.xml +4 -4
- 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 +4 -4
- 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 +271 -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 +10 -8
- data/test-workflow-locally.sh +152 -0
- metadata +64 -35
- 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)}")
|