Skip to content

Commit d06c7c1

Browse files
authored
Merge pull request #15 from UNSW-CEEM/master
Move inprogress marginal methodology to separate branch
2 parents 709bb45 + a953323 commit d06c7c1

File tree

2 files changed

+84
-18
lines changed

2 files changed

+84
-18
lines changed

src/nemed/downloader.py

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -225,7 +225,7 @@ def download_genset_map(cache, asof_date=None):
225225
return filtered.sort_values(['GENSETID','EFFECTIVEDATE']).reset_index(drop=True)
226226

227227

228-
def download_dudetailsummary(cache, asof_date=None):
228+
def download_dudetailsummary(cache, include_mlfs=False, asof_date=None):
229229
"""Download the DUDETAILSUMMARY MMS table with mapping of Dispatch Type and Region to DUID
230230
231231
Parameters
@@ -271,14 +271,21 @@ def download_dudetailsummary(cache, asof_date=None):
271271
end_time=latest,
272272
table_name="DUDETAILSUMMARY",
273273
raw_data_location=cache,
274-
select_columns=["START_DATE", "DUID", "DISPATCHTYPE", "REGIONID", "LASTCHANGED"],
274+
select_columns=["START_DATE", "DUID", "DISPATCHTYPE", "REGIONID", \
275+
"TRANSMISSIONLOSSFACTOR", "LASTCHANGED"],
275276
date_filter=None,
276277
fformat="feather",
277278
)
278279
df = pd.concat(df)
279280
df = df.drop(['file_year','file_month', 'LASTCHANGED'], axis=1)
280-
filtered = df.sort_values('START_DATE').drop_duplicates(['DUID'], keep='last')
281-
return filtered.sort_values(['DUID','START_DATE']).reset_index(drop=True)
281+
df["TRANSMISSIONLOSSFACTOR"] =df["TRANSMISSIONLOSSFACTOR"].astype(float)
282+
283+
if include_mlfs:
284+
filtered = df.sort_values(['DUID','START_DATE']).drop_duplicates(['DUID','TRANSMISSIONLOSSFACTOR'], keep='last')
285+
else:
286+
filtered = df.drop(['TRANSMISSIONLOSSFACTOR'], axis=1).sort_values(['DUID','START_DATE'])\
287+
.drop_duplicates(['DUID'], keep='last')
288+
return filtered.reset_index(drop=True)
282289

283290

284291
def _download_duid_mapping():

src/nemed/process.py

Lines changed: 73 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,37 @@ def _total_emissions_process(start_time, end_time, cache, filter_regions=None,
171171
return result
172172

173173

174+
def _adjust_ef_with_mlf(co2_factors_df, cache):
175+
""" Divides emissions factors by MLF for each plant """
176+
# Get MLF data for DUIDs
177+
duid_mlfs = download_dudetailsummary(cache, include_mlfs=True)
178+
duid_mlfs['FILE_DATE'] = pd.to_datetime(duid_mlfs['START_DATE'], format="%Y-%m-%d %H:%M:%S")
179+
180+
df = co2_factors_df.copy()
181+
df.insert(0,'FILE_DATE', pd.to_datetime(df.file_year.astype(str) \
182+
+ '/' + df.file_month.astype(str) + '/01', \
183+
format="%Y-%m-%d"))
184+
185+
df.sort_values(['FILE_DATE', 'DUID'], inplace=True)
186+
duid_mlfs.sort_values(['FILE_DATE', 'DUID'], inplace=True)
187+
adj_factors = pd.merge_asof(left=df,
188+
right=duid_mlfs,
189+
on=['FILE_DATE'],
190+
by=['DUID'],
191+
direction='backward')
192+
193+
# Save raw EF data
194+
adj_factors['unadjusted_CO2E_EF'] = adj_factors['CO2E_EMISSIONS_FACTOR']
195+
196+
# Keep emissions factors where mlf is not given, else adjust by dividing by MLF
197+
adj_factors['CO2E_EMISSIONS_FACTOR'] = np.where(adj_factors['TRANSMISSIONLOSSFACTOR'].isna(), \
198+
adj_factors['CO2E_EMISSIONS_FACTOR'], \
199+
adj_factors['CO2E_EMISSIONS_FACTOR'] / adj_factors['TRANSMISSIONLOSSFACTOR'])
200+
201+
return adj_factors[['file_year', 'file_month', 'DUID', 'CO2E_EMISSIONS_FACTOR', \
202+
'CO2E_ENERGY_SOURCE', 'CO2E_DATA_SOURCE', 'unadjusted_CO2E_EF']]
203+
204+
174205
def _get_duid_emissions_intensities(start_time, end_time, cache):
175206
"""Merges emissions factors from GENSETID to DUID and cleans data"""
176207
co2factors_df = download_plant_emissions_factors(start_time, end_time, cache)
@@ -261,9 +292,9 @@ def _calculate_sent_out(energy_df):
261292

262293
def get_marginal_emitter(start_time, end_time, cache):
263294
"""Retrieves the marginal emissions intensity for each dispatch interval and region. This factor being the weighted
264-
sum of the generators contributing to price-setting. Although not necessarily common, there may be times where
265-
multiple technology types contribute to the marginal emissions - note however that the 'DUID' and 'CO2E_ENERGY_SOURCE'
266-
returned will reflect only the plant which makes the greatest contribution towards price-setting.
295+
sum of the generators contributing to price-setting. Note also the emissions factors for each plant are adjusted by
296+
their corresponding MLF given that the weighted contribution of a generator to price setting is indicative of the
297+
generator's MW contribution as seen at the RRN, not generator site.
267298
268299
Parameters
269300
----------
@@ -283,9 +314,8 @@ def get_marginal_emitter(start_time, end_time, cache):
283314
Columns: Type: Description:
284315
Time datetime Timestamp reported as end of dispatch interval.
285316
Region str The NEM region corresponding to the marginal emitter data.
286-
Intensity_Index float The intensity index [tCO2e/MWh] (as by weighted contributions) of the price-setting generators.
287-
DUID str Unit identifier of the generator with the largest contribution on the margin for that Time-Region.
288-
CO2E_ENERGY_SOURCE str Unit energy source with the largest contribution on the margin for that Time-Region.
317+
Intensity_Index float The intensity index [tCO2e/MWh] (as by weighted contributions and adjusted for MLFs) of the price-setting generators.
318+
Energy source str Combined string of energy sources which are the marginal generators for that Time-Region.
289319
================== ======== ==================================================================================================
290320
291321
"""
@@ -296,6 +326,11 @@ def get_marginal_emitter(start_time, end_time, cache):
296326
## gen_info = download_generators_info(cache)
297327
logger.warning('Warning: Gen_info table only has most recent NEM registration and exemption list. Does not account for retired generators')
298328
co2_factors = _get_duid_emissions_intensities(start_time, end_time, cache)
329+
330+
# Adjust EF by MLF for each plant
331+
co2_factors = _adjust_ef_with_mlf(co2_factors, cache)
332+
333+
# Get Price Setter data (marginal generators)
299334
price_setters = download_pricesetter_files(start_time, end_time, cache)
300335

301336
# Drop Basslink
@@ -313,14 +348,38 @@ def get_marginal_emitter(start_time, end_time, cache):
313348
# Weigh CO2 intensity by 'Increase' contributions
314349
filt_df['weighted_co2_factor'] = filt_df['Increase'] * filt_df['CO2E_EMISSIONS_FACTOR']
315350

316-
# Aggregate sum of weighted CO2 intensities
317-
values = filt_df.groupby(by=['Time','Region'], axis=0).sum()
318-
values = values.reset_index()[['Time','Region','weighted_co2_factor']]
319-
values.rename(columns={'weighted_co2_factor': 'Intensity_Index'}, inplace=True)
320-
321-
# Identify Emissions tech/DUID with the largest contribution (increase) value for Time-Region
322-
source = filt_df.sort_values(['Increase']).drop_duplicates(['Time','Region'], keep="last")[['Time', 'Region', 'DUID', 'CO2E_ENERGY_SOURCE']]
323-
result = values.merge(source, on=['Time','Region'], how='left')
351+
# Count the number of price setters per Time-Region
352+
filt_df = filt_df.merge(right = filt_df.groupby(["Time", "Region"],as_index=False).size(),
353+
how='left',
354+
on=["Time", "Region"])
355+
356+
# Filter out price setter increase factors less than 0.05MW, and Hydro 0 tCO2/e instances where the Hydro generator is not the main contributor to price setting
357+
filt_df_adj = filt_df[filt_df['Increase']>0.05]
358+
filt_df_adj = filt_df_adj[~((filt_df_adj.duplicated(['Time','Region'])) & \
359+
(filt_df_adj['weighted_co2_factor']==0) & \
360+
(filt_df_adj['CO2E_ENERGY_SOURCE'].isin(['Hydro', 'Battery Storage'])) & \
361+
(filt_df_adj['Increase'] < (1/filt_df_adj['size'])))]
362+
363+
# Aggregate CO2 intensities by weighted sum
364+
values = filt_df_adj.groupby(by=['Time','Region'], axis=0).sum()
365+
values.insert(0,'Intensity_Index', values['weighted_co2_factor'] / values['Increase'])
366+
values = values.reset_index()[['Time', 'Region', 'Intensity_Index']]
367+
368+
# Collate all energy source technologies per Time-Region
369+
source = filt_df_adj[["Time", "Region", "CO2E_ENERGY_SOURCE"]]
370+
for tech in source["CO2E_ENERGY_SOURCE"].unique():
371+
source.insert(3, tech, np.where(source['CO2E_ENERGY_SOURCE']==tech, 1, np.nan))
372+
source_count = source.groupby(by=['Time','Region'], axis=0).sum()
373+
374+
# Concatenate string of all tech types contributing to marginal emissions
375+
for tech in source_count.columns:
376+
source_count.loc[:, tech] = np.where(source_count[tech]>0, tech + " + ", "")
377+
source_count.reset_index(inplace=True)
378+
source_count["Energy Source"] = source_count[source_count.columns[~source_count.columns.isin(['Time','Region'])]].agg(''.join, axis=1)
379+
source_count["Energy Source"] = source_count["Energy Source"].str.rstrip(' + ')
380+
381+
# Merge to marginal data
382+
result = values.merge(source_count[["Time", "Region", "Energy Source"]], on=["Time", "Region"], how="left")
324383

325384
return result
326385

0 commit comments

Comments
 (0)