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.
Files changed (75) hide show
  1. checksums.yaml +4 -4
  2. data/.coverage +0 -0
  3. data/.github/workflows/test-with-openstudio.yml +109 -74
  4. data/.gitignore +21 -0
  5. data/.rubocop.yml +15 -2
  6. data/CHANGELOG.md +12 -0
  7. data/Gemfile +7 -8
  8. data/README.md +5 -1
  9. data/WORKFLOW_CHANGES.md +74 -0
  10. data/doc_templates/LICENSE.md +1 -1
  11. data/lib/measures/AddDaylightSensors/measure.rb +79 -79
  12. data/lib/measures/AddDaylightSensors/measure.xml +5 -5
  13. data/lib/measures/AddOverhangsByProjectionFactor/measure.rb +38 -41
  14. data/lib/measures/AddOverhangsByProjectionFactor/measure.xml +5 -5
  15. data/lib/measures/EnableDemandControlledVentilation/measure.rb +37 -40
  16. data/lib/measures/EnableDemandControlledVentilation/measure.xml +5 -5
  17. data/lib/measures/EnableEconomizerControl/measure.rb +36 -37
  18. data/lib/measures/EnableEconomizerControl/measure.xml +5 -5
  19. data/lib/measures/GLHEProExportLoadsforGroundHeatExchangerSizing/measure.rb +27 -41
  20. data/lib/measures/GLHEProExportLoadsforGroundHeatExchangerSizing/measure.xml +5 -5
  21. data/lib/measures/GLHEProGFunctionImport/measure.rb +11 -15
  22. data/lib/measures/GLHEProGFunctionImport/measure.xml +5 -5
  23. data/lib/measures/GLHEProSetupExportLoadsforGroundHeatExchangerSizing/measure.rb +5 -9
  24. data/lib/measures/GLHEProSetupExportLoadsforGroundHeatExchangerSizing/measure.xml +4 -4
  25. data/lib/measures/ImproveFanBeltEfficiency/measure.rb +78 -95
  26. data/lib/measures/ImproveFanBeltEfficiency/measure.xml +7 -7
  27. data/lib/measures/ImproveMotorEfficiency/measure.rb +75 -100
  28. data/lib/measures/ImproveMotorEfficiency/measure.xml +7 -7
  29. data/lib/measures/IncreaseInsulationRValueForExteriorWalls/measure.rb +137 -130
  30. data/lib/measures/IncreaseInsulationRValueForExteriorWalls/measure.xml +5 -5
  31. data/lib/measures/IncreaseInsulationRValueForExteriorWallsByPercentage/measure.rb +114 -115
  32. data/lib/measures/IncreaseInsulationRValueForExteriorWallsByPercentage/measure.xml +4 -4
  33. data/lib/measures/IncreaseInsulationRValueForRoofs/measure.rb +137 -130
  34. data/lib/measures/IncreaseInsulationRValueForRoofs/measure.xml +5 -5
  35. data/lib/measures/IncreaseInsulationRValueForRoofsByPercentage/measure.rb +114 -115
  36. data/lib/measures/IncreaseInsulationRValueForRoofsByPercentage/measure.xml +4 -4
  37. data/lib/measures/ReduceElectricEquipmentLoadsByPercentage/measure.rb +69 -63
  38. data/lib/measures/ReduceElectricEquipmentLoadsByPercentage/measure.xml +7 -7
  39. data/lib/measures/ReduceLightingLoadsByPercentage/measure.rb +77 -66
  40. data/lib/measures/ReduceLightingLoadsByPercentage/measure.xml +7 -7
  41. data/lib/measures/ReduceNightTimeElectricEquipmentLoads/measure.rb +45 -43
  42. data/lib/measures/ReduceNightTimeElectricEquipmentLoads/measure.xml +5 -5
  43. data/lib/measures/ReduceNightTimeLightingLoads/measure.rb +45 -43
  44. data/lib/measures/ReduceNightTimeLightingLoads/measure.xml +5 -5
  45. data/lib/measures/ReduceSpaceInfiltrationByPercentage/measure.rb +58 -52
  46. data/lib/measures/ReduceSpaceInfiltrationByPercentage/measure.xml +7 -7
  47. data/lib/measures/ReduceVentilationByPercentage/measure.rb +49 -46
  48. data/lib/measures/ReduceVentilationByPercentage/measure.xml +7 -7
  49. data/lib/measures/add_variable_speed_rtu_control_logic/measure.rb +31 -23
  50. data/lib/measures/add_variable_speed_rtu_control_logic/measure.xml +5 -5
  51. data/lib/measures/create_variable_speed_rtu/measure.rb +166 -174
  52. data/lib/measures/create_variable_speed_rtu/measure.xml +7 -7
  53. data/lib/measures/fan_assist_night_ventilation/measure.rb +33 -32
  54. data/lib/measures/fan_assist_night_ventilation/measure.xml +5 -5
  55. data/lib/measures/nze_hvac/measure.rb +72 -62
  56. data/lib/measures/nze_hvac/measure.xml +5 -5
  57. data/lib/measures/replace_water_heater_mixed_with_thermal_storage_chilled_water/measure.rb +16 -19
  58. data/lib/measures/replace_water_heater_mixed_with_thermal_storage_chilled_water/measure.xml +5 -5
  59. data/lib/measures/window_enhancement/LICENSE.md +14 -0
  60. data/lib/measures/window_enhancement/README.md +112 -0
  61. data/lib/measures/window_enhancement/docs/.gitkeep +0 -0
  62. data/lib/measures/window_enhancement/measure.py +386 -0
  63. data/lib/measures/window_enhancement/measure.xml +128 -0
  64. data/lib/measures/window_enhancement/resources/EC3_lookup.py +321 -0
  65. data/lib/measures/window_enhancement/resources/Test_API.py +32 -0
  66. data/lib/measures/window_enhancement/resources/__pycache__/EC3_lookup.cpython-39.pyc +0 -0
  67. data/lib/measures/window_enhancement/resources/__pycache__/Original_EC3_lookup.py +322 -0
  68. data/lib/measures/window_enhancement/resources/__pycache__/Test_API.cpython-39.pyc +0 -0
  69. data/lib/measures/window_enhancement/resources/calculate_perimeter.py +39 -0
  70. data/lib/measures/window_enhancement/test_output.log +39 -0
  71. data/lib/openstudio/ee_measures/version.rb +1 -1
  72. data/openstudio-ee.gemspec +11 -9
  73. data/test-workflow-locally.sh +152 -0
  74. metadata +66 -37
  75. data/Jenkinsfile +0 -11
@@ -0,0 +1,14 @@
1
+ OpenStudio(R), Copyright (c) 2008, 2025 Alliance for Sustainable Energy, LLC.
2
+
3
+ Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
4
+
5
+ 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
6
+
7
+ 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
8
+
9
+ 3. Redistribution of this software, without modification, must refer to the software by the same designation. Redistribution of a modified version of this software (i) may not refer to the modified version by the same designation, or by any confusingly similar designation, and (ii) must refer to the underlying software originally provided by Alliance as “OpenStudio®”. Except to comply with the foregoing, the term “OpenStudio®”, or any confusingly similar designation may not be used to refer to any modified version of this software or any modified version of the underlying software originally provided by Alliance without the prior written consent of Alliance.
10
+
11
+ 4. The name of the copyright holder(s), any contributors, the United States Government, the United States Department of Energy, or any of their employees may not be used to endorse or promote products derived from this software without specific prior written permission from the respective party.
12
+
13
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDER(S) AND ANY CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER(S), ANY CONTRIBUTORS, THE UNITED STATES GOVERNMENT, OR THE UNITED STATES DEPARTMENT OF ENERGY, NOR ANY OF THEIR EMPLOYEES, BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
14
+
@@ -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>