# Please note, this file is for illustrative purposes only and should not be relied on, or treated as a substitute for the Service Terms.

import pandas
import numpy as np
from math import floor, ceil, inf
from datetime import datetime
import pandas as pd

## Apply separate ramp limits

def RLU(t, v_in, r):
    dt = t[2] - t[1]
    r_dt = r * dt
    v = v_in.copy()

    for i in range(1, len(v)):
        change = v[i] - v[i - 1]
        if change > r_dt:
            v[i] = v[i - 1] + r_dt
    return v


def RLD(t, v_in, r):
    dt = t[2] - t[1]
    r_dt = r * dt
    v = v_in.copy()

    for i in range(1, len(v)):
        change = v[i] - v[i - 1]
        if change < -r_dt:
            v[i] = v[i - 1] - r_dt
    return v

# Apply the response curve to a vector of frequencies
def response_curve(f, freq = np.array([49.5,49.8,49.985, 50.015, 50.2, 50.5]), frac=np.array([1, 0.05, 0, 0, -0.05, -1])):
    return np.interp(f, freq, frac)

# Get the minimum frequency
def low_freq_window(t, f, max_lag = 0.55):
    # This version is only looking data quantized to 0.05 s from the hour so it doesn't use t for much
    dt = t[1] - t[0]
    max_step = round(max_lag / dt)
    
    # Calculate lagged rolling minimum
    new_f = f.rolling(max_step+1, min_periods=1).min().shift(0, fill_value=f[0])
    
    return new_f

# Get the maximum
def high_freq_window(t, f, max_lag = 0.55):
    dt = t[1] - t[0]
    max_step = round(max_lag / dt)
    
    # Calculate lagged rolling maximum
    new_f = f.rolling(max_step+1, min_periods=1).max().shift(0, fill_value=f[0])
    
    return new_f

# Calculate lower bound
def lower_bound(t, f, max_lag = 0.55, ramp_rate = 2):
    # Get high frequencies
    new_f = high_freq_window(t, f, max_lag)
    # Calculate response curve
    no_ramp = response_curve(new_f)
    #Apply ramp limit
    ramp = RLU(t, no_ramp, ramp_rate)
    return ramp

# Calculate upper bound
def upper_bound(t, f, max_lag = 0.55, ramp_rate = 2):
    # Get low frequencies
    new_f = low_freq_window(t, f, max_lag)
    # Calculate response curve
    no_ramp = response_curve(new_f)
    #Apply ramp limit
    ramp = RLD(t, no_ramp, ramp_rate)
    
    return ramp


# Read in input file ####

input_file = "sample.csv"
input_df = pandas.read_csv(input_file)

# Convert to datetime, then to seconds from the first datapoint
ts = input_df.t.transform(lambda t: datetime.strptime(t, '%Y-%m-%dT%H:%M:%S.%fZ'))
t = ts.transform(lambda ti : (ti - ts[0]).total_seconds())

# Get other relevant variables
f = input_df.f_hz
response = input_df.p_mw - input_df.baseline_mw
availability = input_df.availability

# Contracted times and MW are read in from a file in the actual tool
contracted = pandas.Series([True for x in range(len(f))])
MW = 100


# Bounds calculation for symmetric service####

lb = lower_bound(t,f) * MW
ub = upper_bound(t,f) * MW

# For the first 0.55 seconds after a response unit begins delivery, after a period of missing data, or after switching from unavailable to available the upper and lower performance bounds will be set to P and -Q respectively.
#This example is for a missing settlement period, beggining of delivery, and availability changes
gap_mask = (t.diff() > 29 * 60) | (availability.diff() != 0)
fill_mask = gap_mask.rolling(11,min_periods=1).apply(lambda x : x.any(), raw=True).astype(bool) # Extend mask 1 second forward for the 0.5 seconds lag and 0.5 seconds ramp time
lb[fill_mask] = -MW
ub[fill_mask] = MW

# Ignore times with unavailability and times not contracted
ua_mask = availability == 0 | ~contracted
lb[ua_mask] = -inf
ub[ua_mask] = inf


# Calculate error ####

## Distance from upper/lower bounds
def bound_error(v, lower, upper):
  error = np.where(v < lower, lower - v, np.where(v > upper, v - upper, 0))
  return error

# Vectors of error values for each time
error = bound_error(response, lb, ub)
proportional_error = error / MW
rolling_error = pandas.Series(proportional_error).rolling(4, min_periods=1).min()

# Final error value used in settlements calculation
result = max(rolling_error)
