Skip to content

Commit ef0c6a0

Browse files
authored
Merge pull request #3893 from yiqiaowang-arch/emission_timeline
Emission timeline for individual building and district
2 parents c2fbdef + df2b69b commit ef0c6a0

18 files changed

+1225
-169
lines changed
Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
import pandas as pd
2+
3+
from cea.config import Configuration
4+
from cea.inputlocator import InputLocator
5+
6+
from cea.analysis.lca.emission_timeline import BuildingEmissionTimeline
7+
from cea.analysis.lca.hourly_operational_emission import OperationalHourlyTimeline
8+
from cea.demand.building_properties import BuildingProperties
9+
from cea.datamanagement.database.envelope_lookup import EnvelopeLookup
10+
from cea.datamanagement.database.components import Feedstocks
11+
from cea.utilities import epwreader
12+
13+
14+
def operational_hourly(config: Configuration) -> None:
15+
locator = InputLocator(config.scenario)
16+
buildings = config.emissions.buildings
17+
weather_path = locator.get_weather_file()
18+
weather_data = epwreader.epw_reader(weather_path)[
19+
["year", "drybulb_C", "wetbulb_C", "relhum_percent", "windspd_ms", "skytemp_C"]
20+
]
21+
building_properties = BuildingProperties(locator, weather_data, buildings)
22+
feedstock_db = Feedstocks.from_locator(locator)
23+
results: list[tuple[str, pd.DataFrame]] = []
24+
25+
for building in buildings:
26+
bpr = building_properties[building]
27+
timeline = OperationalHourlyTimeline(locator, bpr, feedstock_db)
28+
timeline.calculate_operational_emission()
29+
timeline.save_results()
30+
print(
31+
f"Hourly operational emissions for {building} calculated and saved in: {locator.get_lca_operational_hourly_building(building)}."
32+
)
33+
results.append([building, timeline.operational_emission_timeline])
34+
35+
df_by_building = to_ton(sum_by_building(results))
36+
df_by_hour = to_ton(sum_by_index([df for _, df in results]))
37+
df_by_building.to_csv(locator.get_total_yearly_operational_building(), float_format='%.2f')
38+
df_by_hour.to_csv(locator.get_total_yearly_operational_hour(), float_format='%.2f')
39+
print(
40+
f"District-level operational emissions saved in: {locator.get_lca_timeline_folder()}"
41+
)
42+
43+
44+
def total_yearly(config: Configuration) -> None:
45+
locator = InputLocator(scenario=config.scenario)
46+
buildings: list[str] = config.emissions.buildings
47+
if config.emissions.year_end is None:
48+
end_year: int = 2100
49+
else:
50+
end_year: int = config.emissions.year_end
51+
52+
envelope_lookup = EnvelopeLookup.from_locator(locator)
53+
weather_path = locator.get_weather_file()
54+
weather_data = epwreader.epw_reader(weather_path)[
55+
["year", "drybulb_C", "wetbulb_C", "relhum_percent", "windspd_ms", "skytemp_C"]
56+
]
57+
building_properties = BuildingProperties(locator, weather_data, buildings)
58+
results: list[tuple[str, pd.DataFrame]] = []
59+
for building in buildings:
60+
timeline = BuildingEmissionTimeline(
61+
building_properties=building_properties,
62+
envelope_lookup=envelope_lookup,
63+
building_name=building,
64+
locator=locator,
65+
end_year=end_year,
66+
)
67+
timeline.fill_timeline()
68+
timeline.demolish(demolition_year=end_year + 1) # no demolition by default
69+
timeline.save_timeline()
70+
print(
71+
f"Emission timeline for {building} calculated and saved in: {locator.get_lca_timeline_building(building)}."
72+
)
73+
results.append((building, timeline.timeline))
74+
75+
df_by_building = to_ton(sum_by_building(results))
76+
df_by_year = to_ton(sum_by_index([df for _, df in results]))
77+
df_by_building.to_csv(locator.get_total_emissions_building_year_end(year_end=end_year), float_format='%.2f')
78+
df_by_year.to_csv(locator.get_total_emissions_timeline_year_end(year_end=end_year), float_format='%.2f')
79+
print(
80+
f"District-level total emissions saved in: {locator.get_lca_timeline_folder()}"
81+
)
82+
83+
84+
def sum_by_building(result_list: list[tuple[str, pd.DataFrame]]) -> pd.DataFrame:
85+
"""Sum the dataframes in the result list by building. Result in a new dataframe
86+
with buildings as index and summed values as data.
87+
88+
For example:
89+
```
90+
building_1: col1 col2
91+
idx
92+
0 1 2
93+
1 3
94+
building_2: col1 col2
95+
idx
96+
0 5 6
97+
1 7 8
98+
```
99+
The result would be:
100+
```
101+
output: col1 col2
102+
name
103+
building_1 4 6
104+
building_2 12 14
105+
```
106+
107+
:param result_list: a list of tuple, contains building name and its corresponding dataframe.
108+
:type result_list: list[tuple[str, pd.DataFrame]]
109+
:return: a dataframe with buildings as index and summed values as data.
110+
It has the same columns as the input dataframes.
111+
:rtype: pd.DataFrame
112+
"""
113+
# create a new df, each row is the summed value for a building across all its df's indices
114+
summed_df = pd.DataFrame(
115+
data=0.0,
116+
index=[building for building, _ in result_list],
117+
columns=result_list[0][1].columns,
118+
)
119+
summed_df.index.rename("name", inplace=True)
120+
for building, df in result_list:
121+
summed_df.loc[building] += df.sum(axis=0).to_numpy()
122+
return summed_df
123+
124+
125+
def sum_by_index(dfs: list[pd.DataFrame]) -> pd.DataFrame:
126+
"""Sum all values across all dataframes that share the same index.
127+
Useful for getting district-level time-dependent data across multiple buildings.
128+
129+
For example:
130+
```
131+
building_1: col1 col2
132+
index
133+
2000 1 2
134+
2001 3 4
135+
building_2: col1 col2
136+
index
137+
1999 5 6
138+
2000 7 8
139+
building_3: col1 col2
140+
index
141+
2005 1 2
142+
2006 3 4
143+
144+
```
145+
The result would be:
146+
```
147+
output: col1 col2
148+
index
149+
1999 5 6
150+
2000 8 10
151+
2001 3 4
152+
2002 0 0
153+
2003 0 0
154+
2004 0 0
155+
2005 1 2
156+
2006 3 4
157+
```
158+
159+
:param result_list: A list of dataframes to sum.
160+
:type result_list: list[pd.DataFrame]
161+
:return: A dataframe with the summed values.
162+
:rtype: pd.DataFrame
163+
"""
164+
if not dfs:
165+
raise ValueError("result_list must be non-empty")
166+
index_min = min(df.index.min() for df in dfs)
167+
index_max = max(df.index.max() for df in dfs)
168+
out = (
169+
pd.concat(dfs)
170+
.groupby(level=0, sort=True)
171+
.sum(numeric_only=True)
172+
.reindex(pd.RangeIndex(index_min, index_max + 1), fill_value=float(0))
173+
)
174+
out.index.rename(dfs[0].index.name, inplace=True)
175+
return out
176+
177+
178+
def to_ton(df: pd.DataFrame) -> pd.DataFrame:
179+
"""Convert a dataframe in kgCO2 to tonCO2 by dividing all values by 1000, and also rename the columns by changing 'kgCO2' to 'tonCO2'.
180+
181+
:param df: A dataframe with values in kgCO2.
182+
:type df: pd.DataFrame
183+
:return: A dataframe with values in tonCO2.
184+
:rtype: pd.DataFrame
185+
"""
186+
df_ton = df / 1000.0
187+
df_ton.columns = df_ton.columns.str.replace("kgCO2", "tonCO2")
188+
return df_ton
189+
190+
191+
def main(config: Configuration) -> None:
192+
operational_hourly(config)
193+
total_yearly(config)
194+
195+
196+
if __name__ == "__main__":
197+
main(Configuration())

0 commit comments

Comments
 (0)