Skip to content

Commit 0fdfa38

Browse files
Initial commit for morpher integration
Integrating pyepwmorph into CEA as a part of the Weather Helper. Have introduced general functionality. Need to make a new pyepwmorph version and then another scenario will become available.
1 parent b17770f commit 0fdfa38

File tree

6 files changed

+2133
-20
lines changed

6 files changed

+2133
-20
lines changed
Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
"""
2+
Functions to enable climate morphing for the EPW files based on pyepwmorph
3+
4+
Title: pyepwmorph
5+
Description: This module provides functions to morph EPW files using the pyepwmorph library, a python package authored by Justin McCarty for accessing global climate models and using them to morph time series weather data in EPW files. An independent application can be found at www.morphepw.app. For a peer-reviewed article that uses pyepwmorph, please see https://doi.org/10.1088/1742-6596/2600/8/082005.
6+
Citation: Follow CRAX example
7+
"""
8+
9+
# general import
10+
import os
11+
import datetime
12+
import shutil
13+
14+
# CEA import
15+
import cea.inputlocator
16+
import cea.config
17+
18+
# pyepwmorph import
19+
from pyepwmorph.tools import workflow as pyepwmorph_workflow
20+
from pyepwmorph.tools import utilities as pyepwmorph_utilities
21+
from pyepwmorph.tools import io as pyepwmorph_io
22+
from pyepwmorph.tools import configuration as pyepwmorph_config
23+
24+
25+
def convert_scenario_names(name):
26+
"""
27+
Convert the scenario names from CEA to the names used in pyepwmorph
28+
29+
:param string name: name of the scenario in CEA
30+
:return: name of the scenario in pyepwmorph
31+
:rtype: string
32+
"""
33+
if name == "Best Case":
34+
return "Best Case Scenario"
35+
elif name == "Moderate":
36+
return "Middle of the Road"
37+
elif name == "Upper Middle": # pending a change in pyepwmorph
38+
return "Upper Middle Scenario"
39+
elif name == "Worst Case":
40+
return "Worst Case Scenario"
41+
else:
42+
raise ValueError(f"Could not interpret the climate pathway: {name}. "
43+
f"Please choose one of the following: best_case, moderate_case, semi_moderate_case, worst_case")
44+
45+
def morphing_workflow(locator, config):
46+
47+
# 1. Read the inputs and create a epw_morph_configuration
48+
# 1.1. project_name and output_directory
49+
project_name = f"{config.general.project.split(os.sep)[-1]}_{config.general.scenario}"
50+
# today = datetime.datetime.now().strftime("%Y%m%d")
51+
52+
output_directory = os.path.dirname(locator.get_weather_file())
53+
shutil.copy(locator.get_weather_file(), os.path.join(output_directory, "before_morph_weather.epw"))
54+
55+
if not os.path.exists(output_directory):
56+
os.makedirs(output_directory)
57+
58+
59+
# 1.2. user_epw_file is specified in the config but defaults to the scenario file
60+
user_epw_file = locator.get_weather_file()
61+
if not os.path.exists(user_epw_file):
62+
raise FileNotFoundError(f"Could not find the specified EPW file: {user_epw_file}")
63+
64+
user_epw_object = pyepwmorph_io.Epw(user_epw_file)
65+
66+
# 1.3. variable choice is specified in the config but defaults to temperature
67+
# variables.choices = temperature, radiation, relative_humidity, wind_speed, pressure, dew_point
68+
user_variables = config.weather_helper.variables
69+
70+
# 1.4 the baseline range against which the calculations are made
71+
# this the range of years used to calculate the baseline, should be taken from the EPW files
72+
try:
73+
baseline_range = user_epw_object.detect_baseline_range()
74+
except:
75+
print("Could not detect the baseline range from the EPW file, using default of 1985-2014")
76+
baseline_range = (1985, 2014) # default if the EPW file does not have the years in it
77+
78+
# 1.5 year can be any future year but defaults to 2050
79+
user_future_year = config.weather_helper.year
80+
user_future_range = pyepwmorph_utilities.calc_period(user_future_year, baseline_range)
81+
82+
# 1.6 climate pathway can be specified in config but defaults to moderate_case
83+
user_climate_pathway = config.weather_helper.climate_pathway
84+
print(f"User pathway is: {user_climate_pathway}")
85+
86+
# 1.7. percentile can be specified in config but defaults to 50 (single choice)
87+
# percentile.choices = 1, 5, 10, 25, 50, 75, 90, 95, 99
88+
user_percentile = int(config.weather_helper.percentile)
89+
90+
# 1.8 model_sources is not something the user can change in CEA
91+
# Sources as of 2025.09.23 using r1i1p1f1
92+
# 'KACE-1-0-G', 'MRI-ESM2-0', 'GFDL-ESM4', 'INM-CM4-8', 'IPSL-CM6A-LR', 'INM-CM5-0', 'ACCESS-CM2', 'MIROC6', 'EC-Earth3-Veg-LR', 'BCC-CSM2-MR'
93+
model_sources = [
94+
'KACE-1-0-G', 'MRI-ESM2-0', 'GFDL-ESM4', 'INM-CM4-8', 'IPSL-CM6A-LR',
95+
'INM-CM5-0', 'ACCESS-CM2', 'MIROC6', 'EC-Earth3-Veg-LR', 'BCC-CSM2-MR'
96+
]
97+
98+
# 1. create the config object for the morpher
99+
morph_config = pyepwmorph_config.MorphConfig(project_name,
100+
user_epw_file,
101+
user_variables,
102+
[convert_scenario_names(user_climate_pathway)],
103+
[user_percentile],
104+
user_future_range,
105+
output_directory,
106+
model_sources=model_sources,
107+
baseline_range=baseline_range)
108+
109+
# 2. Morph the EPW file
110+
print(f"Config pathways are: {morph_config.model_pathways}")
111+
112+
# 2.1 get climate model data
113+
year_model_dict = pyepwmorph_workflow.iterate_compile_model_data(morph_config.model_pathways,
114+
morph_config.model_variables,
115+
morph_config.model_sources,
116+
morph_config.epw.location['longitude'],
117+
morph_config.epw.location['latitude'],
118+
morph_config.percentiles)
119+
120+
121+
# write the new epw file to the output directory
122+
print("Morphing")
123+
morphed_data = pyepwmorph_workflow.morph_epw(morph_config.epw,
124+
morph_config.user_variables,
125+
morph_config.baseline_range,
126+
user_future_range,
127+
year_model_dict,
128+
[p for p in morph_config.model_pathways if p!="historical"][0],
129+
user_percentile)
130+
morphed_data.dataframe['year'] = int(user_future_year)
131+
132+
# morphed_data.write_to_file(os.path.join(morph_config.output_directory,
133+
# f"{str(user_future_year)}_{user_climate_pathway}_{percentile_key}.epw"))
134+
135+
morphed_data.write_to_file(os.path.join(morph_config.output_directory,"weather.epw"))
136+
137+
138+
139+
def main(config):
140+
"""
141+
This script gets a polygon and calculates the zone.shp and the occupancy.dbf and age.dbf inputs files for CEA
142+
"""
143+
assert os.path.exists(
144+
config.scenario), 'Scenario not found: %s' % config.scenario
145+
locator = cea.inputlocator.InputLocator(config.scenario)
146+
print(f"{'=' * 10} Starting the climate morphing workflow for scenario {config.general.scenario} {'=' * 10}")
147+
morphing_workflow(locator, config)
148+
149+
150+
if __name__ == '__main__':
151+
main(cea.config.Configuration())

