Skip to content

Commit

Permalink
Merge pull request #14 from UNSW-CEEM/over-frequency_droop_compliance…
Browse files Browse the repository at this point in the history
…_assessment_2020_standard

Validated against benchmarking datasets and 2022-11-12 SA event - with the same settings as the 2015 compliance the results match
  • Loading branch information
exfletch authored Nov 29, 2022
2 parents 4ab861c + 7d6f2aa commit 1e22f32
Show file tree
Hide file tree
Showing 8 changed files with 221 additions and 34 deletions.
1 change: 1 addition & 0 deletions aggregation/aggregate_functions.R
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ find_grouping_cols <- function(settings){
if (settings$response_agg==TRUE){grouping_cols <- c(grouping_cols, "response_category")}
if (settings$zone_agg==TRUE){grouping_cols <- c(grouping_cols, "zone")}
if (settings$compliance_agg==TRUE){grouping_cols <- c(grouping_cols, "compliance_status")}
if (settings$compliance_2020_agg==TRUE){grouping_cols <- c(grouping_cols, "compliance_status_2020")}
if (settings$reconnection_compliance_agg==TRUE){grouping_cols <- c(grouping_cols, "reconnection_compliance_status")}
if (settings$v_excursion_agg==TRUE){grouping_cols <- c(grouping_cols, "voltage_excursion")}
if (settings$circuit_agg==TRUE){grouping_cols <- c(grouping_cols, "site_id", "c_id")}
Expand Down
64 changes: 49 additions & 15 deletions ideal_response/ideal_response_functions.R
Original file line number Diff line number Diff line change
@@ -1,32 +1,52 @@

ideal_response <- function(frequency_data){
#' Calculate ideal f-W response
#'
#' Once the frequency has been within f_hyst of f_ulco for t_hyst then the system should end f-W
#' and ramp back up
#'
#' @param f_ulco upper limit of continuous operation (the point at which f-W droop should start)
#' @param f_hyst hysteresis frequency value
#' @param t_hyst hysteresis time in seconds
#' @param f_upper frequency droop response upper limit. Default 52Hz in most cases
ideal_response <- function(frequency_data, f_ulco, f_hyst, t_hyst, f_upper){
frequency_data <- frequency_data[order(frequency_data$ts),]
start_times <- c()
end_times <- c()
ts <- c()
f <- c()
norm_power <- c()
for(i in 1:length(frequency_data$ts)){
last_60_seconds_of_data <- filter(frequency_data, (ts>frequency_data$ts[i]-60) & (ts<=frequency_data$ts[i]))
if(frequency_data$f[i]>=50.25 & length(start_times)==length(end_times)){
# Look at last t_hyst seconds of data (the 2015 std default is 60s, the 2020 std default is 20s)
last_60_seconds_of_data <- filter(frequency_data, (ts>frequency_data$ts[i]-t_hyst) & (ts<=frequency_data$ts[i]))
# If there is a new over-frequency event (i.e. frequency goes above 50.25Hz and we're not already in a f-W droop)
# then record this as a new start time.
if(frequency_data$f[i]>=f_ulco & length(start_times)==length(end_times)){
start_times <- c(start_times, frequency_data$ts[i])
# And if we don't have any data yet in the ts, then it's the first event, so record this ts. Else append this ts.
if(length(ts)==0){
ts <- c(frequency_data$ts[i])
}else{
ts <- c(ts, frequency_data$ts[i])
}
f <- c(f, frequency_data$f[i])
# Ideal response profile at this time (when frequency is > 50.25Hz for the first time) is set to 1
norm_power <- c(norm_power, 1)
} else if(max(last_60_seconds_of_data$f)<=50.15 & length(start_times)>length(end_times)){
# Check for whether the frequency has been below 50.15Hz for at least 60s for the first time since the droop start
} else if(max(last_60_seconds_of_data$f)<=(f_ulco-f_hyst) & length(start_times)>length(end_times)){
# If so, then add this time to end_times, record the ts, set norm_power to the last value in the norm power list
end_times <- c(end_times, frequency_data$ts[i])
ts <- c(ts, frequency_data$ts[i])
norm_power <- c(norm_power, tail(norm_power, n=1))
f <- c(f, frequency_data$f[i])
# If this interval is during a f-W droop window (may be the first interval!), then copy across these ts and
# frequency data points
} else if(length(start_times)>length(end_times)){
ts <- c(ts, frequency_data$ts[i])
f <- c(f, frequency_data$f[i])
if(frequency_data$f[i]>=50.25){
norm_power <- c(norm_power, min(norm_p_over_frequency(frequency_data$f[i]),tail(norm_power, n=1)))
# Check if frequency has reached a new maximum above 50.25Hz, else norm power is based on the previous norm power
if(frequency_data$f[i]>=f_ulco){
norm_power <- c(norm_power, min(norm_p_over_frequency(frequency_data$f[i], f_ulco, f_upper),
tail(norm_power, n=1)))
} else {
norm_power <- c(norm_power, tail(norm_power, n=1))
}
Expand All @@ -36,10 +56,10 @@ ideal_response <- function(frequency_data){
return(response_data)
}

norm_p_over_frequency <- function(f){
if(f>=50.25 & f<=52.00){
norm_p <- 1 - ((f-50.25)/(52-50.25))
} else if (f>52){
norm_p_over_frequency <- function(f, f_ulco, f_upper){
if(f>=f_ulco & f<=f_upper){
norm_p <- 1 - ((f-f_ulco)/(f_upper-f_ulco))
} else if (f>f_upper){
norm_p <- 0.0
} else {
norm_p = 1.0
Expand Down Expand Up @@ -119,10 +139,15 @@ calc_error_metric_and_compliance_2 <- function(combined_data, ideal_response_dow
end_buffer <- max(ideal_response$ts) - end_buffer
end_buffer_responding <- min(ideal_response$ts) + end_buffer_responding
disconnecting_threshold <- disconnecting_threshold
# If checking both 2015 and 2020 compliance status, then we need to store the first compliance assessment that's
# already happened under a different col name before embarking on the next assessment.
if('compliance_status' %in% colnames(combined_data)){
combined_data <- dplyr::rename(combined_data, "compliance_status_2015" = "compliance_status")
}
# First pass compliance
ideal_response_downsampled <- filter(ideal_response_downsampled, time_group >= start_buffer_t)
ideal_response_downsampled <- filter(ideal_response_downsampled, time_group <= end_buffer)
error_by_c_id <- inner_join(combined_data, ideal_response_downsampled, by=c("ts"="time_group"))
ideal_response_downsampled_f <- filter(ideal_response_downsampled, time_group >= start_buffer_t)
ideal_response_downsampled_f <- filter(ideal_response_downsampled_f, time_group <= end_buffer)
error_by_c_id <- inner_join(combined_data, ideal_response_downsampled_f, by=c("ts"="time_group"))
error_by_c_id <- mutate(error_by_c_id, error=(1 - c_id_norm_power) - ((1 - norm_power) * threshold))
error_by_c_id <- group_by(error_by_c_id, c_id, clean)
error_by_c_id <- summarise(error_by_c_id, min_error=min(error))
Expand All @@ -131,8 +156,8 @@ calc_error_metric_and_compliance_2 <- function(combined_data, ideal_response_dow
combined_data <- left_join(combined_data, error_by_c_id, by=c("c_id","clean"))

# Change 'Non compliant' to 'Non Compliant Responding' where complaint at start
ideal_response_downsampled <- filter(ideal_response_downsampled, time_group <= end_buffer_responding)
error_by_c_id <- inner_join(combined_data, ideal_response_downsampled, by=c("ts"="time_group"))
ideal_response_downsampled_f <- filter(ideal_response_downsampled, time_group <= end_buffer_responding)
error_by_c_id <- inner_join(combined_data, ideal_response_downsampled_f, by=c("ts"="time_group"))
error_by_c_id <- mutate(error_by_c_id, error=(1 - c_id_norm_power) - ((1 - norm_power) * threshold))
error_by_c_id <- group_by(error_by_c_id, c_id, clean)
error_by_c_id <- summarise(error_by_c_id, max_error=max(error), compliance_status=first(compliance_status))
Expand All @@ -141,6 +166,8 @@ calc_error_metric_and_compliance_2 <- function(combined_data, ideal_response_dow
combined_data <- left_join(subset(combined_data, select = -c(compliance_status)), error_by_c_id, by=c("c_id","clean"))

# Set disconnecting categories
# First check that the ideal response doesn't drop low (to close to zero), since many sites would therefore remain as
# 'compliant' rather than 'Disconnect' etc.
min_ideal_response <- min(ideal_response_downsampled$norm_power)
if (min_ideal_response > disconnecting_threshold) {
combined_data <- mutate(combined_data, compliance_status=ifelse(response_category=='4 Disconnect', 'Disconnect/Drop to Zero', compliance_status))
Expand All @@ -151,5 +178,12 @@ calc_error_metric_and_compliance_2 <- function(combined_data, ideal_response_dow
combined_data <- mutate(combined_data, compliance_status=ifelse(response_category=='5 Off at t0', 'Off at t0', compliance_status))
combined_data <- mutate(combined_data, compliance_status=ifelse(response_category=='6 Not enough data', 'Not enough data', compliance_status))

# Now check for whether there was already a compliance_status col when the function was called, rename the new one to
# 2020 std, and keep both in the output combined_data.
if('compliance_status_2015' %in% colnames(combined_data)){
combined_data <- dplyr::rename(combined_data, "compliance_status_2020" = "compliance_status")
combined_data <- dplyr::rename(combined_data, "compliance_status" = "compliance_status_2015")
}

return(combined_data)
}
6 changes: 3 additions & 3 deletions process_input_data/process_input_data_functions.R
Original file line number Diff line number Diff line change
Expand Up @@ -201,15 +201,15 @@ site_categorisation <- function(combined_data){
pv_installation_year_month < "2020-10-01" & s_state == 'SA',
"AS4777.2:2015", Standard_Version)) %>%
# Assumes systems installed in SA during October 2020 are 2015 VDRT" and systems installed during December 2020
# are "transition 2". This means the VDRT group will be small (only 2mon).
# are "Transition 2020-21". This means the VDRT group will be small (only 2mon).
mutate(Standard_Version=ifelse(pv_installation_year_month >= "2020-10-01" & s_state == 'SA' &
pv_installation_year_month < "2020-12-01",
"AS4777.2:2015 VDRT", Standard_Version)) %>%
# Assumes systems installed during December 2020 are "transition 2", and during December 2021 are "transition 2"
# Assumes systems installed during December 2020, and during December 2021 are "Transition 2020-21"
mutate(Standard_Version=ifelse(pv_installation_year_month >= "2020-12-01" &
pv_installation_year_month < "2022-01-01",
"Transition 2020-21", Standard_Version)) %>%
# Assumes systems installed during January 2022 (and onwards) are
# Assumes systems installed during January 2022 (and onwards) are "AS4777.2:2020"
mutate(Standard_Version=ifelse(pv_installation_year_month >= "2022-01-01",
"AS4777.2:2020", Standard_Version))

Expand Down
70 changes: 59 additions & 11 deletions run_analysis/run_analysis.R
Original file line number Diff line number Diff line change
Expand Up @@ -43,12 +43,12 @@ validate_pre_event_interval <- function(pre_event_interval, load_start_time, loa
#' @param frequency_data The frequency data input to the tool
#' @param region_to_load Which region (state) to pull from the frequency data
#' @return Ideal response time series dataframe
ideal_response_from_frequency <- function(frequency_data, region_to_load) {
ideal_response_from_frequency <- function(frequency_data, region_to_load, f_ulco, f_hyst, t_hyst, f_upper) {
if (dim(frequency_data)[1] > 0) {
temp_f_data <- select(frequency_data, ts, region_to_load)
temp_f_data <- setnames(temp_f_data, c(region_to_load), c("f"))
temp_f_data <- mutate(temp_f_data, f = as.numeric(f))
ideal_response_to_plot <- ideal_response(temp_f_data)
ideal_response_to_plot <- ideal_response(temp_f_data, f_ulco, f_hyst, t_hyst, f_upper)
} else {
temp_f_data <- data.frame()
ideal_response_to_plot <- data.frame()
Expand Down Expand Up @@ -200,8 +200,8 @@ upscale_and_summarise_disconnections <- function(circuit_summary, manufacturer_i
check_grouping <- function(settings) {
if (settings$standard_agg==FALSE & settings$pst_agg==FALSE & settings$grouping_agg==FALSE &
settings$manufacturer_agg==FALSE & settings$model_agg==FALSE & settings$zone_agg==FALSE &
settings$circuit_agg==TRUE & settings$compliance_agg==TRUE & settings$reconnection_compliance_agg &
settings$v_excursion_agg==FALSE){
settings$circuit_agg==TRUE & settings$compliance_agg==TRUE & settings$compliance_2020_agg==TRUE
& settings$reconnection_compliance_agg & settings$v_excursion_agg==FALSE){
no_grouping=TRUE
} else {
no_grouping=FALSE
Expand All @@ -223,13 +223,38 @@ run_analysis <- function(data, settings) {

if (length(errors$errors) == 0) {
logging::logdebug("error checks passed", logger=logger)


# First get ideal response profile for 2015 standard, AS4777.2:2015.
response_data <- ideal_response_from_frequency(
data$frequency_data, settings$region_to_load
data$frequency_data, settings$region_to_load, f_ulco=50.25, f_hyst=0.1, t_hyst=60, f_upper=52.00
)
data$ideal_response_to_plot <- response_data$ideal_response_to_plot
data$region_frequency <- response_data$region_frequency


# Next, get ideal response profile for 2020 standard, AS4777.2:2020 (uses different settings based on region).
# Currently, WA (Western Power) uses "Australia B", TAS uses "Australia C", all other NEM regions use "Australia A".
if(settings$region_to_load == "WA"){
f_ulco <- 50.15
f_hyst <- 0.1
t_hyst <- 20
f_upper <- 52.00
} else if(settings$region_to_load == "TAS"){
f_ulco <- 50.5
f_hyst <- 0.05
t_hyst <- 20
f_upper <- 55.00
} else{
f_ulco <- 50.25
f_hyst <- 0.1
t_hyst <- 20
f_upper <- 52.00
}
response_data_2020 <- ideal_response_from_frequency(
data$frequency_data, settings$region_to_load, f_ulco, f_hyst, t_hyst, f_upper
)
data$ideal_response_to_plot_2020 <- response_data_2020$ideal_response_to_plot
data$region_frequency_2020 <- response_data_2020$region_frequency

# -------- filter combined data by user filters --------
combined_data_f <- filter_combined_data(
data$combined_data, data$off_grid_postcodes, settings$cleaned, settings$size_groupings, settings$standards,
Expand Down Expand Up @@ -291,7 +316,7 @@ run_analysis <- function(data, settings) {
if(length(combined_data_f$ts) > 0){
combined_data_f <- determine_performance_factors(combined_data_f, settings$pre_event_interval)

# -------- determine f-W compliance --------
# -------- determine AS4777.2:2015 over-frequency f-W compliance --------
if(dim(data$ideal_response_to_plot)[1]>0){
ideal_response_downsampled <- down_sample_1s(
data$ideal_response_to_plot, settings$duration, min(combined_data_f$ts))
Expand All @@ -313,7 +338,30 @@ run_analysis <- function(data, settings) {
if (length(settings$compliance) < 8) {
combined_data_f <- filter(combined_data_f, compliance_status %in% settings$compliance)
}


# -------- determine AS4777.2:2020 over-frequency f-W compliance --------
if(dim(data$ideal_response_to_plot_2020)[1]>0){
ideal_response_downsampled_2020 <- down_sample_1s(
data$ideal_response_to_plot_2020, settings$duration, min(combined_data_f$ts))
data$ideal_response_downsampled_2020 <- ideal_response_downsampled_2020
combined_data_f <-
calc_error_metric_and_compliance_2(combined_data_f,
ideal_response_downsampled_2020,
data$ideal_response_to_plot_2020,
settings$compliance_threshold_2020,
settings$start_buffer_2020,
settings$end_buffer_2020,
settings$end_buffer_responding_2020,
settings$disconnecting_threshold)
combined_data_f <- mutate(
combined_data_f, compliance_status_2020=ifelse(compliance_status_2020 %in% c(NA), "NA", compliance_status_2020))
} else {
combined_data_f <- mutate(combined_data_f, compliance_status_2020="Undefined")
}
if (length(settings$compliance_2020) < 8) {
combined_data_f <- filter(combined_data_f, compliance_status_2020 %in% settings$compliance_2020)
}

# -------- determine reconnection compliace --------
logdebug('Set reconnection compliance values', logger=logger)
max_power <- data$db$get_max_circuit_powers(settings$region_to_load)
Expand Down Expand Up @@ -367,7 +415,7 @@ run_analysis <- function(data, settings) {
"s_postcode", "pv_installation_year_month", "Standard_Version", "Grouping", "sum_ac",
"clean", "manufacturer", "model", "site_performance_factor", "response_category",
"zone", "distance", "lat", "lon", "e", "con_type", "first_ac", "polarity",
"compliance_status", "reconnection_compliance_status",
"compliance_status","compliance_status_2020", "reconnection_compliance_status",
"manual_droop_compliance", "manual_reconnect_compliance", "reconnection_time",
"ramp_above_threshold", "c_id_daily_norm_power", "max_power", "ufls_status",
"pre_event_sampled_seconds", "post_event_sampled_seconds", "ufls_status_v",
Expand Down Expand Up @@ -404,7 +452,7 @@ run_analysis <- function(data, settings) {
circ_sum_cols <- c("site_id", "c_id", "s_state", "s_postcode", "pv_installation_year_month",
"Standard_Version", "Grouping", "sum_ac", "clean", "manufacturer", "model",
"response_category", "zone", "distance", "lat", "lon", "con_type", "first_ac", "polarity",
"compliance_status", "reconnection_compliance_status", "manual_droop_compliance",
"compliance_status", "compliance_status_2020", "reconnection_compliance_status", "manual_droop_compliance",
"manual_reconnect_compliance", "reconnection_time", "ramp_above_threshold", "max_power",
"ufls_status", "pre_event_sampled_seconds", "post_event_sampled_seconds", "ufls_status_v",
"pre_event_v_mean", "post_event_v_mean", "vmax_max", "vmin_min", "vmean_mean",
Expand Down
Loading

0 comments on commit 1e22f32

Please sign in to comment.