openstudio-ee 0.12.3 → 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 +3 -0
- data/Gemfile +7 -8
- data/README.md +1 -0
- 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 +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 +10 -8
- data/test-workflow-locally.sh +152 -0
- metadata +64 -35
- data/Jenkinsfile +0 -11
@@ -0,0 +1,112 @@
|
|
1
|
+
|
2
|
+
|
3
|
+
###### (Automatically generated documentation)
|
4
|
+
|
5
|
+
# Calculate embodied emissions for window enhancements.
|
6
|
+
|
7
|
+
## Description
|
8
|
+
Calculate embodied emissions for window enhancements to change thermal or lighting performance.
|
9
|
+
|
10
|
+
## Modeler Description
|
11
|
+
Window enhancements like adding a storm window or film layer on top of the existing window have corresponding embodied carbon emissions associated with the enhancement. Based on what enhancement is being made to the existing window, we perform a lookup of the corresponding EC3 EPD and calculate a sum total embodied carbon emissions for that enhancement.
|
12
|
+
|
13
|
+
## Measure Type
|
14
|
+
ModelMeasure
|
15
|
+
|
16
|
+
## Taxonomy
|
17
|
+
|
18
|
+
|
19
|
+
## Arguments
|
20
|
+
|
21
|
+
|
22
|
+
### Pick a Window Construction From the Model to Replace Existing Window Constructions.
|
23
|
+
|
24
|
+
**Name:** construction,
|
25
|
+
**Type:** Choice,
|
26
|
+
**Units:** ,
|
27
|
+
**Required:** true,
|
28
|
+
**Model Dependent:** false
|
29
|
+
|
30
|
+
### Change Fixed Windows?
|
31
|
+
|
32
|
+
**Name:** change_fixed_windows,
|
33
|
+
**Type:** Boolean,
|
34
|
+
**Units:** ,
|
35
|
+
**Required:** true,
|
36
|
+
**Model Dependent:** false
|
37
|
+
|
38
|
+
### Change Operable Windows?
|
39
|
+
|
40
|
+
**Name:** change_operable_windows,
|
41
|
+
**Type:** Boolean,
|
42
|
+
**Units:** ,
|
43
|
+
**Required:** true,
|
44
|
+
**Model Dependent:** false
|
45
|
+
|
46
|
+
### Remove Existing Costs?
|
47
|
+
|
48
|
+
**Name:** remove_costs,
|
49
|
+
**Type:** Boolean,
|
50
|
+
**Units:** ,
|
51
|
+
**Required:** true,
|
52
|
+
**Model Dependent:** false
|
53
|
+
|
54
|
+
### Material and Installation Costs for Construction per Area Used ($/ft^2).
|
55
|
+
|
56
|
+
**Name:** material_cost_ip,
|
57
|
+
**Type:** Double,
|
58
|
+
**Units:** ,
|
59
|
+
**Required:** true,
|
60
|
+
**Model Dependent:** false
|
61
|
+
|
62
|
+
### Demolition Costs for Construction per Area Used ($/ft^2).
|
63
|
+
|
64
|
+
**Name:** demolition_cost_ip,
|
65
|
+
**Type:** Double,
|
66
|
+
**Units:** ,
|
67
|
+
**Required:** true,
|
68
|
+
**Model Dependent:** false
|
69
|
+
|
70
|
+
### Years Until Costs Start (whole years).
|
71
|
+
|
72
|
+
**Name:** years_until_costs_start,
|
73
|
+
**Type:** Integer,
|
74
|
+
**Units:** ,
|
75
|
+
**Required:** true,
|
76
|
+
**Model Dependent:** false
|
77
|
+
|
78
|
+
### Demolition Costs Occur During Initial Construction?
|
79
|
+
|
80
|
+
**Name:** demo_cost_initial_const,
|
81
|
+
**Type:** Boolean,
|
82
|
+
**Units:** ,
|
83
|
+
**Required:** true,
|
84
|
+
**Model Dependent:** false
|
85
|
+
|
86
|
+
### Expected Life (whole years).
|
87
|
+
|
88
|
+
**Name:** expected_life,
|
89
|
+
**Type:** Integer,
|
90
|
+
**Units:** ,
|
91
|
+
**Required:** true,
|
92
|
+
**Model Dependent:** false
|
93
|
+
|
94
|
+
### O & M Costs for Construction per Area Used ($/ft^2).
|
95
|
+
|
96
|
+
**Name:** om_cost_ip,
|
97
|
+
**Type:** Double,
|
98
|
+
**Units:** ,
|
99
|
+
**Required:** true,
|
100
|
+
**Model Dependent:** false
|
101
|
+
|
102
|
+
### O & M Frequency (whole years).
|
103
|
+
|
104
|
+
**Name:** om_frequency,
|
105
|
+
**Type:** Integer,
|
106
|
+
**Units:** ,
|
107
|
+
**Required:** true,
|
108
|
+
**Model Dependent:** false
|
109
|
+
|
110
|
+
|
111
|
+
|
112
|
+
|
File without changes
|
@@ -0,0 +1,386 @@
|
|
1
|
+
# *******************************************************************************
|
2
|
+
# OpenStudio(R), Copyright (c) Alliance for Sustainable Energy, LLC.
|
3
|
+
# See also https://openstudio.net/license
|
4
|
+
# *******************************************************************************
|
5
|
+
|
6
|
+
import openstudio
|
7
|
+
import typing
|
8
|
+
import numpy as np
|
9
|
+
import pprint as pp
|
10
|
+
from resources.EC3_lookup import fetch_epd_data
|
11
|
+
from resources.EC3_lookup import parse_product_epd
|
12
|
+
from resources.EC3_lookup import parse_industrial_epd
|
13
|
+
from resources.EC3_lookup import generate_url
|
14
|
+
from resources.EC3_lookup import calculate_geometry
|
15
|
+
|
16
|
+
# Start the measure
|
17
|
+
class WindowEnhancement(openstudio.measure.ModelMeasure):
|
18
|
+
|
19
|
+
"""A ModelMeasure for window enhancement, calculating embodied carbon."""
|
20
|
+
|
21
|
+
def name(self):
|
22
|
+
"""Measure name."""
|
23
|
+
return "Window Enhancement"
|
24
|
+
|
25
|
+
def description(self):
|
26
|
+
"""Brief description of the measure."""
|
27
|
+
return "Calculates embodied emissions for window frame enhancements using EC3 database lookup. This measure only functions if you have an EC3 key and the required Python libraries installed. In addition to getting embodied car value it does also alter the thermal performance of the windows based on the selections made"
|
28
|
+
|
29
|
+
def modeler_description(self):
|
30
|
+
"""Detailed description of the measure."""
|
31
|
+
return ("This measure evaluates the embodied carbon impact of adding an IGU or storm window "
|
32
|
+
"to an existing structure by analyzing frame material data from EC3.")
|
33
|
+
@staticmethod
|
34
|
+
def gwp_statistics():
|
35
|
+
return ["minimum","maximum","mean","median"]
|
36
|
+
|
37
|
+
@staticmethod
|
38
|
+
def igu_options():
|
39
|
+
return ["electrochromic","fire_resistant","laminated","low_emissivity","tempered"]
|
40
|
+
|
41
|
+
@staticmethod
|
42
|
+
def wf_options():
|
43
|
+
return ["anodized","painted","thermally_improved"]
|
44
|
+
|
45
|
+
@staticmethod
|
46
|
+
def epd_types():
|
47
|
+
return ["Product","Industry"]
|
48
|
+
|
49
|
+
def arguments(self, model: typing.Optional[openstudio.model.Model] = None):
|
50
|
+
"""Define the arguments that user will input."""
|
51
|
+
args = openstudio.measure.OSArgumentVector()
|
52
|
+
|
53
|
+
#make an argument for analysis period
|
54
|
+
analysis_period = openstudio.measure.OSArgument.makeIntegerArgument("analysis_period",True)
|
55
|
+
analysis_period.setDisplayName("Analysis Period")
|
56
|
+
analysis_period.setDescription("Analysis period of embodied carbon of building/building assembly")
|
57
|
+
analysis_period.setDefaultValue(30)
|
58
|
+
args.append(analysis_period)
|
59
|
+
|
60
|
+
#make an argument for igu options for filtering EPDs of igu
|
61
|
+
igu_options_chs = openstudio.StringVector()
|
62
|
+
for option in self.igu_options():
|
63
|
+
igu_options_chs.append(option)
|
64
|
+
igu_option = openstudio.measure.OSArgument.makeChoiceArgument("igu_option", igu_options_chs, True)
|
65
|
+
igu_option.setDisplayName("IGU option")
|
66
|
+
igu_option.setDescription("Type of insulating glazing unit")
|
67
|
+
args.append(igu_option)
|
68
|
+
|
69
|
+
# make an argument for product life time of igu
|
70
|
+
igu_lifetime = openstudio.measure.OSArgument.makeIntegerArgument("igu_lifetime",True)
|
71
|
+
igu_lifetime.setDisplayName("Product Lifetime of IGU")
|
72
|
+
igu_lifetime.setDescription("Life expectancy of insulating glazing unit")
|
73
|
+
igu_lifetime.setDefaultValue(15)
|
74
|
+
args.append(igu_lifetime)
|
75
|
+
|
76
|
+
# make an argument for product life time of window frame
|
77
|
+
wf_lifetime = openstudio.measure.OSArgument.makeIntegerArgument("wf_lifetime",True)
|
78
|
+
wf_lifetime.setDisplayName("Product Lifetime of Window Frame")
|
79
|
+
wf_lifetime.setDescription("Life expectancy of window frame")
|
80
|
+
wf_lifetime.setDefaultValue(15)
|
81
|
+
args.append(wf_lifetime)
|
82
|
+
|
83
|
+
#make an argument for window frame options for filtering EPDs
|
84
|
+
wf_options_chs = openstudio.StringVector()
|
85
|
+
for option in self.wf_options():
|
86
|
+
wf_options_chs.append(option)
|
87
|
+
wf_option = openstudio.measure.OSArgument.makeChoiceArgument("wf_option",wf_options_chs, True)
|
88
|
+
wf_option.setDisplayName("Window frame option")
|
89
|
+
wf_option.setDescription("Type of aluminum extrusion")
|
90
|
+
args.append(wf_option)
|
91
|
+
|
92
|
+
# make an argument for cross-sectional area of window frame
|
93
|
+
frame_cross_section_area = openstudio.measure.OSArgument.makeDoubleArgument("frame_cross_section_area", True)
|
94
|
+
frame_cross_section_area.setDisplayName("Frame Cross Section Area (m²)")
|
95
|
+
frame_cross_section_area.setDescription("Cross-sectional area of the IGU frame in square meters.")
|
96
|
+
frame_cross_section_area.setDefaultValue(0.0025)
|
97
|
+
args.append(frame_cross_section_area)
|
98
|
+
|
99
|
+
# make an argument for selecting EPD type
|
100
|
+
edp_type_chs = openstudio.StringVector()
|
101
|
+
for type in self.epd_types():
|
102
|
+
edp_type_chs.append(type)
|
103
|
+
epd_type = openstudio.measure.OSArgument.makeChoiceArgument("epd_type",edp_type_chs, True)
|
104
|
+
epd_type.setDisplayName("EPD Type")
|
105
|
+
epd_type.setDescription("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.")
|
106
|
+
args.append(epd_type)
|
107
|
+
|
108
|
+
# make an argument for selecting which gwp statistic to use for embodied carbon calculation
|
109
|
+
gwp_statistics_chs = openstudio.StringVector()
|
110
|
+
for gwp_statistic in self.gwp_statistics():
|
111
|
+
gwp_statistics_chs.append(gwp_statistic)
|
112
|
+
gwp_statistic = openstudio.measure.OSArgument.makeChoiceArgument("gwp_statistic",gwp_statistics_chs, True)
|
113
|
+
gwp_statistic.setDisplayName("GWP Statistic")
|
114
|
+
gwp_statistic.setDescription("Statistic type (minimum or maximum or mean or median) of returned GWP value")
|
115
|
+
args.append(gwp_statistic)
|
116
|
+
|
117
|
+
# make an argument for total embodied carbon (TEC) of whole construction/building
|
118
|
+
total_embodied_carbon = openstudio.measure.OSArgument.makeDoubleArgument("total_embodied_carbon", True)
|
119
|
+
total_embodied_carbon.setDisplayName("Total Embodied Carbon of Building/Building Assembly")
|
120
|
+
total_embodied_carbon.setDescription("Total GWP or embodied carbon intensity of the building (assembly) in kg CO2 eq.")
|
121
|
+
total_embodied_carbon.setDefaultValue(0.0)
|
122
|
+
args.append(total_embodied_carbon)
|
123
|
+
|
124
|
+
# make an argument for api_token
|
125
|
+
api_key = openstudio.measure.OSArgument.makeStringArgument("api_key",True)
|
126
|
+
api_key.setDisplayName("API Token")
|
127
|
+
api_key.setDescription("API Token for sending API call to EC3 EPD Database")
|
128
|
+
api_key.setDefaultValue("Obtain the key from EC3 website")
|
129
|
+
args.append(api_key)
|
130
|
+
|
131
|
+
return args
|
132
|
+
|
133
|
+
def run(self, model: openstudio.model.Model, runner: openstudio.measure.OSRunner, user_arguments: openstudio.measure.OSArgumentMap):
|
134
|
+
"""Define what happens when the measure is run. Execute the measure."""
|
135
|
+
runner.registerInfo("Starting WindowEnhancement measure execution.")
|
136
|
+
|
137
|
+
# Check if model exists
|
138
|
+
if not model:
|
139
|
+
runner.registerError("Model is None. Exiting measure.")
|
140
|
+
return False
|
141
|
+
# built-in error checking
|
142
|
+
if not runner.validateUserArguments(self.arguments(model), user_arguments):
|
143
|
+
return False
|
144
|
+
|
145
|
+
# Retrieve user inputs
|
146
|
+
frame_cross_section_area = runner.getDoubleArgumentValue("frame_cross_section_area", user_arguments)
|
147
|
+
gwp_statistic = runner.getStringArgumentValue("gwp_statistic", user_arguments)
|
148
|
+
igu_option = runner.getStringArgumentValue("igu_option", user_arguments)
|
149
|
+
wf_option = runner.getStringArgumentValue("wf_option", user_arguments)
|
150
|
+
analysis_period = runner.getIntegerArgumentValue("analysis_period",user_arguments)
|
151
|
+
igu_lifetime = runner.getIntegerArgumentValue("igu_lifetime",user_arguments)
|
152
|
+
wf_lifetime = runner.getIntegerArgumentValue("wf_lifetime",user_arguments)
|
153
|
+
total_embodied_carbon = runner.getDoubleArgumentValue("total_embodied_carbon",user_arguments)
|
154
|
+
api_key = runner.getStringArgumentValue("api_key", user_arguments)
|
155
|
+
epd_type = runner.getStringArgumentValue("epd_type", user_arguments)
|
156
|
+
|
157
|
+
# Debug: Print all user arguments received
|
158
|
+
runner.registerInfo(f"User Arguments: {user_arguments}")
|
159
|
+
for arg_name, arg_value in user_arguments.items():
|
160
|
+
try:
|
161
|
+
# Ensure that arg_value is valid and that the valueAsString() method can be called
|
162
|
+
value_str = arg_value.valueAsString() if arg_value is not None else "None"
|
163
|
+
runner.registerInfo(f"user_argument: {arg_name} = {value_str}")
|
164
|
+
except Exception as e:
|
165
|
+
runner.registerInfo(f"Error processing argument: {arg_name} - {str(e)}")
|
166
|
+
|
167
|
+
# Check if numeric values are reasonable
|
168
|
+
if analysis_period <= 0:
|
169
|
+
runner.registerError("Choose an integer larger than 0 for analysis period of embodeid carbon calcualtion.")
|
170
|
+
if igu_lifetime <= 0:
|
171
|
+
runner.registerError("Choose an integer larger than 0 for product lifetime of insulating glazing unit.")
|
172
|
+
if wf_lifetime <= 0:
|
173
|
+
runner.registerError("Choose an integer larger than 0 for product lifetime of window frame.")
|
174
|
+
|
175
|
+
# Print the number of sub-surfaces before processing
|
176
|
+
sub_surfaces = model.getSubSurfaces()
|
177
|
+
runner.registerInfo(f"Total sub-surfaces found: {len(sub_surfaces)}")
|
178
|
+
# List storing subsurface object subject to change, here we want to catch "Name: Sub Surface 2, Surface Type: FixedWindow, Space Name: Space 2"
|
179
|
+
sub_surfaces_to_change = []
|
180
|
+
# loop through sub surfaces
|
181
|
+
for subsurface in sub_surfaces:
|
182
|
+
|
183
|
+
if subsurface.subSurfaceType() in ["FixedWindow","OperableWindow","Skylight"]:
|
184
|
+
# append the subsurface objects carrying windows into list
|
185
|
+
sub_surfaces_to_change.append(subsurface)
|
186
|
+
runner.registerInfo(f"Processing window construction in {subsurface.nameString()}")
|
187
|
+
else:# if sub_surface.subSurfaceType() not in ["FixedWindow", "OperableWindow"]:
|
188
|
+
runner.registerInfo(f"Skipping non-window surface: {subsurface.nameString()}")
|
189
|
+
continue
|
190
|
+
|
191
|
+
# dictionary storing properties of subsurfaces containing window construcitons
|
192
|
+
subsurface_dict = {}
|
193
|
+
# loop through layered window construciton to collect glazing materials
|
194
|
+
for subsurface in sub_surfaces_to_change:
|
195
|
+
subsurface_name = subsurface.nameString()
|
196
|
+
subsurface_dict[subsurface_name] = {}
|
197
|
+
if subsurface.construction().is_initialized():
|
198
|
+
subsurface_const = subsurface.construction().get()
|
199
|
+
if subsurface_const.to_LayeredConstruction().is_initialized():
|
200
|
+
layered_construction = subsurface_const.to_LayeredConstruction().get()
|
201
|
+
|
202
|
+
subsurface_dict[subsurface_name]["Glazing"] = {}
|
203
|
+
subsurface_dict[subsurface_name]["Frame"] = {}
|
204
|
+
subsurface_dict[subsurface_name]["Subsurface object"] = subsurface
|
205
|
+
subsurface_dict[subsurface_name]["Glazing"]["Object"] = layered_construction
|
206
|
+
subsurface_dict[subsurface_name]["Glazing"]["Lifetime"] = igu_lifetime
|
207
|
+
subsurface_dict[subsurface_name]["Frame"]["Lifetime"] = wf_lifetime
|
208
|
+
subsurface_dict[subsurface_name]["window_embodied_carbon"] = 0.0
|
209
|
+
subsurface_dict[subsurface_name]["Dimension"] = calculate_geometry(self, subsurface)
|
210
|
+
|
211
|
+
# determine number of panes of window
|
212
|
+
# EC3 handle num_panes use ">=" operator instead of "="
|
213
|
+
# If use unprocessed single pane EPD, underestimate emission associated with product manufacturing
|
214
|
+
# if use multiple pane EPD, thickness of each pane in EPD might be different from the model (adopt this option, smaller error than single pane option)
|
215
|
+
print(layered_construction.numLayers())
|
216
|
+
if layered_construction.numLayers() == 1:
|
217
|
+
num_panes = 1
|
218
|
+
elif layered_construction.numLayers() == 3:
|
219
|
+
num_panes = 2
|
220
|
+
elif layered_construction.numLayers() == 5:
|
221
|
+
num_panes = 3
|
222
|
+
else:
|
223
|
+
runner.registerInfo("currently unable to handle more complex scenarios.")
|
224
|
+
subsurface_dict[subsurface_name]["Number of panes"] = num_panes
|
225
|
+
|
226
|
+
# get thickness from each window construction layer
|
227
|
+
total_glazing_thickness = 0.0
|
228
|
+
for i in range(layered_construction.numLayers()):
|
229
|
+
material = layered_construction.getLayer(i)
|
230
|
+
runner.registerInfo(f"Layer {i+1}: {material.nameString()}")
|
231
|
+
if material.thickness(): # count air layer thickness (delete: and "Air" not in material.nameString():)
|
232
|
+
glazing_thickness = material.thickness()
|
233
|
+
runner.registerInfo(f"In {subsurface_name}: Material: {material.nameString()}, Thickness: {glazing_thickness} m")
|
234
|
+
total_glazing_thickness += glazing_thickness
|
235
|
+
else: # handle the case when no thickness is prodvied by the model
|
236
|
+
glazing_thickness = 0.003
|
237
|
+
total_glazing_thickness += glazing_thickness
|
238
|
+
runner.registerInfo(f"In {subsurface_name}: Material: {material.nameString()} doesn't have thickness attribute and a default thickness of 3 mm assigned.")
|
239
|
+
|
240
|
+
glazing_area = subsurface_dict[subsurface_name]["Dimension"]["area"]
|
241
|
+
subsurface_dict[subsurface_name]["Glazing"]["Total thickness (m)"] = total_glazing_thickness
|
242
|
+
subsurface_dict[subsurface_name]["Glazing"]["Area (m2)"] = glazing_area
|
243
|
+
subsurface_dict[subsurface_name]["Glazing"]["Volume (m3)"] = total_glazing_thickness * glazing_area
|
244
|
+
|
245
|
+
# get frame and divider dimensions:
|
246
|
+
# reference: https://bigladdersoftware.com/epx/docs/9-3/input-output-reference/group-thermal-zone-description-geometry.html#windowpropertyframeanddivider
|
247
|
+
if subsurface.windowPropertyFrameAndDivider().is_initialized(): # check if frame_and_divider exist in selected subsurface
|
248
|
+
frame = subsurface.windowPropertyFrameAndDivider().get()
|
249
|
+
frame_name = frame.nameString()
|
250
|
+
frame_width = frame.frameWidth()
|
251
|
+
frame_op = frame.frameOutsideProjection()
|
252
|
+
frame_ip = frame.frameInsideProjection()
|
253
|
+
frame_cross_section_area = frame_width * (frame_op + frame_ip + subsurface_dict[subsurface_name]["Glazing"]["Total thickness (m)"])
|
254
|
+
frame_perimeter = subsurface_dict[subsurface_name]["Dimension"]["perimeter"]
|
255
|
+
|
256
|
+
if frame.numberOfHorizontalDividers() != 0 or frame.numberOfVerticalDividers() != 0:
|
257
|
+
divider_width = frame.dividerWidth()
|
258
|
+
divider_op = frame.dividerOutsideProjection()
|
259
|
+
divider_ip = frame.dividerInsideProjection()
|
260
|
+
divider_cross_section_area = divider_width * (divider_op + divider_ip + subsurface_dict[subsurface_name]["Glazing"]["Total thickness (m)"])
|
261
|
+
num_hori_divider = frame.numberOfHorizontalDividers() # integer
|
262
|
+
num_verti_divider = frame.numberOfVerticalDividers() # integer
|
263
|
+
total_divider_length = (num_hori_divider * subsurface_dict[subsurface_name]["Dimension"]["width"] +
|
264
|
+
num_verti_divider * subsurface_dict[subsurface_name]["Dimension"]["length"])
|
265
|
+
else:
|
266
|
+
divider_width = 0.0
|
267
|
+
divider_cross_section_area = 0.0
|
268
|
+
runner.registerInfo(f"In {subsurface.nameString()}'s Frame {frame_name}: no divider")
|
269
|
+
|
270
|
+
runner.registerInfo(f"In {subsurface.nameString()}'s Frame and divider: {frame_name},"
|
271
|
+
f"Frame Width: {frame_width} m, cross sectional area: {frame_cross_section_area} m2, perimeter: {frame_perimeter}"
|
272
|
+
f"Divider width: {divider_width}m, cross sectional area: {divider_cross_section_area} m2, total length: {total_divider_length} m")
|
273
|
+
else:
|
274
|
+
runner.registerInfo(f"In {subsurface.nameString()}: no frame and divider")
|
275
|
+
|
276
|
+
subsurface_dict[subsurface_name]["Frame"]["Object"] = frame
|
277
|
+
subsurface_dict[subsurface_name]["Frame"]["Frame cross sectional area (m2)"] = frame_cross_section_area
|
278
|
+
subsurface_dict[subsurface_name]["Frame"]["Divider cross sectional area (m2)"] = divider_cross_section_area
|
279
|
+
subsurface_dict[subsurface_name]["Frame"]["Volume (m3)"] = frame_cross_section_area * frame_perimeter + divider_cross_section_area * total_divider_length
|
280
|
+
|
281
|
+
epd_datalist = {}
|
282
|
+
|
283
|
+
glazing_product_url = generate_url(material_name = "InsulatingGlazingUnits", option = igu_option, glass_panes = num_panes, epd_type= "Product", endpoint = "materials")
|
284
|
+
frame_product_url = generate_url(material_name = "AluminiumExtrusions", epd_type= "Product", endpoint = "materials") # EC3 only has aluminum option, revisit later
|
285
|
+
glazing_industry_url = generate_url(material_name = "InsulatingGlazingUnits", option = igu_option, glass_panes = num_panes, epd_type= "Industry", endpoint = "industry_epds")
|
286
|
+
frame_industry_url = generate_url(material_name = "AluminiumExtrusions", epd_type= "Industry", endpoint = "industry_epds")
|
287
|
+
glazing_product_epd = fetch_epd_data(url = glazing_product_url, api_token = api_key)
|
288
|
+
frame_product_epd = fetch_epd_data(url = frame_product_url, api_token = api_key)
|
289
|
+
glazing_industry_epd = fetch_epd_data(url = glazing_industry_url, api_token = api_key)
|
290
|
+
frame_industry_epd = fetch_epd_data(url = frame_industry_url, api_token = api_key)
|
291
|
+
|
292
|
+
if epd_type == "Product":
|
293
|
+
if not glazing_product_epd:
|
294
|
+
glazing_epd = glazing_industry_epd
|
295
|
+
runner.registerInfo("Product EPDs are not avialable, industry EPDs are accessed instead")
|
296
|
+
else:
|
297
|
+
glazing_epd = glazing_product_epd
|
298
|
+
epd_datalist["Glazing"] = glazing_epd
|
299
|
+
if not frame_product_epd:
|
300
|
+
frame_epd = frame_industry_epd
|
301
|
+
runner.registerInfo("Product EPDs are not avialable, industry EPDs are accessed instead")
|
302
|
+
else:
|
303
|
+
frame_epd = frame_product_epd
|
304
|
+
epd_datalist["Frame"] = frame_epd
|
305
|
+
|
306
|
+
elif epd_type == "Industry":
|
307
|
+
if not glazing_industry_epd:
|
308
|
+
glazing_epd = glazing_product_epd
|
309
|
+
runner.registerInfo("Product EPDs are not avialable, industry EPDs are accessed instead")
|
310
|
+
else:
|
311
|
+
glazing_epd = glazing_industry_epd
|
312
|
+
epd_datalist["Glazing"] = glazing_epd
|
313
|
+
if not frame_industry_epd:
|
314
|
+
frame_epd = frame_product_epd
|
315
|
+
runner.registerInfo("Product EPDs are not avialable, industry EPDs are accessed instead")
|
316
|
+
else:
|
317
|
+
frame_epd = frame_industry_epd
|
318
|
+
epd_datalist["Frame"] = frame_epd
|
319
|
+
|
320
|
+
for material_name, epd_data in epd_datalist.items():
|
321
|
+
# collect GWP values per functional unit
|
322
|
+
gwp_values = {}
|
323
|
+
gwp_values["gwp_per_m2"] = []
|
324
|
+
gwp_values["gwp_per_kg"] = []
|
325
|
+
gwp_values["gwp_per_m3"] = []
|
326
|
+
|
327
|
+
for idx, epd in enumerate(epd_data,start = 1):
|
328
|
+
# parse json repsonse based on epd_eype
|
329
|
+
if epd_type == "Industry":
|
330
|
+
parsed_data = parse_industrial_epd(epd)
|
331
|
+
elif epd_type == "Product":
|
332
|
+
parsed_data = parse_product_epd(epd)
|
333
|
+
|
334
|
+
gwp_per_m2 = parsed_data["gwp_per_m2 (kg CO2 eq/m2)"]
|
335
|
+
if gwp_per_m2 != None:
|
336
|
+
gwp_values["gwp_per_m2"].append(float(gwp_per_m2))
|
337
|
+
|
338
|
+
gwp_per_kg = parsed_data["gwp_per_kg (kg CO2 eq/kg)"]
|
339
|
+
if gwp_per_kg != None:
|
340
|
+
gwp_values["gwp_per_kg"].append(float(gwp_per_kg))
|
341
|
+
|
342
|
+
gwp_per_m3 = parsed_data["gwp_per_m3 (kg CO2 eq/m3)"]
|
343
|
+
if gwp_per_m3 != None:
|
344
|
+
gwp_values["gwp_per_m3"].append(float(gwp_per_m3))
|
345
|
+
|
346
|
+
# extract gwp statistics by
|
347
|
+
for functional_unit, list in gwp_values.items():
|
348
|
+
if len(list) == 0:
|
349
|
+
gwp = None
|
350
|
+
runner.registerInfo(f"No GWP values returned from {functional_unit}")
|
351
|
+
elif len(list) == 1:
|
352
|
+
gwp = list[0]
|
353
|
+
elif gwp_statistic == "minimum":
|
354
|
+
gwp = float(np.min(list))
|
355
|
+
elif gwp_statistic == "maximum":
|
356
|
+
gwp = float(np.max(list))
|
357
|
+
elif gwp_statistic == "mean":
|
358
|
+
gwp = float(np.mean(list))
|
359
|
+
elif gwp_statistic == "median":
|
360
|
+
gwp = float(np.median(list))
|
361
|
+
# store gwp value
|
362
|
+
subsurface_dict[subsurface_name][material_name][functional_unit] = gwp
|
363
|
+
|
364
|
+
if analysis_period <= subsurface_dict[subsurface_name][material_name]["Lifetime"]:
|
365
|
+
embodied_carbon = float(subsurface_dict[subsurface_name][material_name]["gwp_per_m3"] * subsurface_dict[subsurface_name][material_name]["Volume (m3)"])
|
366
|
+
subsurface_dict[subsurface_name][material_name]["embodied_carbon"] = embodied_carbon
|
367
|
+
else:
|
368
|
+
multiplier = np.ceil(analysis_period/subsurface_dict[subsurface_name][material_name]["Lifetime"])
|
369
|
+
embodied_carbon = float(subsurface_dict[subsurface_name][material_name]["gwp_per_m3"] * subsurface_dict[subsurface_name][material_name]["Volume (m3)"] * multiplier)
|
370
|
+
subsurface_dict[subsurface_name][material_name]["embodied_carbon"] = embodied_carbon
|
371
|
+
|
372
|
+
subsurface_dict[subsurface_name]["window_embodied_carbon"] += subsurface_dict[subsurface_name][material_name]["embodied_carbon"]
|
373
|
+
|
374
|
+
runner.registerInfo(f"window's embodied carbon in this subsurface: {subsurface_dict[subsurface_name]['window_embodied_carbon']}")
|
375
|
+
|
376
|
+
# attach additional properties to openstudio material
|
377
|
+
additional_properties = subsurface_dict[subsurface_name]["Subsurface object"].additionalProperties()
|
378
|
+
additional_properties.setFeature("Subsurface name", subsurface_name)
|
379
|
+
additional_properties.setFeature("Embodied carbon", subsurface_dict[subsurface_name]["window_embodied_carbon"])
|
380
|
+
|
381
|
+
pp.pprint(subsurface_dict)
|
382
|
+
|
383
|
+
return True
|
384
|
+
|
385
|
+
# Register the measure
|
386
|
+
WindowEnhancement().registerWithApplication()
|
@@ -0,0 +1,128 @@
|
|
1
|
+
<?xml version="1.0"?>
|
2
|
+
<measure>
|
3
|
+
<schema_version>3.1</schema_version>
|
4
|
+
<error>Failed to infer measure name from '/workspace/lib/measures/window_enhancement/measure.py'</error>
|
5
|
+
<name>window_enhancment</name>
|
6
|
+
<uid>0ad8e761-8624-4d9d-a7d6-ebf30cce243b</uid>
|
7
|
+
<version_id>6e806249-5eab-444e-b724-f00dd42c13fb</version_id>
|
8
|
+
<version_modified>2025-09-25T04:50:20Z</version_modified>
|
9
|
+
<xml_checksum>7DADF7C5</xml_checksum>
|
10
|
+
<class_name>WindowEnhancment</class_name>
|
11
|
+
<display_name>Window Enhancment</display_name>
|
12
|
+
<description>Make existing window better by adding film, storm window, or something else.</description>
|
13
|
+
<modeler_description>I'm going to use layred construction and not simple glazing to do this. We have to think about how to address simple glazing with this.</modeler_description>
|
14
|
+
<arguments>
|
15
|
+
<argument>
|
16
|
+
<name>space_name</name>
|
17
|
+
<display_name>New space name</display_name>
|
18
|
+
<description>This name will be used as the name of the new space.</description>
|
19
|
+
<type>String</type>
|
20
|
+
<required>true</required>
|
21
|
+
<model_dependent>false</model_dependent>
|
22
|
+
</argument>
|
23
|
+
</arguments>
|
24
|
+
<outputs />
|
25
|
+
<provenances />
|
26
|
+
<tags>
|
27
|
+
<tag>Envelope.Fenestration</tag>
|
28
|
+
</tags>
|
29
|
+
<attributes>
|
30
|
+
<attribute>
|
31
|
+
<name>Measure Type</name>
|
32
|
+
<value>ModelMeasure</value>
|
33
|
+
<datatype>string</datatype>
|
34
|
+
</attribute>
|
35
|
+
<attribute>
|
36
|
+
<name>Measure Language</name>
|
37
|
+
<value>Python</value>
|
38
|
+
<datatype>string</datatype>
|
39
|
+
</attribute>
|
40
|
+
<attribute>
|
41
|
+
<name>Intended Software Tool</name>
|
42
|
+
<value>Apply Measure Now</value>
|
43
|
+
<datatype>string</datatype>
|
44
|
+
</attribute>
|
45
|
+
<attribute>
|
46
|
+
<name>Intended Software Tool</name>
|
47
|
+
<value>OpenStudio Application</value>
|
48
|
+
<datatype>string</datatype>
|
49
|
+
</attribute>
|
50
|
+
<attribute>
|
51
|
+
<name>Intended Software Tool</name>
|
52
|
+
<value>Parametric Analysis Tool</value>
|
53
|
+
<datatype>string</datatype>
|
54
|
+
</attribute>
|
55
|
+
<attribute>
|
56
|
+
<name>Intended Use Case</name>
|
57
|
+
<value>Retrofit EE</value>
|
58
|
+
<datatype>string</datatype>
|
59
|
+
</attribute>
|
60
|
+
</attributes>
|
61
|
+
<files>
|
62
|
+
<file>
|
63
|
+
<filename>LICENSE.md</filename>
|
64
|
+
<filetype>md</filetype>
|
65
|
+
<usage_type>license</usage_type>
|
66
|
+
<checksum>FFCBFF29</checksum>
|
67
|
+
</file>
|
68
|
+
<file>
|
69
|
+
<filename>README.md</filename>
|
70
|
+
<filetype>md</filetype>
|
71
|
+
<usage_type>readme</usage_type>
|
72
|
+
<checksum>DE9B0464</checksum>
|
73
|
+
</file>
|
74
|
+
<file>
|
75
|
+
<filename>.gitkeep</filename>
|
76
|
+
<filetype>gitkeep</filetype>
|
77
|
+
<usage_type>doc</usage_type>
|
78
|
+
<checksum>00000000</checksum>
|
79
|
+
</file>
|
80
|
+
<file>
|
81
|
+
<version>
|
82
|
+
<software_program>OpenStudio</software_program>
|
83
|
+
<identifier>3.8.0</identifier>
|
84
|
+
<min_compatible>3.8.0</min_compatible>
|
85
|
+
</version>
|
86
|
+
<filename>measure.py</filename>
|
87
|
+
<filetype>py</filetype>
|
88
|
+
<usage_type>script</usage_type>
|
89
|
+
<checksum>3D0A891D</checksum>
|
90
|
+
</file>
|
91
|
+
<file>
|
92
|
+
<filename>EC3_lookup.py</filename>
|
93
|
+
<filetype>py</filetype>
|
94
|
+
<usage_type>resource</usage_type>
|
95
|
+
<checksum>2832F008</checksum>
|
96
|
+
</file>
|
97
|
+
<file>
|
98
|
+
<filename>Test_API.py</filename>
|
99
|
+
<filetype>py</filetype>
|
100
|
+
<usage_type>resource</usage_type>
|
101
|
+
<checksum>26458C20</checksum>
|
102
|
+
</file>
|
103
|
+
<file>
|
104
|
+
<filename>calculate_perimeter.py</filename>
|
105
|
+
<filetype>py</filetype>
|
106
|
+
<usage_type>resource</usage_type>
|
107
|
+
<checksum>D7980CA8</checksum>
|
108
|
+
</file>
|
109
|
+
<file>
|
110
|
+
<filename>example_model.osm</filename>
|
111
|
+
<filetype>osm</filetype>
|
112
|
+
<usage_type>test</usage_type>
|
113
|
+
<checksum>E08CA027</checksum>
|
114
|
+
</file>
|
115
|
+
<file>
|
116
|
+
<filename>example_model_2.osm</filename>
|
117
|
+
<filetype>osm</filetype>
|
118
|
+
<usage_type>test</usage_type>
|
119
|
+
<checksum>E01ECAD1</checksum>
|
120
|
+
</file>
|
121
|
+
<file>
|
122
|
+
<filename>test_window_enhancement.py</filename>
|
123
|
+
<filetype>py</filetype>
|
124
|
+
<usage_type>test</usage_type>
|
125
|
+
<checksum>061487A7</checksum>
|
126
|
+
</file>
|
127
|
+
</files>
|
128
|
+
</measure>
|