cea/datamanagement/weather_helper/weather_helper.py

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010

1111
import geopandas as gpd
1212
import requests
13-
import pyepwmorph
13+
import cea.datamanagement.weather_helper.epwmorpher as epwmorpher
1414

1515
import cea.config
1616
import cea.inputlocator
@@ -87,17 +87,22 @@ def main(config):
8787
locator = cea.inputlocator.InputLocator(config.scenario)
8888
weather = config.weather_helper.weather
8989

90-
if not weather:
91-
raise ValueError("No weather file provided. "
92-
"Please specify a weather file or select an option to fetch data automatically. "
93-
"e.g --weather climate.onebuilding.org")
94-
95-
locator.ensure_parent_folder_exists(locator.get_weather_file())
96-
if config.weather_helper.weather == 'climate.onebuilding.org':
97-
print("No weather provided, fetching from online sources.")
98-
fetch_weather_data(locator.get_weather_file(), locator.get_zone_geometry())
90+
if config.weather_helper.morph:
91+
epwmorpher.main(config)
92+
print("Weather morphing complete. The original weather file has been backed up as 'before_morph_weather.epw' in the inputs folder. The morphed weather file according to your percentile of warming choice is now set as 'weather.epw'.")
93+
9994
else:
100-
copy_weather_file(weather, locator)
95+
if not weather:
96+
raise ValueError("No weather file provided. "
97+
"Please specify a weather file or select an option to fetch data automatically. "
98+
"e.g --weather climate.onebuilding.org")
99+
100+
locator.ensure_parent_folder_exists(locator.get_weather_file())
101+
if config.weather_helper.weather == 'climate.onebuilding.org':
102+
print("No weather provided, fetching from online sources.")
103+
fetch_weather_data(locator.get_weather_file(), locator.get_zone_geometry())
104+
else:
105+
copy_weather_file(weather, locator)
101106

102107

103108
if __name__ == '__main__':

cea/default.config

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -134,21 +134,20 @@ year.type = IntegerParameter
134134
year.help = Year to which the weather file is morphed.
135135
year.category = Morph Settings
136136

137-
climate-pathways =
138-
climate-pathways.type = ChoiceParameter
139-
climate-pathways.choices = best_case, moderate_case, worst_case
140-
climate-pathways.help = Climate pathways to morph the weather file to. The "Best Case" pathway corresponds to a 1.8°C increase in global temperature, the "Moderate Case" pathway corresponds to a 2.7°C increase, and the "Worst Case" pathway corresponds to a 4.4°C increase.
141-
climate-pathways.category = Morph Settings
137+
climate-pathway = moderate_case
138+
climate-pathway.type = ChoiceParameter
139+
climate-pathway.choices = Best Case, Moderate, Worst Case
140+
climate-pathway.help = Climate pathway to morph the weather file to. The "Best Case" pathway corresponds to a 1.8°C increase in global temperature, the "Moderate" pathway corresponds to a 2.7°C increase, and the "Worst Case" pathway corresponds to a 4.4°C increase. More information about these pathways can be read here https://www.carbonbrief.org/explainer-how-shared-socioeconomic-pathways-explore-future-climate-change/.
141+
climate-pathway.category = Morph Settings
142142

143143
percentile = 50
144144
percentile.type = ChoiceParameter
145145
percentile.choices = 1, 5, 10, 25, 50, 75, 90, 95, 99
146146
percentile.help = Percentile of the weather file to morph to. The percentile is applied to the temperature and radiation data of the weather file.
147-
percentile.category = Morph Settings
148147

149-
variables =
148+
variables = Temperature
150149
variables.type = MultiChoiceParameter
151-
variables.choices = temperature, radiation, relative_humidity, wind_speed, pressure, dew_point
150+
variables.choices = Temperature, Clouds and Radiation, Humidity, Wind, Pressure, Dew Point
152151
variables.help = List of weather variables to morph.
153152
variables.category = Morph Settings
154153

cea/utilities/epwreader.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,23 @@ def epw_reader(weather_path):
7272

7373
return epw_data
7474

75+
def epw_location(weather_path):
76+
"""
77+
Returns the location of the EPW file as a tuple (latitude, longitude, utc, elevation).
78+
79+
:param weather_path: Path to the EPW file
80+
:return: dict containing latitude, longitude, utc, and elevation
81+
"""
82+
with open(weather_path, 'r') as f:
83+
epw_data = f.readlines()
84+
85+
lat = float(epw_data[0].split(',')[6])
86+
lon = float(epw_data[0].split(',')[7])
87+
utc = float(epw_data[0].split(',')[8])
88+
elevation = float(epw_data[0].split(',')[9].strip("\n"))
89+
90+
return {"latitude": lat, "longitude": lon, "utc": utc, "elevation": elevation}
91+
7592

7693
def calc_horirsky(Tdrybulb, Tdewpoint, N):
7794
"""

0 commit comments

Comments
 (0)