From dc3c1c4121a0b83c2f2d9c103ee67acec801faeb Mon Sep 17 00:00:00 2001 From: "pierre-francois.duc" Date: Tue, 10 May 2022 12:32:54 +0200 Subject: [PATCH 01/33] Change variable names and refactor Appliance class - Mapping between old and new parameters in file constants.py - Move the class Appliance outside of User class --- ramp/core/constants.py | 41 +++++ ramp/core/core.py | 274 +++++++++++++++++++++++--------- ramp/core/stochastic_process.py | 50 +++--- 3 files changed, 261 insertions(+), 104 deletions(-) create mode 100644 ramp/core/constants.py diff --git a/ramp/core/constants.py b/ramp/core/constants.py new file mode 100644 index 00000000..e3115079 --- /dev/null +++ b/ramp/core/constants.py @@ -0,0 +1,41 @@ +OLD_TO_NEW_MAPPING = { + "name": "user_name", + "n_users": "num_users", + "us_pref": "user_preference", + "P": "power", + "P_11": "p_11", + "P_12": "p_12", + "P_21": "p_21", + "P_22": "p_22", + "P_31": "p_31", + "P_32": "p_32", + "pref_index": "pref_index", + "P_series": "p_series", + "Thermal_P_var": "thermal_p_var", + "w": "num_windows", + "cw11": "cw11", + "cw12": "cw12", + "cw21": "cw21", + "cw22": "cw22", + "cw31": "cw31", + "cw32": "cw32", + "fixed": "fixed", + "flat": "flat", + "c": "func_cycle", + "t": "func_time", + "n": "number", + "occasional_use": "occasional_use", + "r_c1": "r_c1", + "r_c2": "r_c2", + "r_c3": "r_c3", + "r_t": "time_fraction_random_variability", + "random_var_w": "random_var_w", + "t_11": "t_11", + "t_12": "t_12", + "wd_we_type": "wd_we_type", + "window_1": "window_1", + "window_2": "window_2", + "window_3": "window_3", +} + +NEW_TO_OLD_MAPPING = {value: key for (key, value) in OLD_TO_NEW_MAPPING.items()} diff --git a/ramp/core/core.py b/ramp/core/core.py index a7463d64..93ea4964 100644 --- a/ramp/core/core.py +++ b/ramp/core/core.py @@ -4,6 +4,8 @@ import numpy as np import numpy.ma as ma import pandas as pd +from ramp.core.constants import NEW_TO_OLD_MAPPING, OLD_TO_NEW_MAPPING + #%% Definition of Python classes that constitute the model architecture ''' The code is based on two concatenated python classes, namely 'User' and @@ -13,83 +15,197 @@ created to define windows of use and, if needed, specific duty cycles ''' -#Define the outer python class that represents 'User classes' -class User(): - - def __init__(self, name = "", n_users = 1, us_pref = 0): - self.user_name = name - self.num_users = n_users #specifies the number of users within the class - self.user_preference = us_pref #allows to check if random number coincides with user preference, to distinguish between various appliance_use options (e.g. different cooking options) - self.App_list = [] #each instance of User (i.e. each user class) has its own list of Appliances - -#Define the inner class for modelling user's appliances within the correspoding user class - class Appliance(): - - def __init__(self,user, n = 1, P = 0, w = 1, t = 0, r_t = 0, c = 1, fixed = 'no', fixed_cycle = 0, occasional_use = 1, flat = 'no', thermal_P_var = 0, pref_index = 0, wd_we_type = 2, P_series = False): - self.user = user #user to which the appliance is bounded - self.number = n #number of appliances of the specified kind - self.num_windows = w #number of functioning windows to be considered - self.func_time = t #total time the appliance is on during the day - self.r_t = r_t #percentage of total time of use that is subject to random variability - self.func_cycle = c #minimum time the appliance is kept on after switch-on event - self.fixed = fixed #if 'yes', all the 'n' appliances of this kind are always switched-on together - self.activate = fixed_cycle #if equal to 1,2 or 3, respectively 1,2 or 3 duty cycles can be modelled, for different periods of the day - self.occasional_use = occasional_use #probability that the appliance is always (i.e. everyday) included in the mix of appliances that the user actually switches-on during the day - self.flat = flat #allows to model appliances that are not subject to any kind of random variability, such as public lighting - self.Thermal_P_var = thermal_P_var #allows to randomly variate the App power within a range - self.Pref_index = pref_index #defines preference index for association with random User daily preference behaviour - self.wd_we = wd_we_type #defines if the App is associated with weekdays or weekends | 0 is wd 1 is we 2 is all week - if P_series == False and isinstance(P, pd.DataFrame) == False: #check if the user defined P as timeseries - self.POWER = P*np.ones(365) #treat the power as single value for the entire year + +class User: + def __init__(self, user_name="", num_users=1, user_preference=0): + self.user_name = user_name + self.num_users = num_users # specifies the number of users within the class + self.user_preference = user_preference # allows to check if random number coincides with user preference, to distinguish between various appliance_use options (e.g. different cooking options) + self.App_list = ( + [] + ) # each instance of User (i.e. each user class) has its own list of Appliances + + def add_appliance(self, *args, **kwargs): + # I would add the appliance explicitely here, unless the appliance works only if a windows is defined + return Appliance(self, *args, **kwargs) + def Appliance( + self, + user, + n=1, + POWER=0, + w=1, + t=0, + r_t=0, + c=1, + fixed="no", + fixed_cycle=0, + occasional_use=1, + flat="no", + thermal_P_var=0, + pref_index=0, + wd_we_type=2, + P_series=False, + name="", + ): + """Back-compatibility with legacy code""" + return self.add_appliance( + number=n, + power=POWER, + num_windows=w, + func_time=t, + time_fraction_random_variability=r_t, + func_cycle=c, + fixed=fixed, + fixed_cycle=fixed_cycle, + occasional_use=occasional_use, + flat=flat, + thermal_p_var=thermal_P_var, + pref_index=pref_index, + wd_we_type=wd_we_type, + P_series=P_series, + name=name, + ) +# Define the inner class for modelling user's appliances within the correspoding user class +class Appliance: + def __init__( + self, + user, + number=1, + power=0, + num_windows=1, + func_time=0, + time_fraction_random_variability=0, + func_cycle=1, + fixed="no", + fixed_cycle=0, + occasional_use=1, + flat="no", + thermal_p_var=0, + pref_index=0, + wd_we_type=2, + p_series=False, + name="", + ): + self.user = user #user to which the appliance is bounded + self.name = name + self.number = number #number of appliances of the specified kind + self.num_windows = num_windows #number of functioning windows to be considered + self.func_time = func_time #total time the appliance is on during the day + self.time_fraction_random_variability = time_fraction_random_variability #percentage of total time of use that is subject to random variability + self.func_cycle = ( + func_cycle #minimum time the appliance is kept on after switch-on event + ) + self.fixed = fixed #if 'yes', all the 'n' appliances of this kind are always switched-on together + self.fixed_cycle = fixed_cycle #if equal to 1,2 or 3, respectively 1,2 or 3 duty cycles can be modelled, for different periods of the day + self.occasional_use = occasional_use #probability that the appliance is always (i.e. everyday) included in the mix of appliances that the user actually switches-on during the day + self.flat = flat #allows to model appliances that are not subject to any kind of random variability, such as public lighting + self.thermal_p_var = ( + thermal_p_var #allows to randomly variate the App power within a range + ) + self.pref_index = pref_index #defines preference index for association with random User daily preference behaviour + self.wd_we_type = wd_we_type #defines if the App is associated with weekdays or weekends | 0 is wd 1 is we 2 is all week + #TODO detect if power is a timeseries or a constant and get rid of P_series altogether + if ( + p_series is False and isinstance(power, pd.DataFrame) is False + ): #check if the user defined P as timeseries + self.power = power * np.ones( + 365 + ) #treat the power as single value for the entire year + else: + self.power = power.values[ + :, 0 + ] #if a timeseries is given the power is treated as so + + # attributes initialized by self.windows + self.random_var_w = None + self.daily_use = None + self.daily_use_masked = None + + # attributes used for specific fixed and random cycles + self.p_11 = None + self.p_12 = None + self.t_11 = None + self.t_12 = None + self.r_c1 = None + self.p_21 = None + self.p_22 = None + self.t_21 = None + self.t_22 = None + self.r_c2 = None + self.p_31 = None + self.p_32 = None + self.t_31 = None + self.t_32 = None + self.r_c3 = None + + # attribute used for cycle_behaviour + self.cw11 = np.array([0, 0]) + self.cw12 = np.array([0, 0]) + self.cw21 = np.array([0, 0]) + self.cw22 = np.array([0, 0]) + self.cw31 = np.array([0, 0]) + self.cw32 = np.array([0, 0]) + + self.random_cycle1 = np.array([]) + self.random_cycle2 = np.array([]) + self.random_cycle3 = np.array([]) + else: - self.POWER = P.values[:,0] #if a timeseries is given the power is treated as so - - def windows(self, w1 = np.array([0,0]), w2 = np.array([0,0]),r_w = 0, w3 = np.array([0,0])): - self.window_1 = w1 #array of start and ending time for window of use #1 - self.window_2 = w2 #array of start and ending time for window of use #2 - self.window_3 = w3 #array of start and ending time for window of use #3 - self.random_var_w = r_w #percentage of variability in the start and ending times of the windows - self.daily_use = np.zeros(1440) #create an empty daily use profile - self.daily_use[w1[0]:(w1[1])] = np.full(np.diff(w1),0.001) #fills the daily use profile with infinitesimal values that are just used to identify the functioning windows - self.daily_use[w2[0]:(w2[1])] = np.full(np.diff(w2),0.001) #same as above for window2 - self.daily_use[w3[0]:(w3[1])] = np.full(np.diff(w3),0.001) #same as above for window3 - self.daily_use_masked = np.zeros_like(ma.masked_not_equal(self.daily_use,0.001)) #apply a python mask to the daily_use array to make only functioning windows 'visibile' - self.random_var_1 = int(r_w*np.diff(w1)) #calculate the random variability of window1, i.e. the maximum range of time they can be enlarged or shortened - self.random_var_2 = int(r_w*np.diff(w2)) #same as above - self.random_var_3 = int(r_w*np.diff(w3)) #same as above - self.user.App_list.append(self) #automatically appends the appliance to the user's appliance list - - #if needed, specific duty cycles can be defined for each Appliance, for a maximum of three different ones - def specific_cycle_1(self, P_11 = 0, t_11 = 0, P_12 = 0, t_12 = 0, r_c1 = 0): - self.P_11 = P_11 #power absorbed during first part of the duty cycle - self.t_11 = t_11 #duration of first part of the duty cycle - self.P_12 = P_12 #power absorbed during second part of the duty cycle - self.t_12 = t_12 #duration of second part of the duty cycle - self.r_c1 = r_c1 #random variability of duty cycle segments duration - self.fixed_cycle1 = np.concatenate(((np.ones(t_11)*P_11),(np.ones(t_12)*P_12))) #create numpy array representing the duty cycle - - def specific_cycle_2(self, P_21 = 0, t_21 = 0, P_22 = 0, t_22 = 0, r_c2 = 0): - self.P_21 = P_21 #same as for cycle1 - self.t_21 = t_21 - self.P_22 = P_22 - self.t_22 = t_22 - self.r_c2 = r_c2 - self.fixed_cycle2 = np.concatenate(((np.ones(t_21)*P_21),(np.ones(t_22)*P_22))) - - def specific_cycle_3(self, P_31 = 0, t_31 = 0, P_32 = 0, t_32 = 0, r_c3 = 0): - self.P_31 = P_31 #same as for cycle1 - self.t_31 = t_31 - self.P_32 = P_32 - self.t_32 = t_32 - self.r_c3 = r_c3 - self.fixed_cycle3 = np.concatenate(((np.ones(t_31)*P_31),(np.ones(t_32)*P_32))) - - #different time windows can be associated with different specific duty cycles - def cycle_behaviour(self, cw11 = np.array([0,0]), cw12 = np.array([0,0]), cw21 = np.array([0,0]), cw22 = np.array([0,0]), cw31 = np.array([0,0]), cw32 = np.array([0,0])): - self.cw11 = cw11 #first window associated with cycle1 - self.cw12 = cw12 #second window associated with cycle1 - self.cw21 = cw21 #same for cycle2 - self.cw22 = cw22 - self.cw31 = cw31 #same for cycle 3 - self.cw32 = cw32 - + + def windows(self, window_1 = np.array([0,0]), window_2 = np.array([0,0]),random_var_w = 0, window_3 = np.array([0,0])): + self.window_1 = window_1 #array of start and ending time for window of use #1 + self.window_2 = window_2 #array of start and ending time for window of use #2 + self.window_3 = window_3 #array of start and ending time for window of use #3 + self.random_var_w = random_var_w #percentage of variability in the start and ending times of the windows + self.daily_use = np.zeros(1440) #create an empty daily use profile + self.daily_use[window_1[0]:(window_1[1])] = np.full(np.diff(window_1),0.001) #fills the daily use profile with infinitesimal values that are just used to identify the functioning windows + self.daily_use[window_2[0]:(window_2[1])] = np.full(np.diff(window_2),0.001) #same as above for window2 + self.daily_use[window_3[0]:(window_3[1])] = np.full(np.diff(window_3),0.001) #same as above for window3 + self.daily_use_masked = np.zeros_like(ma.masked_not_equal(self.daily_use,0.001)) #apply a python mask to the daily_use array to make only functioning windows 'visibile' + self.random_var_1 = int(random_var_w*np.diff(window_1)) #calculate the random variability of window1, i.e. the maximum range of time they can be enlarged or shortened + self.random_var_2 = int(random_var_w*np.diff(window_2)) #same as above + self.random_var_3 = int(random_var_w*np.diff(window_3)) #same as above + self.user.App_list.append(self) #automatically appends the appliance to the user's appliance list + + if self.fixed_cycle == 1: + self.cw11 = self.window_1 + self.cw12 = self.window_2 + + #if needed, specific duty cycles can be defined for each Appliance, for a maximum of three different ones + def specific_cycle_1(self, p_11 = 0, t_11 = 0, p_21 = 0, t_21 = 0, r_c1 = 0): + self.p_11 = p_11 #power absorbed during first part of the duty cycle + self.t_11 = t_11 #duration of first part of the duty cycle + self.p_12 = p_21 #power absorbed during second part of the duty cycle + self.t_12 = t_21 #duration of second part of the duty cycle + self.r_c1 = r_c1 #random variability of duty cycle segments duration + # Below is not used + self.fixed_cycle1 = np.concatenate(((np.ones(t_11)*p_11),(np.ones(t_21)*p_21))) #create numpy array representing the duty cycle + + def specific_cycle_2(self, p_21 = 0, t_21 = 0, p_22 = 0, t_22 = 0, r_c2 = 0): + self.p_21 = p_21 #same as for cycle1 + self.t_21 = t_21 + self.p_22 = p_22 + self.t_22 = t_22 + self.r_c2 = r_c2 + # Below is not used + self.fixed_cycle2 = np.concatenate(((np.ones(t_21)*p_21),(np.ones(t_22)*p_22))) + + def specific_cycle_3(self, p_31 = 0, t_31 = 0, p_32 = 0, t_32 = 0, r_c3 = 0): + self.p_31 = p_31 #same as for cycle1 + self.t_31 = t_31 + self.p_32 = p_32 + self.t_32 = t_32 + self.r_c3 = r_c3 + # Below is not used + self.fixed_cycle3 = np.concatenate(((np.ones(t_31)*p_31),(np.ones(t_32)*p_32))) + + #different time windows can be associated with different specific duty cycles + def cycle_behaviour(self, cw11 = np.array([0,0]), cw12 = np.array([0,0]), cw21 = np.array([0,0]), cw22 = np.array([0,0]), cw31 = np.array([0,0]), cw32 = np.array([0,0])): + + # only used around line 223 + self.cw11 = cw11 #first window associated with cycle1 + self.cw12 = cw12 #second window associated with cycle1 + self.cw21 = cw21 #same for cycle2 + self.cw22 = cw22 + self.cw31 = cw31 #same for cycle 3 + self.cw32 = cw32 diff --git a/ramp/core/stochastic_process.py b/ramp/core/stochastic_process.py index 7d6451e6..8acf933d 100644 --- a/ramp/core/stochastic_process.py +++ b/ramp/core/stochastic_process.py @@ -57,14 +57,14 @@ def Stochastic_Process(j): else: pass - if App.Pref_index == 0: + if App.pref_index == 0: pass else: - if rand_daily_pref == App.Pref_index: #evaluates if daily preference coincides with the randomised daily preference number + if rand_daily_pref == App.pref_index: #evaluates if daily preference coincides with the randomised daily preference number pass else: continue - if App.wd_we == Year_behaviour[prof_i] or App.wd_we == 2 : #checks if the app is allowed in the given yearly behaviour pattern + if App.wd_we_type == Year_behaviour[prof_i] or App.wd_we_type == 2 : #checks if the app is allowed in the given yearly behaviour pattern pass else: continue @@ -104,28 +104,28 @@ def Stochastic_Process(j): App.power = App.POWER[prof_i] #random variability is applied to the total functioning time and to the duration of the duty cycles, if they have been specified - random_var_t = random.uniform((1-App.r_t),(1+App.r_t)) - if App.activate == 1: - App.p_11 = App.P_11*(random.uniform((1-App.Thermal_P_var),(1+App.Thermal_P_var))) #randomly variates the power of thermal apps, otherwise variability is 0 - App.p_12 = App.P_12*(random.uniform((1-App.Thermal_P_var),(1+App.Thermal_P_var))) #randomly variates the power of thermal apps, otherwise variability is 0 + random_var_t = random.uniform((1-App.time_fraction_random_variability),(1+App.time_fraction_random_variability)) + if App.fixed_cycle == 1: + App.p_11 = App.p_11*(random.uniform((1-App.thermal_p_var),(1+App.thermal_p_var))) #randomly variates the power of thermal apps, otherwise variability is 0 + App.p_12 = App.p_12*(random.uniform((1-App.thermal_p_var),(1+App.thermal_p_var))) #randomly variates the power of thermal apps, otherwise variability is 0 random_cycle1 = np.concatenate(((np.ones(int(App.t_11*(random.uniform((1+App.r_c1),(1-App.r_c1)))))*App.p_11),(np.ones(int(App.t_12*(random.uniform((1+App.r_c1),(1-App.r_c1)))))*App.p_12))) #randomise also the fixed cycle random_cycle2 = random_cycle1 random_cycle3 = random_cycle1 - elif App.activate == 2: - App.p_11 = App.P_11*(random.uniform((1-App.Thermal_P_var),(1+App.Thermal_P_var))) #randomly variates the power of thermal apps, otherwise variability is 0 - App.p_12 = App.P_12*(random.uniform((1-App.Thermal_P_var),(1+App.Thermal_P_var))) #randomly variates the power of thermal apps, otherwise variability is 0 - App.p_21 = App.P_21*(random.uniform((1-App.Thermal_P_var),(1+App.Thermal_P_var))) #randomly variates the power of thermal apps, otherwise variability is 0 - App.p_22 = App.P_22*(random.uniform((1-App.Thermal_P_var),(1+App.Thermal_P_var))) #randomly variates the power of thermal apps, otherwise variability is 0 + elif App.fixed_cycle == 2: + App.p_11 = App.p_11*(random.uniform((1-App.thermal_p_var),(1+App.thermal_p_var))) #randomly variates the power of thermal apps, otherwise variability is 0 + App.p_12 = App.p_12*(random.uniform((1-App.thermal_p_var),(1+App.thermal_p_var))) #randomly variates the power of thermal apps, otherwise variability is 0 + App.p_21 = App.p_21*(random.uniform((1-App.thermal_p_var),(1+App.thermal_p_var))) #randomly variates the power of thermal apps, otherwise variability is 0 + App.p_22 = App.p_22*(random.uniform((1-App.thermal_p_var),(1+App.thermal_p_var))) #randomly variates the power of thermal apps, otherwise variability is 0 random_cycle1 = np.concatenate(((np.ones(int(App.t_11*(random.uniform((1+App.r_c1),(1-App.r_c1)))))*App.p_11),(np.ones(int(App.t_12*(random.uniform((1+App.r_c1),(1-App.r_c1)))))*App.p_12))) #randomise also the fixed cycle random_cycle2 = np.concatenate(((np.ones(int(App.t_21*(random.uniform((1+App.r_c2),(1-App.r_c2)))))*App.p_21),(np.ones(int(App.t_22*(random.uniform((1+App.r_c2),(1-App.r_c2)))))*App.p_22))) #randomise also the fixed cycle random_cycle3 = random_cycle1 - elif App.activate == 3: - App.p_11 = App.P_11*(random.uniform((1-App.Thermal_P_var),(1+App.Thermal_P_var))) #randomly variates the power of thermal apps, otherwise variability is 0 - App.p_12 = App.P_12*(random.uniform((1-App.Thermal_P_var),(1+App.Thermal_P_var))) #randomly variates the power of thermal apps, otherwise variability is 0 - App.p_21 = App.P_21*(random.uniform((1-App.Thermal_P_var),(1+App.Thermal_P_var))) #randomly variates the power of thermal apps, otherwise variability is 0 - App.p_22 = App.P_22*(random.uniform((1-App.Thermal_P_var),(1+App.Thermal_P_var))) #randomly variates the power of thermal apps, otherwise variability is 0 - App.p_31 = App.P_31*(random.uniform((1-App.Thermal_P_var),(1+App.Thermal_P_var))) #randomly variates the power of thermal apps, otherwise variability is 0 - App.p_32 = App.P_32*(random.uniform((1-App.Thermal_P_var),(1+App.Thermal_P_var))) #randomly variates the power of thermal apps, otherwise variability is 0 + elif App.fixed_cycle == 3: + App.p_11 = App.p_11*(random.uniform((1-App.thermal_p_var),(1+App.thermal_p_var))) #randomly variates the power of thermal apps, otherwise variability is 0 + App.p_12 = App.p_12*(random.uniform((1-App.thermal_p_var),(1+App.thermal_p_var))) #randomly variates the power of thermal apps, otherwise variability is 0 + App.p_21 = App.p_21*(random.uniform((1-App.thermal_p_var),(1+App.thermal_p_var))) #randomly variates the power of thermal apps, otherwise variability is 0 + App.p_22 = App.p_22*(random.uniform((1-App.thermal_p_var),(1+App.thermal_p_var))) #randomly variates the power of thermal apps, otherwise variability is 0 + App.p_31 = App.p_31*(random.uniform((1-App.thermal_p_var),(1+App.thermal_p_var))) #randomly variates the power of thermal apps, otherwise variability is 0 + App.p_32 = App.p_32*(random.uniform((1-App.thermal_p_var),(1+App.thermal_p_var))) #randomly variates the power of thermal apps, otherwise variability is 0 random_cycle1 = random.choice([np.concatenate(((np.ones(int(App.t_11*(random.uniform((1+App.r_c1),(1-App.r_c1)))))*App.p_11),(np.ones(int(App.t_12*(random.uniform((1+App.r_c1),(1-App.r_c1)))))*App.p_12))),np.concatenate(((np.ones(int(App.t_12*(random.uniform((1+App.r_c1),(1-App.r_c1)))))*App.p_12),(np.ones(int(App.t_11*(random.uniform((1+App.r_c1),(1-App.r_c1)))))*App.p_11)))]) #randomise also the fixed cycle random_cycle2 = random.choice([np.concatenate(((np.ones(int(App.t_21*(random.uniform((1+App.r_c2),(1-App.r_c2)))))*App.p_21),(np.ones(int(App.t_22*(random.uniform((1+App.r_c2),(1-App.r_c2)))))*App.p_22))),np.concatenate(((np.ones(int(App.t_22*(random.uniform((1+App.r_c2),(1-App.r_c2)))))*App.p_22),(np.ones(int(App.t_21*(random.uniform((1+App.r_c2),(1-App.r_c2)))))*App.p_21)))]) random_cycle3 = random.choice([np.concatenate(((np.ones(int(App.t_31*(random.uniform((1+App.r_c3),(1-App.r_c3)))))*App.p_31),(np.ones(int(App.t_32*(random.uniform((1+App.r_c3),(1-App.r_c3)))))*App.p_32))),np.concatenate(((np.ones(int(App.t_32*(random.uniform((1+App.r_c3),(1-App.r_c3)))))*App.p_32),(np.ones(int(App.t_31*(random.uniform((1+App.r_c3),(1-App.r_c3)))))*App.p_31)))])#this is to avoid that all cycles are sincronous @@ -222,7 +222,7 @@ def Stochastic_Process(j): coincidence = on_number #randomly selects how many apps are on at the same time for each app type based on the above probabilistic algorithm else: coincidence = App.number #this is the case when App.fixed is activated. All 'n' apps of an App instance are switched_on altogether - if App.activate > 0: #evaluates if the app has some duty cycles to be considered + if App.fixed_cycle > 0: #evaluates if the app has some duty cycles to be considered if indexes_adj.size > 0: evaluate = round(np.mean(indexes_adj)) #calculates the mean time position of the current switch_on event, to later select the proper duty cycle else: @@ -238,8 +238,8 @@ def Stochastic_Process(j): np.put(App.daily_use,indexes_adj,(random_cycle3*coincidence)) np.put(App.daily_use_masked,indexes_adj,(random_cycle3*coincidence),mode='clip') else: #if no duty cycles are specififed, a regular switch_on event is modelled - np.put(App.daily_use,indexes_adj,(App.power*(random.uniform((1-App.Thermal_P_var),(1+App.Thermal_P_var)))*coincidence)) #randomises also the App Power if Thermal_P_var is on - np.put(App.daily_use_masked,indexes_adj,(App.power*(random.uniform((1-App.Thermal_P_var),(1+App.Thermal_P_var)))*coincidence),mode='clip') + np.put(App.daily_use,indexes_adj,(App.power*(random.uniform((1-App.thermal_p_var),(1+App.thermal_p_var)))*coincidence)) #randomises also the App Power if thermal_p_var is on + np.put(App.daily_use_masked,indexes_adj,(App.power*(random.uniform((1-App.thermal_p_var),(1+App.thermal_p_var)))*coincidence),mode='clip') App.daily_use_masked = np.zeros_like(ma.masked_greater_equal(App.daily_use_masked,0.001)) #updates the mask excluding the current switch_on event to identify the free_spots for the next iteration tot_time = (tot_time - indexes.size) + indexes_adj.size #updates the total time correcting the previous value break #exit cycle and go to next App @@ -256,7 +256,7 @@ def Stochastic_Process(j): coincidence = on_number else: coincidence = App.number - if App.activate > 0: + if App.fixed_cycle > 0: if indexes.size > 0: evaluate = round(np.mean(indexes)) else: @@ -271,8 +271,8 @@ def Stochastic_Process(j): np.put(App.daily_use,indexes,(random_cycle3*coincidence)) np.put(App.daily_use_masked,indexes,(random_cycle3*coincidence),mode='clip') else: - np.put(App.daily_use,indexes,(App.power*(random.uniform((1-App.Thermal_P_var),(1+App.Thermal_P_var)))*coincidence)) - np.put(App.daily_use_masked,indexes,(App.power*(random.uniform((1-App.Thermal_P_var),(1+App.Thermal_P_var)))*coincidence),mode='clip') + np.put(App.daily_use,indexes,(App.power*(random.uniform((1-App.thermal_p_var),(1+App.thermal_p_var)))*coincidence)) + np.put(App.daily_use_masked,indexes,(App.power*(random.uniform((1-App.thermal_p_var),(1+App.thermal_p_var)))*coincidence),mode='clip') App.daily_use_masked = np.zeros_like(ma.masked_greater_equal(App.daily_use_masked,0.001)) tot_time = tot_time #no correction applied to previously calculated value From 8a51d0b4524f021823411030ed281049ab44a245 Mon Sep 17 00:00:00 2001 From: "pierre-francois.duc" Date: Wed, 8 Jun 2022 22:18:12 +0200 Subject: [PATCH 02/33] Create save/export methods for Appliance and User classes --- ramp/core/core.py | 86 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 86 insertions(+) diff --git a/ramp/core/core.py b/ramp/core/core.py index 93ea4964..9a106f95 100644 --- a/ramp/core/core.py +++ b/ramp/core/core.py @@ -28,6 +28,17 @@ def __init__(self, user_name="", num_users=1, user_preference=0): def add_appliance(self, *args, **kwargs): # I would add the appliance explicitely here, unless the appliance works only if a windows is defined return Appliance(self, *args, **kwargs) + + def save(self, filename=None): + answer = pd.concat([app.save() for app in self.App_list], ignore_index=True) + if filename is not None: + answer.to_excel(f"{filename}.xlsx") + else: + return answer + + def export_to_dataframe(self): + return self.save() + def Appliance( self, user, @@ -150,7 +161,82 @@ def __init__( self.random_cycle2 = np.array([]) self.random_cycle3 = np.array([]) + def save(self): + dm = {} + for user_attribute in ("user_name", "num_users", "user_preference"): + dm[user_attribute] = getattr(self.user, user_attribute) + for attribute in ( + "number", + "power", + "num_windows", + "func_time", + "time_fraction_random_variability", + "func_cycle", + "fixed", + "fixed_cycle", + "occasional_use", + "flat", + "thermal_p_var", + "pref_index", + "wd_we_type", + "p_11", + "t_11", + "p_12", + "t_12", + "r_c1", + "p_21", + "t_21", + "p_22", + "t_22", + "r_c2", + "p_31", + "t_31", + "p_32", + "t_32", + "r_c3", + "window_1", + "window_2", + "window_3", + "random_var_w", + ): + + if hasattr(self, attribute): + if "window_" in attribute: + window_value = getattr(self, attribute) + dm[attribute + "_start"] = window_value[0] + dm[attribute + "_end"] = window_value[1] + elif attribute == "power": + power_values = getattr(self, attribute) + if np.diff(power_values).sum() == 0: + power_values = power_values[0] + else: + power_values = power_values.tolist() + dm[attribute] = power_values + else: + dm[attribute] = getattr(self, attribute) else: + # this is for legacy purpose, so that people can export their old models to new format + old_attribute = NEW_TO_OLD_MAPPING[attribute] + if hasattr(self, old_attribute): + if "window_" in old_attribute: + window_value = getattr(self, old_attribute) + dm[attribute + "_start"] = window_value[0] + dm[attribute + "_end"] = window_value[1] + elif old_attribute == "POWER": + power_values = getattr(self, old_attribute) + if np.diff(power_values).sum() == 0: + power_values = power_values[0] + else: + power_values = power_values.tolist() + dm[attribute] = power_values + else: + dm[attribute] = getattr(self, old_attribute) + else: + dm[attribute] = None + return pd.DataFrame.from_records([dm]) + + def export_to_dataframe(self): + return self.save() def windows(self, window_1 = np.array([0,0]), window_2 = np.array([0,0]),random_var_w = 0, window_3 = np.array([0,0])): self.window_1 = window_1 #array of start and ending time for window of use #1 From c74c079bcfd37d8b67875566b428e6795e04d086 Mon Sep 17 00:00:00 2001 From: "pierre-francois.duc" Date: Tue, 10 May 2022 13:02:25 +0200 Subject: [PATCH 03/33] Add UseCase class A UseCase is simply a list of users --- ramp/core/core.py | 98 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 98 insertions(+) diff --git a/ramp/core/core.py b/ramp/core/core.py index 9a106f95..ad2a4f4a 100644 --- a/ramp/core/core.py +++ b/ramp/core/core.py @@ -14,6 +14,97 @@ their own characteristics. Within the Appliance class, some other functions are created to define windows of use and, if needed, specific duty cycles ''' +class UseCase: + def __init__(self, name="", users=None): + self.name = name + if users is None: + users = [] + self.users = users + + def add_user(self, user): + if isinstance(user, User): + self.users.append(user) + + def save(self, filename=None): + answer = pd.concat([user.save() for user in self.users], ignore_index=True) + if filename is not None: + answer.to_excel(f"{filename}.xlsx", index=False) + else: + return answer + + def export_to_dataframe(self): + return self.save() + + def load(self, filename): + """Open an .xlsx file which was produces via the save method and create instances of Users and Appliances""" + + appliance_args = ( + "number", + "power", + "num_windows", + "func_time", + "time_fraction_random_variability", + "func_cycle", + "fixed", + "fixed_cycle", + "occasional_use", + "flat", + "thermal_p_var", + "pref_index", + "wd_we_type", + ) + windows_args = ("window_1", "window_2", "window_3", "random_var_w") + cycle_args = ( + ("p_11", "t_11", "p_12", "t_12", "r_c1"), + ("p_21", "t_21", "p_22", "t_22", "r_c2"), + ("p_31", "t_31", "p_32", "t_32", "r_c3"), + ) + + df = pd.read_excel(filename) + for user_name in df.user_name.unique()[0:1]: + user_df = df.loc[df.user_name == user_name] + num_users = user_df.num_users.unique() + if len(num_users) == 1: + num_users = num_users[0] + else: + raise ValueError( + "'num_users' should be the same for a given user profile" + ) + user_preference = user_df.user_preference.unique() + if len(user_preference) == 1: + user_preference = user_preference[0] + else: + raise ValueError( + "'user_preference' should be the same for a given user profile" + ) + + # create user and add it to usecase + user = User(user_name, num_users, user_preference) + self.add_user(user) + # itereate through the lines of the DataFrame, each line representing one Appliance instance + for row in user_df.loc[ + :, ~user_df.columns.isin(["user_name", "num_users", "user_preference"]) + ].to_dict(orient="records"): + # assign Appliance arguments + appliance_parameters = {k: row[k] for k in appliance_args} + appliance = user.add_appliance(**appliance_parameters) + + # assign windows arguments + windows_parameters = {} + for k in windows_args: + if "window" in k: + windows_parameters[k] = np.array( + [row.get(k + "_start"), row.get(k + "_end")] + ) + else: + windows_parameters[k] = row.get(k) + appliance.windows(**windows_parameters) + + # assign cycles arguments + for i, cycle in enumerate(cycle_args): + if np.isnan(row.get(cycle[0])) is False: + cycle_parameters = {k: row.get(k) for k in cycle} + appliance.specific_cycle(cycle_num=i, **cycle_parameters) class User: @@ -257,6 +348,13 @@ def windows(self, window_1 = np.array([0,0]), window_2 = np.array([0,0]),random_ self.cw11 = self.window_1 self.cw12 = self.window_2 + def specific_cycle(self, cycle_num, **kwargs): + if cycle_num == 1: + self.specific_cycle_1(**kwargs) + elif cycle_num == 2: + self.specific_cycle_2(**kwargs) + elif cycle_num == 3: + self.specific_cycle_3(**kwargs) #if needed, specific duty cycles can be defined for each Appliance, for a maximum of three different ones def specific_cycle_1(self, p_11 = 0, t_11 = 0, p_21 = 0, t_21 = 0, r_c1 = 0): self.p_11 = p_11 #power absorbed during first part of the duty cycle From 48e301f856a84682424faf485905fc79177b6cf0 Mon Sep 17 00:00:00 2001 From: "pierre-francois.duc" Date: Tue, 10 May 2022 13:04:26 +0200 Subject: [PATCH 04/33] Update core module docstring --- ramp/core/core.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/ramp/core/core.py b/ramp/core/core.py index ad2a4f4a..4aae72ef 100644 --- a/ramp/core/core.py +++ b/ramp/core/core.py @@ -7,13 +7,14 @@ from ramp.core.constants import NEW_TO_OLD_MAPPING, OLD_TO_NEW_MAPPING #%% Definition of Python classes that constitute the model architecture -''' -The code is based on two concatenated python classes, namely 'User' and -'Appliance', which are used to define at the outer level the User classes and -at the inner level all the available appliances within each user class, with -their own characteristics. Within the Appliance class, some other functions are -created to define windows of use and, if needed, specific duty cycles -''' +""" +The code is based on UseCase, User and Appliance classes. +A UseCase instance consists of a list of User instances which own Appliance instances +Within the Appliance class, some other functions are created to define windows of use and, +if needed, specific duty cycles +""" + + class UseCase: def __init__(self, name="", users=None): self.name = name From 08555b05c879529785f33b84e9ebb228ff9f3971 Mon Sep 17 00:00:00 2001 From: "pierre-francois.duc" Date: Tue, 10 May 2022 14:30:43 +0200 Subject: [PATCH 05/33] Add a function to convert old input files --- ramp/core/core.py | 6 ++++-- ramp/ramp_convert_old_input_files.py | 30 ++++++++++++++++++++++++++++ 2 files changed, 34 insertions(+), 2 deletions(-) create mode 100644 ramp/ramp_convert_old_input_files.py diff --git a/ramp/core/core.py b/ramp/core/core.py index 4aae72ef..65148196 100644 --- a/ramp/core/core.py +++ b/ramp/core/core.py @@ -165,7 +165,7 @@ def Appliance( thermal_p_var=thermal_P_var, pref_index=pref_index, wd_we_type=wd_we_type, - P_series=P_series, + p_series=P_series, name=name, ) # Define the inner class for modelling user's appliances within the correspoding user class @@ -308,7 +308,7 @@ def save(self): dm[attribute] = getattr(self, attribute) else: # this is for legacy purpose, so that people can export their old models to new format - old_attribute = NEW_TO_OLD_MAPPING[attribute] + old_attribute = NEW_TO_OLD_MAPPING.get(attribute,attribute) if hasattr(self, old_attribute): if "window_" in old_attribute: window_value = getattr(self, old_attribute) @@ -356,6 +356,7 @@ def specific_cycle(self, cycle_num, **kwargs): self.specific_cycle_2(**kwargs) elif cycle_num == 3: self.specific_cycle_3(**kwargs) + #if needed, specific duty cycles can be defined for each Appliance, for a maximum of three different ones def specific_cycle_1(self, p_11 = 0, t_11 = 0, p_21 = 0, t_21 = 0, r_c1 = 0): self.p_11 = p_11 #power absorbed during first part of the duty cycle @@ -394,3 +395,4 @@ def cycle_behaviour(self, cw11 = np.array([0,0]), cw12 = np.array([0,0]), cw21 = self.cw22 = cw22 self.cw31 = cw31 #same for cycle 3 self.cw32 = cw32 + diff --git a/ramp/ramp_convert_old_input_files.py b/ramp/ramp_convert_old_input_files.py new file mode 100644 index 00000000..2228d86d --- /dev/null +++ b/ramp/ramp_convert_old_input_files.py @@ -0,0 +1,30 @@ +# -*- coding: utf-8 -*- +""" +""" + +#%% Import required modules + +import sys,os, importlib +sys.path.append('../') + + +from core.core import UseCase + + +def load_old_user_input_file(fname_path): + """ + Imports an input file from a path and returns a processed User_list + """ + + print(os.path.sep) + fname = fname_path.replace(".py", "").replace(os.path.sep, ".") + output_fname = fname_path.replace(".py", "") + file_module = importlib.import_module(fname) + + User_list = file_module.User_list + + UseCase(users=User_list).save(output_fname) + +#Add your filepathes down there +print(load_old_user_input_file(os.path.join("input_files", "input_file_1.py"))) +print(load_old_user_input_file("input_files/input_file_3.py")) \ No newline at end of file From 25659cc3e5742e8070c35c0608e7cfb82ad09564 Mon Sep 17 00:00:00 2001 From: "pierre-francois.duc" Date: Wed, 11 May 2022 22:54:54 +0200 Subject: [PATCH 06/33] Improve conversion script - Renamed Appliance's attribute POWER to power - Add argument parsing to convert script - Add lines automatically to the end of .py inputfile in order to register the variable names as Appliance instances names --- ramp/core/core.py | 6 +-- ramp/core/stochastic_process.py | 18 ++++----- ramp/ramp_convert_old_input_files.py | 57 ++++++++++++++++++++++------ 3 files changed, 57 insertions(+), 24 deletions(-) diff --git a/ramp/core/core.py b/ramp/core/core.py index 65148196..cb34b33a 100644 --- a/ramp/core/core.py +++ b/ramp/core/core.py @@ -135,7 +135,7 @@ def Appliance( self, user, n=1, - POWER=0, + P=0, w=1, t=0, r_t=0, @@ -153,7 +153,7 @@ def Appliance( """Back-compatibility with legacy code""" return self.add_appliance( number=n, - power=POWER, + power=P, num_windows=w, func_time=t, time_fraction_random_variability=r_t, @@ -168,7 +168,6 @@ def Appliance( p_series=P_series, name=name, ) -# Define the inner class for modelling user's appliances within the correspoding user class class Appliance: def __init__( self, @@ -258,6 +257,7 @@ def save(self): for user_attribute in ("user_name", "num_users", "user_preference"): dm[user_attribute] = getattr(self.user, user_attribute) for attribute in ( + "name" "number", "power", "num_windows", diff --git a/ramp/core/stochastic_process.py b/ramp/core/stochastic_process.py index 8acf933d..e1b6f48b 100644 --- a/ramp/core/stochastic_process.py +++ b/ramp/core/stochastic_process.py @@ -24,7 +24,7 @@ def Stochastic_Process(j): App_count = 0 for App in Us.App_list: #Calculate windows curve, i.e. the theoretical maximum curve that can be obtained, for each app, by switching-on always all the 'n' apps altogether in any time-step of the functioning windows - single_wcurve = Us.App_list[App_count].daily_use*np.mean(Us.App_list[App_count].POWER)*Us.App_list[App_count].number #this computes the curve for the specific App + single_wcurve = Us.App_list[App_count].daily_use*np.mean(Us.App_list[App_count].power)*Us.App_list[App_count].number #this computes the curve for the specific App windows_curve = np.vstack([windows_curve, single_wcurve]) #this stacks the specific App curve in an overall curve comprising all the Apps within a User class App_count += 1 Us.windows_curve = windows_curve #after having iterated for all the Apps within a User class, saves the overall User class theoretical maximum curve @@ -90,9 +90,9 @@ def Stochastic_Process(j): #redefines functioning windows based on the previous randomisation of the boundaries if App.flat == 'yes': #if the app is "flat" the code stops right after filling the newly created windows without applying any further stochasticity - App.daily_use[rand_window_1[0]:rand_window_1[1]] = np.full(np.diff(rand_window_1),App.POWER[prof_i]*App.number) - App.daily_use[rand_window_2[0]:rand_window_2[1]] = np.full(np.diff(rand_window_2),App.POWER[prof_i]*App.number) - App.daily_use[rand_window_3[0]:rand_window_3[1]] = np.full(np.diff(rand_window_3),App.POWER[prof_i]*App.number) + App.daily_use[rand_window_1[0]:rand_window_1[1]] = np.full(np.diff(rand_window_1),App.power[prof_i]*App.number) + App.daily_use[rand_window_2[0]:rand_window_2[1]] = np.full(np.diff(rand_window_2),App.power[prof_i]*App.number) + App.daily_use[rand_window_3[0]:rand_window_3[1]] = np.full(np.diff(rand_window_3),App.power[prof_i]*App.number) Us.load = Us.load + App.daily_use continue else: #otherwise, for "non-flat" apps it puts a mask on the newly defined windows and continues @@ -100,8 +100,6 @@ def Stochastic_Process(j): App.daily_use[rand_window_2[0]:rand_window_2[1]] = np.full(np.diff(rand_window_2),0.001) App.daily_use[rand_window_3[0]:rand_window_3[1]] = np.full(np.diff(rand_window_3),0.001) App.daily_use_masked = np.zeros_like(ma.masked_not_equal(App.daily_use,0.001)) - - App.power = App.POWER[prof_i] #random variability is applied to the total functioning time and to the duration of the duty cycles, if they have been specified random_var_t = random.uniform((1-App.time_fraction_random_variability),(1+App.time_fraction_random_variability)) @@ -238,8 +236,8 @@ def Stochastic_Process(j): np.put(App.daily_use,indexes_adj,(random_cycle3*coincidence)) np.put(App.daily_use_masked,indexes_adj,(random_cycle3*coincidence),mode='clip') else: #if no duty cycles are specififed, a regular switch_on event is modelled - np.put(App.daily_use,indexes_adj,(App.power*(random.uniform((1-App.thermal_p_var),(1+App.thermal_p_var)))*coincidence)) #randomises also the App Power if thermal_p_var is on - np.put(App.daily_use_masked,indexes_adj,(App.power*(random.uniform((1-App.thermal_p_var),(1+App.thermal_p_var)))*coincidence),mode='clip') + np.put(App.daily_use,indexes_adj,(App.power[prof_i]*(random.uniform((1-App.thermal_p_var),(1+App.thermal_p_var)))*coincidence)) #randomises also the App Power if thermal_p_var is on + np.put(App.daily_use_masked,indexes_adj,(App.power[prof_i]*(random.uniform((1-App.thermal_p_var),(1+App.thermal_p_var)))*coincidence),mode='clip') App.daily_use_masked = np.zeros_like(ma.masked_greater_equal(App.daily_use_masked,0.001)) #updates the mask excluding the current switch_on event to identify the free_spots for the next iteration tot_time = (tot_time - indexes.size) + indexes_adj.size #updates the total time correcting the previous value break #exit cycle and go to next App @@ -271,8 +269,8 @@ def Stochastic_Process(j): np.put(App.daily_use,indexes,(random_cycle3*coincidence)) np.put(App.daily_use_masked,indexes,(random_cycle3*coincidence),mode='clip') else: - np.put(App.daily_use,indexes,(App.power*(random.uniform((1-App.thermal_p_var),(1+App.thermal_p_var)))*coincidence)) - np.put(App.daily_use_masked,indexes,(App.power*(random.uniform((1-App.thermal_p_var),(1+App.thermal_p_var)))*coincidence),mode='clip') + np.put(App.daily_use,indexes,(App.power[prof_i]*(random.uniform((1-App.thermal_p_var),(1+App.thermal_p_var)))*coincidence)) + np.put(App.daily_use_masked,indexes,(App.power[prof_i]*(random.uniform((1-App.thermal_p_var),(1+App.thermal_p_var)))*coincidence),mode='clip') App.daily_use_masked = np.zeros_like(ma.masked_greater_equal(App.daily_use_masked,0.001)) tot_time = tot_time #no correction applied to previously calculated value diff --git a/ramp/ramp_convert_old_input_files.py b/ramp/ramp_convert_old_input_files.py index 2228d86d..18c539cb 100644 --- a/ramp/ramp_convert_old_input_files.py +++ b/ramp/ramp_convert_old_input_files.py @@ -1,22 +1,46 @@ -# -*- coding: utf-8 -*- -""" -""" - #%% Import required modules - import sys,os, importlib sys.path.append('../') +import argparse +from core.core import UseCase +parser = argparse.ArgumentParser( + prog="python ramp_convert_old_input_files.py", description="Convert old python input files to xlsx ones" +) +parser.add_argument( + "-i", + dest="fname_path", + nargs="+", + type=str, + help="path to the input file (including filename)", +) -from core.core import UseCase + +convert_names = """ +# Code automatically added by ramp_convert_old_input_files.py +from ramp.core.core import Appliance +local_var_names = [(i, a) for i, a in locals().items() if isinstance(a, Appliance)] +for i, a in local_var_names: + a.name = i +""" -def load_old_user_input_file(fname_path): +def convert_old_user_input_file(fname_path, keep_names=True): """ Imports an input file from a path and returns a processed User_list """ - print(os.path.sep) + # Check if the lines to save the names of the Appliance instances is already there + with open(fname_path, "r") as fp: + lines = fp.readlines() + if "# Code automatically added by ramp_convert_old_input_files.py\n" in lines: + keep_names = False + + # Add code to the input file to assign the variable name of the Appliance instances to their name attribute + if keep_names is True: + with open(fname_path, "a") as fp: + fp.write(convert_names) + fname = fname_path.replace(".py", "").replace(os.path.sep, ".") output_fname = fname_path.replace(".py", "") file_module = importlib.import_module(fname) @@ -25,6 +49,17 @@ def load_old_user_input_file(fname_path): UseCase(users=User_list).save(output_fname) -#Add your filepathes down there -print(load_old_user_input_file(os.path.join("input_files", "input_file_1.py"))) -print(load_old_user_input_file("input_files/input_file_3.py")) \ No newline at end of file + +if __name__ == "__main__": + + args = vars(parser.parse_args()) + fname = args["fname_path"] + if fname is None: + print("Please provide path to input file with option -i") + else: + if isinstance(fname, list): + fnames = fname + for fname in fnames: + convert_old_user_input_file(fname) + else: + convert_old_user_input_file(fname) From c1f201b24e69da046baacb8dbe38f81a5ac83924 Mon Sep 17 00:00:00 2001 From: "pierre-francois.duc" Date: Thu, 12 May 2022 00:23:00 +0200 Subject: [PATCH 07/33] Fix loading of specific cycle parameters --- ramp/core/core.py | 77 ++++++++++++++++++++++++++++++++--------------- 1 file changed, 53 insertions(+), 24 deletions(-) diff --git a/ramp/core/core.py b/ramp/core/core.py index cb34b33a..da5b6fa0 100644 --- a/ramp/core/core.py +++ b/ramp/core/core.py @@ -40,6 +40,7 @@ def load(self, filename): """Open an .xlsx file which was produces via the save method and create instances of Users and Appliances""" appliance_args = ( + "name", "number", "power", "num_windows", @@ -56,9 +57,9 @@ def load(self, filename): ) windows_args = ("window_1", "window_2", "window_3", "random_var_w") cycle_args = ( - ("p_11", "t_11", "p_12", "t_12", "r_c1"), - ("p_21", "t_21", "p_22", "t_22", "r_c2"), - ("p_31", "t_31", "p_32", "t_32", "r_c3"), + ("p_11", "t_11", "cw11", "p_12", "t_12", "cw12", "r_c1"), + ("p_21", "t_21", "cw21", "p_22", "t_22", "cw22", "r_c2"), + ("p_31", "t_31", "cw31", "p_32", "t_32", "cw32", "r_c3"), ) df = pd.read_excel(filename) @@ -102,10 +103,16 @@ def load(self, filename): appliance.windows(**windows_parameters) # assign cycles arguments - for i, cycle in enumerate(cycle_args): - if np.isnan(row.get(cycle[0])) is False: - cycle_parameters = {k: row.get(k) for k in cycle} - appliance.specific_cycle(cycle_num=i, **cycle_parameters) + for i in range(appliance.fixed_cycle): + cycle_parameters ={} + for k in cycle_args[i]: + if "cw" in k: + cycle_parameters[k] = np.array( + [row.get(k + "_start"), row.get(k + "_end")],dtype=np.intc + ) + else: + cycle_parameters[k] = row.get(k) + appliance.specific_cycle(cycle_num=i+1, **cycle_parameters) class User: @@ -257,7 +264,7 @@ def save(self): for user_attribute in ("user_name", "num_users", "user_preference"): dm[user_attribute] = getattr(self.user, user_attribute) for attribute in ( - "name" + "name", "number", "power", "num_windows", @@ -273,18 +280,24 @@ def save(self): "wd_we_type", "p_11", "t_11", + "cw11", "p_12", "t_12", + "cw12", "r_c1", "p_21", "t_21", + "cw21", "p_22", "t_22", + "cw22", "r_c2", "p_31", "t_31", + "cw31", "p_32", "t_32", + "cw32", "r_c3", "window_1", "window_2", @@ -293,7 +306,7 @@ def save(self): ): if hasattr(self, attribute): - if "window_" in attribute: + if "window_" in attribute or "cw" in attribute: window_value = getattr(self, attribute) dm[attribute + "_start"] = window_value[0] dm[attribute + "_end"] = window_value[1] @@ -310,7 +323,7 @@ def save(self): # this is for legacy purpose, so that people can export their old models to new format old_attribute = NEW_TO_OLD_MAPPING.get(attribute,attribute) if hasattr(self, old_attribute): - if "window_" in old_attribute: + if "window_" in attribute or "cw" in attribute: window_value = getattr(self, old_attribute) dm[attribute + "_start"] = window_value[0] dm[attribute + "_end"] = window_value[1] @@ -324,7 +337,11 @@ def save(self): else: dm[attribute] = getattr(self, old_attribute) else: - dm[attribute] = None + if "cw" in old_attribute: + dm[attribute + "_start"] = None + dm[attribute + "_end"] = None + else: + dm[attribute] = None return pd.DataFrame.from_records([dm]) def export_to_dataframe(self): @@ -358,32 +375,44 @@ def specific_cycle(self, cycle_num, **kwargs): self.specific_cycle_3(**kwargs) #if needed, specific duty cycles can be defined for each Appliance, for a maximum of three different ones - def specific_cycle_1(self, p_11 = 0, t_11 = 0, p_21 = 0, t_21 = 0, r_c1 = 0): + def specific_cycle_1(self, p_11 = 0, t_11 = 0, p_12 = 0, t_12 = 0, r_c1 = 0, cw11=None, cw12=None): self.p_11 = p_11 #power absorbed during first part of the duty cycle - self.t_11 = t_11 #duration of first part of the duty cycle - self.p_12 = p_21 #power absorbed during second part of the duty cycle - self.t_12 = t_21 #duration of second part of the duty cycle + self.t_11 = int(t_11) #duration of first part of the duty cycle + self.p_12 = p_12 #power absorbed during second part of the duty cycle + self.t_12 = int(t_12) #duration of second part of the duty cycle self.r_c1 = r_c1 #random variability of duty cycle segments duration + if cw11 is not None: + self.cw11 = cw11 + if cw12 is not None: + self.cw12 = cw12 # Below is not used - self.fixed_cycle1 = np.concatenate(((np.ones(t_11)*p_11),(np.ones(t_21)*p_21))) #create numpy array representing the duty cycle + self.fixed_cycle1 = np.concatenate(((np.ones(self.t_11)*p_11),(np.ones(self.t_12)*p_12))) #create numpy array representing the duty cycle - def specific_cycle_2(self, p_21 = 0, t_21 = 0, p_22 = 0, t_22 = 0, r_c2 = 0): + def specific_cycle_2(self, p_21 = 0, t_21 = 0, p_22 = 0, t_22 = 0, r_c2 = 0, cw21=None, cw22=None): self.p_21 = p_21 #same as for cycle1 - self.t_21 = t_21 + self.t_21 = int(t_21) self.p_22 = p_22 - self.t_22 = t_22 + self.t_22 = int(t_22) self.r_c2 = r_c2 + if cw21 is not None: + self.cw21 = cw21 + if cw22 is not None: + self.cw22 = cw22 # Below is not used - self.fixed_cycle2 = np.concatenate(((np.ones(t_21)*p_21),(np.ones(t_22)*p_22))) + self.fixed_cycle2 = np.concatenate(((np.ones(self.t_21)*p_21),(np.ones(self.t_22)*p_22))) - def specific_cycle_3(self, p_31 = 0, t_31 = 0, p_32 = 0, t_32 = 0, r_c3 = 0): + def specific_cycle_3(self, p_31 = 0, t_31 = 0, p_32 = 0, t_32 = 0, r_c3 = 0, cw31=None, cw32=None): self.p_31 = p_31 #same as for cycle1 - self.t_31 = t_31 + self.t_31 = int(t_31) self.p_32 = p_32 - self.t_32 = t_32 + self.t_32 = int(t_32) self.r_c3 = r_c3 + if cw31 is not None: + self.cw31 = cw31 + if cw32 is not None: + self.cw32 = cw32 # Below is not used - self.fixed_cycle3 = np.concatenate(((np.ones(t_31)*p_31),(np.ones(t_32)*p_32))) + self.fixed_cycle3 = np.concatenate(((np.ones(self.t_31)*p_31),(np.ones(self.t_32)*p_32))) #different time windows can be associated with different specific duty cycles def cycle_behaviour(self, cw11 = np.array([0,0]), cw12 = np.array([0,0]), cw21 = np.array([0,0]), cw22 = np.array([0,0]), cw31 = np.array([0,0]), cw32 = np.array([0,0])): From aa67dd746eae19b03d445876effd5f6415a0f173 Mon Sep 17 00:00:00 2001 From: "pierre-francois.duc" Date: Thu, 12 May 2022 00:30:18 +0200 Subject: [PATCH 08/33] Enable running the code from old and new input files --- ramp/core/core.py | 3 +- ramp/core/initialise.py | 25 ++++++---- ramp/core/stochastic_process.py | 9 ++-- ramp/ramp_run.py | 83 ++++++++++++++++++++++++++++----- 4 files changed, 95 insertions(+), 25 deletions(-) diff --git a/ramp/core/core.py b/ramp/core/core.py index da5b6fa0..dc78876a 100644 --- a/ramp/core/core.py +++ b/ramp/core/core.py @@ -63,7 +63,7 @@ def load(self, filename): ) df = pd.read_excel(filename) - for user_name in df.user_name.unique()[0:1]: + for user_name in df.user_name.unique(): user_df = df.loc[df.user_name == user_name] num_users = user_df.num_users.unique() if len(num_users) == 1: @@ -217,6 +217,7 @@ def __init__( if ( p_series is False and isinstance(power, pd.DataFrame) is False ): #check if the user defined P as timeseries + self.power = power * np.ones( 365 ) #treat the power as single value for the entire year diff --git a/ramp/core/initialise.py b/ramp/core/initialise.py index 175ac5cc..dc0cd7f7 100644 --- a/ramp/core/initialise.py +++ b/ramp/core/initialise.py @@ -4,6 +4,7 @@ import numpy as np import importlib +from ramp.core.core import UseCase def yearly_pattern(): @@ -18,32 +19,38 @@ def yearly_pattern(): return(Year_behaviour) -def user_defined_inputs(j): +def user_defined_inputs(j=None, fname=None): ''' Imports an input file and returns a processed User_list ''' + # Back compatibility with old code + if j is not None: + file_module = importlib.import_module(f'input_files.input_file_{j}') + User_list = file_module.User_list - file_module = importlib.import_module(f'input_files.input_file_{j}') - - User_list = file_module.User_list + if fname is not None: + usecase = UseCase() + usecase.load(fname) + User_list = usecase.users return(User_list) -def Initialise_model(): +def Initialise_model(num_profiles): ''' The model is ready to be initialised ''' - num_profiles = int(input("please indicate the number of profiles to be generated: ")) #asks the user how many profiles (i.e. code runs) he wants + + if num_profiles is None: + num_profiles = int(input("please indicate the number of profiles to be generated: ")) #asks the user how many profiles (i.e. code runs) he wants print('Please wait...') Profile = [] #creates an empty list to store the results of each code run, i.e. each stochastically generated profile return (Profile, num_profiles) -def Initialise_inputs(j): +def Initialise_inputs(j=None, fname=None): Year_behaviour = yearly_pattern() - user_defined_inputs(j) - user_list = user_defined_inputs(j) + user_list = user_defined_inputs(j, fname) # Calibration parameters ''' diff --git a/ramp/core/stochastic_process.py b/ramp/core/stochastic_process.py index e1b6f48b..ffb18c56 100644 --- a/ramp/core/stochastic_process.py +++ b/ramp/core/stochastic_process.py @@ -9,9 +9,12 @@ #%% Core model stochastic script -def Stochastic_Process(j): - Profile, num_profiles = Initialise_model() - peak_enlarg, mu_peak, s_peak, op_factor, Year_behaviour, User_list = Initialise_inputs(j) + + + +def Stochastic_Process(j=None, fname=None, num_profiles=None): + Profile, num_profiles = Initialise_model(num_profiles) + peak_enlarg, mu_peak, s_peak, op_factor, Year_behaviour, User_list = Initialise_inputs(j, fname) ''' Calculation of the peak time range, which is used to discriminate between off-peak and on-peak coincident switch-on probability Calculates first the overall Peak Window (taking into account all User classes). diff --git a/ramp/ramp_run.py b/ramp/ramp_run.py index 4acf88f0..689b2b35 100644 --- a/ramp/ramp_run.py +++ b/ramp/ramp_run.py @@ -25,24 +25,83 @@ import sys,os sys.path.append('../') +import argparse + from core.stochastic_process import Stochastic_Process from post_process import post_process as pp -# Define which input files should be considered and run. -# Files are specified as numbers in a list (e.g. [1,2] will consider input_file_1.py and input_file_2.py) -input_files_to_run = [1,2,3] -# Calls the stochastic process and saves the result in a list of stochastic profiles -for j in input_files_to_run: - Profiles_list = Stochastic_Process(j) - -# Post-processes the results and generates plots +parser = argparse.ArgumentParser( + prog="python ramp_run.py", description="Execute RAMP code" +) +parser.add_argument( + "-i", + dest="fname_path", + nargs="+", + type=str, + help="path to the (xlsx) input files (including filename). If not provided, then legacy .py input files will be fetched", +) +parser.add_argument( + "-n", + dest="num_profiles", + nargs="+", + type=int, + help="number of profiles to be generated", +) + +def run_usecase(j=None, fname=None, num_profiles=None): + # Calls the stochastic process and saves the result in a list of stochastic profiles + Profiles_list = Stochastic_Process(j=j, fname=fname, num_profiles=num_profiles) + + # Post-processes the results and generates plots Profiles_avg, Profiles_list_kW, Profiles_series = pp.Profile_formatting(Profiles_list) - pp.Profile_series_plot(Profiles_series) #by default, profiles are plotted as a series - - pp.export_series(Profiles_series,j) + pp.Profile_series_plot(Profiles_series) # by default, profiles are plotted as a series + + pp.export_series(Profiles_series, j, fname) - if len(Profiles_list) > 1: #if more than one daily profile is generated, also cloud plots are shown + if len(Profiles_list) > 1: # if more than one daily profile is generated, also cloud plots are shown pp.Profile_cloud_plot(Profiles_list, Profiles_avg) + +if __name__ == "__main__": + + args = vars(parser.parse_args()) + fnames = args["fname_path"] + num_profiles = args["num_profiles"] + # Define which input files should be considered and run. + + + if fnames is None: + print("Please provide path to input file with option -i, \n\nDefault to old version of RAMP input files\n") + # Files are specified as numbers in a list (e.g. [1,2] will consider input_file_1.py and input_file_2.py) + input_files_to_run = [1, 2, 3] + + if num_profiles is not None: + if len(num_profiles) == 1: + num_profiles = num_profiles * len(input_files_to_run) + else: + if len(num_profiles) != len(input_files_to_run): + raise ValueError("The number of profiles parameters should match the number of input files provided") + else: + num_profiles = [None] * len(input_files_to_run) + + for i, j in enumerate(input_files_to_run): + run_usecase(j=j, num_profiles=num_profiles[i]) + else: + if num_profiles is not None: + if len(num_profiles) == 1: + num_profiles = num_profiles * len(fnames) + else: + if len(num_profiles) != len(fnames): + raise ValueError( + "The number of profiles parameters should match the number of input files provided") + else: + num_profiles = [None] * len(fnames) + + for i, fname in enumerate(fnames): + run_usecase(fname=fname, num_profiles=num_profiles[i]) + + + + From 19e980245397ad2fd14f8681b4e2745db6e69d00 Mon Sep 17 00:00:00 2001 From: "pierre-francois.duc" Date: Thu, 12 May 2022 09:58:17 +0200 Subject: [PATCH 09/33] Update README --- README.md | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index b90dc904..3f04e3a5 100644 --- a/README.md +++ b/README.md @@ -22,11 +22,13 @@ The model is developed in Python 3.6, and requires the following libraries: * matplotlib * math * random +* pandas +* openpyxl ## Quick start -To get started, download the repository and simply run the "ramp_run.py" script. The console will ask how many profiles (i.e. independent days) need to be simulated, and will provide the results based on the default inputs defined in `input_file_x`.py. To change the inputs, just modify the latter files. Some guidance about the meaning of each input parameter is available in the `core.py` file, where the *User* and *Appliance* Python classes are defined and fully commented. +To get started, download the repository, install the dependencies with `pip install -r requirements.txt`, move to the `ramp` folder and simply run the `python ramp_run.py`. The console will ask how many profiles (i.e. independent days) need to be simulated, and will provide the results based on the default inputs defined in `input_file_x`.py. To change the inputs, just modify the latter files. Some guidance about the meaning of each input parameter is available in the `core.py` file, where the *User* and *Appliance* Python classes are defined and fully commented. -### Example input files +### Example python input files Three different input files are provided as example representing three different categories of appliancces that can be modelled with RAMP. - `input_file_1.py`: represents the most basic electric appliances, is an example of how to model lightbulbs, radios, TVs, fridges, and other electric appliances. This input file is based on the ones used for [this publication](https://doi.org/10.1016/j.energy.2019.04.097). @@ -38,6 +40,16 @@ Repetitive meals do not vary across days, whilst main meals do so. In particular The `user preference` defines how many types of meal are available for each user to choose every day (e.g. 3). Then, each of the available meal options is modelled separately, with a different `preference index` attached. The stochastic process randomly varies the meal preference of each user every day, deciding whether they want a "type 1" meal, or a "type 2", etc. on a given day. This input file is used in [this publication](https://doi.org/10.1109/PTC.2019.8810571) +### Spreadsheet input files +It is also possible to use spreadsheets as input files with the option `-i`: `python ramp_run.py -i `. If you already know how many profile you want to simulate you can indicate it with the `-n` option: `python ramp_run.py -i -n 10` will simulate 10 profiles. Note that you can use this option without providing a `.xlsx` input file, this will then be equivalent to running `python ramp_run.py` without being prompted for the number of profile within the console. + +### Convert python input files to xlsx +If you have existing python input files, you can convert them to spreadsheet. To do so, go to `ramp` folder and run + +``` +python ramp_convert_old_input_files.py -i +``` + ## Citing Please cite the original Journal publication if you use RAMP in your research: *F. Lombardi, S. Balderrama, S. Quoilin, E. Colombo, Generating high-resolution multi-energy load profiles for remote areas with an open-source stochastic model, Energy, 2019, https://doi.org/10.1016/j.energy.2019.04.097.* From 5edaca385e1dc4f8ef6bc91c3fd4e79676fb53a3 Mon Sep 17 00:00:00 2001 From: "pierre-francois.duc" Date: Wed, 18 May 2022 10:29:53 +0200 Subject: [PATCH 10/33] Fix post-processing and add __init__.py files --- ramp/__init__.py | 0 ramp/core/__init__.py | 0 ramp/input_files/__init__.py | 0 ramp/post_process/post_process.py | 9 +++++++-- requirements.txt | 3 +++ 5 files changed, 10 insertions(+), 2 deletions(-) create mode 100644 ramp/__init__.py create mode 100644 ramp/core/__init__.py create mode 100644 ramp/input_files/__init__.py create mode 100644 requirements.txt diff --git a/ramp/__init__.py b/ramp/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/ramp/core/__init__.py b/ramp/core/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/ramp/input_files/__init__.py b/ramp/input_files/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/ramp/post_process/post_process.py b/ramp/post_process/post_process.py index a4180f9b..3a13df7b 100644 --- a/ramp/post_process/post_process.py +++ b/ramp/post_process/post_process.py @@ -1,6 +1,8 @@ # -*- coding: utf-8 -*- #%% Import required libraries +import os.path + import matplotlib.pyplot as plt import numpy as np import pandas as pd @@ -64,6 +66,9 @@ def Profile_series_plot(stoch_profiles_series): # Export Profiles -def export_series(stoch_profiles_series, j): +def export_series(stoch_profiles_series, j=None, fname=None): series_frame = pd.DataFrame(stoch_profiles_series) - series_frame.to_csv('results/output_file_%d.csv' % (j)) \ No newline at end of file + if j is not None: + series_frame.to_csv('results/output_file_%d.csv' % (j)) + if fname is not None: + series_frame.to_csv(f'results/output_file_{os.path.split(fname)[-1]}.csv') diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 00000000..44974cf4 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,3 @@ +pandas +numpy +matplotlib \ No newline at end of file From 936a4ad465dfa1be2212d87e7d2871e2f6884d88 Mon Sep 17 00:00:00 2001 From: "pierre-francois.duc" Date: Fri, 3 Jun 2022 13:38:46 +0200 Subject: [PATCH 11/33] Add a parser of xlsx input files Provide two ways to define power timeseries --- ramp/core/core.py | 22 ++++++------ ramp/core/utils.py | 85 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 95 insertions(+), 12 deletions(-) create mode 100644 ramp/core/utils.py diff --git a/ramp/core/core.py b/ramp/core/core.py index dc78876a..4dd12124 100644 --- a/ramp/core/core.py +++ b/ramp/core/core.py @@ -5,6 +5,7 @@ import numpy.ma as ma import pandas as pd from ramp.core.constants import NEW_TO_OLD_MAPPING, OLD_TO_NEW_MAPPING +from ramp.core.utils import read_input_file #%% Definition of Python classes that constitute the model architecture """ @@ -43,6 +44,7 @@ def load(self, filename): "name", "number", "power", + "p_series", "num_windows", "func_time", "time_fraction_random_variability", @@ -62,7 +64,7 @@ def load(self, filename): ("p_31", "t_31", "cw31", "p_32", "t_32", "cw32", "r_c3"), ) - df = pd.read_excel(filename) + df = read_input_file(filename=filename) for user_name in df.user_name.unique(): user_df = df.loc[df.user_name == user_name] num_users = user_df.num_users.unique() @@ -213,18 +215,14 @@ def __init__( ) self.pref_index = pref_index #defines preference index for association with random User daily preference behaviour self.wd_we_type = wd_we_type #defines if the App is associated with weekdays or weekends | 0 is wd 1 is we 2 is all week - #TODO detect if power is a timeseries or a constant and get rid of P_series altogether - if ( - p_series is False and isinstance(power, pd.DataFrame) is False - ): #check if the user defined P as timeseries - - self.power = power * np.ones( - 365 - ) #treat the power as single value for the entire year + if p_series is True: + if isinstance(power, str): + power = pd.read_json(power) + if not isinstance(power, pd.DataFrame): + raise(ValueError("The input power is excepted to be a series but it is not recognized as such")) + self.power = power.values[:, 0] #if a timeseries is given the power is treated as so else: - self.power = power.values[ - :, 0 - ] #if a timeseries is given the power is treated as so + self.power = power * np.ones(365) # treat the power as single value for the entire year # attributes initialized by self.windows self.random_var_w = None diff --git a/ramp/core/utils.py b/ramp/core/utils.py new file mode 100644 index 00000000..480b8ae6 --- /dev/null +++ b/ramp/core/utils.py @@ -0,0 +1,85 @@ +import json +import pandas as pd +from openpyxl import load_workbook +from openpyxl.worksheet.cell_range import CellRange + +POSSIBLE_FORMATS = """ + The possible formats of the power timeseries are : + - a single value (int or float) if the power is constant throughout the load_profile (with random fluctuations if the variable 'thermal_p_var' is provided) + - an array of value provided as text in a json array format, i.e. : [val1, val2, val3, ...] + - a range of cells in another sheet of the input file (type '=' in the cell and then select the wished range of values to get the correct format automatically) + + ***Note*** + The last two formats will only accept one column (values only) or two columns (timestamps and values, respectively) and exactly 365 rows +""" + + +def read_input_file(filename): + """Parse a RAMP .xlsx input file""" + + wb = load_workbook(filename=filename) + sheet_names = wb.sheetnames + name = sheet_names[0] + headers = [c.value for c in wb[name][1]] + df = pd.DataFrame(tuple(wb[name].values)[1:], columns=headers) + + df["p_series"] = False + for i, v in enumerate(df["power"].values): + if isinstance(v, str): + appliance_name = df["name"].iloc[i] + user_name = df["user_name"].iloc[i] + # the timeseries is provided as a range of values in the spreadsheet + if "=" in v: + ts_sheet_name, ts_range = v.replace("=", "").split("!") + cr = CellRange(ts_range) + if cr.size["columns"] == 1: + ts = json.dumps( + [ + wb[ts_sheet_name].cell(row=c[0], column=c[1]).value + for c in cr.cells + ] + ) + elif cr.size["columns"] == 2: + ts = pd.DataFrame( + [ + [ + wb[ts_sheet_name].cell(row=c[0], column=c[1]).value + for c in col + ] + for col in cr.cols + ] + ).T + ts = ts.to_json(orient="values") + else: + raise ( + ValueError( + f"The provided range for the power timeseries of the appliance '{appliance_name}' of user '{user_name} spans more than two columns in '{filename}' (range {ts_range} of sheet '{ts_sheet_name}')\n{POSSIBLE_FORMATS}" + ) + ) + if cr.size["rows"] != 365: + raise ( + ValueError( + f"The provided range for the power timeseries of the appliance '{appliance_name}' of user '{user_name}' in '{filename}' does not contain 365 values as expected (range {ts_range} of sheet '{ts_sheet_name}')\n{POSSIBLE_FORMATS}" + ) + ) + # the timeseries is expected as an array in json format + else: + try: + ts = json.loads(v) + if len(ts) != 365: + raise ( + ValueError( + f"The provided power timeseries of the appliance '{appliance_name}' of user '{user_name}' in '{filename}' does not contain 365 values as expected\n{POSSIBLE_FORMATS}" + ) + ) + + ts = v + except json.JSONDecodeError: + raise ( + ValueError( + f"Could not parse the power timeseries provided for appliance '{appliance_name}' of user '{user_name}' in '{filename}'\n{POSSIBLE_FORMATS}" + ) + ) + df.loc[i, "power"] = ts + df.loc[i, "p_series"] = True + return df From ba4a6636bc8a0f3153fca45fefd24ed88da05ab2 Mon Sep 17 00:00:00 2001 From: "pierre-francois.duc" Date: Sat, 4 Jun 2022 00:07:13 +0200 Subject: [PATCH 12/33] Add options to save converted file - option -o to provide another output path - option --suffix to provide a different file name as the python files --- ramp/ramp_convert_old_input_files.py | 55 ++++++++++++++++++++++++---- 1 file changed, 47 insertions(+), 8 deletions(-) diff --git a/ramp/ramp_convert_old_input_files.py b/ramp/ramp_convert_old_input_files.py index 18c539cb..d8553db9 100644 --- a/ramp/ramp_convert_old_input_files.py +++ b/ramp/ramp_convert_old_input_files.py @@ -1,11 +1,14 @@ #%% Import required modules -import sys,os, importlib -sys.path.append('../') + +import sys, os, importlib + +sys.path.append("../") import argparse -from core.core import UseCase +from ramp.core.core import UseCase parser = argparse.ArgumentParser( - prog="python ramp_convert_old_input_files.py", description="Convert old python input files to xlsx ones" + prog="python ramp_convert_old_input_files.py", + description="Convert old python input files to xlsx ones", ) parser.add_argument( "-i", @@ -14,6 +17,20 @@ type=str, help="path to the input file (including filename)", ) +parser.add_argument( + "-o", + dest="output_path", + nargs=1, + type=str, + help="path where to save the converted filename", +) +parser.add_argument( + "--suffix", + dest="suffix", + nargs=1, + type=str, + help="suffix appended to the converted filename", +) convert_names = """ @@ -25,7 +42,9 @@ """ -def convert_old_user_input_file(fname_path, keep_names=True): +def convert_old_user_input_file( + fname_path, output_path=None, suffix="", keep_names=True +): """ Imports an input file from a path and returns a processed User_list """ @@ -42,7 +61,14 @@ def convert_old_user_input_file(fname_path, keep_names=True): fp.write(convert_names) fname = fname_path.replace(".py", "").replace(os.path.sep, ".") - output_fname = fname_path.replace(".py", "") + + if output_path is None: + output_path = os.path.dirname(fname_path) + output_fname = fname_path.split(os.path.sep)[-1].replace(".py", suffix) + output_fname = os.path.join(output_path, output_fname) + + if "ramp" not in fname: + fname = f"ramp.{fname}" file_module = importlib.import_module(fname) User_list = file_module.User_list @@ -54,12 +80,25 @@ def convert_old_user_input_file(fname_path, keep_names=True): args = vars(parser.parse_args()) fname = args["fname_path"] + output_path = args.get("output_path") + suffix = args.get("suffix") + + if output_path is not None: + output_path = output_path[0] + + if suffix is None: + suffix = "" + else: + suffix = suffix[0] + if fname is None: print("Please provide path to input file with option -i") else: if isinstance(fname, list): fnames = fname for fname in fnames: - convert_old_user_input_file(fname) + convert_old_user_input_file( + fname, output_path=output_path, suffix=suffix + ) else: - convert_old_user_input_file(fname) + convert_old_user_input_file(fname, output_path=output_path, suffix=suffix) From 0276bca8215dce3d487a578d45c8cb68514fb323 Mon Sep 17 00:00:00 2001 From: "pierre-francois.duc" Date: Sat, 4 Jun 2022 00:23:53 +0200 Subject: [PATCH 13/33] Add a test for conversion of input file This requires pytest to be installed --- tests/__init__.py | 0 tests/test_input_file_conversion.py | 66 +++++++++++++++++++++++++++++ 2 files changed, 66 insertions(+) create mode 100644 tests/__init__.py create mode 100644 tests/test_input_file_conversion.py diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/test_input_file_conversion.py b/tests/test_input_file_conversion.py new file mode 100644 index 00000000..02f768e3 --- /dev/null +++ b/tests/test_input_file_conversion.py @@ -0,0 +1,66 @@ +import os +import pytest + +from ramp.core.initialise import Initialise_inputs +from ramp.ramp_convert_old_input_files import convert_old_user_input_file + + +def load_usecase(j=None, fname=None): + ( + peak_enlarg, + mu_peak, + s_peak, + op_factor, + Year_behaviour, + User_list, + ) = Initialise_inputs(j, fname) + return User_list + + +class TestTransformerClass: + def setup_method(self): + self.input_files_to_run = [1, 2, 3] + self.file_suffix = "_test" + os.chdir( + "ramp" + ) # for legacy code to work the loading of the input file has to happen from the ramp folder + self.py_fnames = [ + os.path.join("input_files", f"input_file_{i}.py") + for i in self.input_files_to_run + ] + self.xlsx_fnames = [ + os.path.join("test", f"input_file_{i}{self.file_suffix}.xlsx") + for i in self.input_files_to_run + ] + for fname in self.xlsx_fnames: + if os.path.exists(fname): + os.remove(fname) + + def teardown_method(self): + auto_code_proof = ( + "# Code automatically added by ramp_convert_old_input_files.py\n" + ) + # remove created files + for fname in self.xlsx_fnames: + if os.path.exists(fname): + os.remove(fname) + # remove additional code in legacy input files to get the appliance name from python variable names + for fname in self.py_fnames: + with open(fname, "r") as fp: + lines = fp.readlines() + if auto_code_proof in lines: + idx = lines.index(auto_code_proof) + with open(fname, "w") as fp: + fp.writelines(lines[: idx - 1]) + + def test_convert_py_to_xlsx(self): + """Convert the 3 example .py input files to xlsx and compare each appliance of each user""" + for i, j in enumerate(self.input_files_to_run): + old_user_list = load_usecase(j=j) + convert_old_user_input_file( + self.py_fnames[i], output_path="test", suffix=self.file_suffix + ) + new_user_list = load_usecase(fname=self.xlsx_fnames[i]) + for old_user, new_user in zip(old_user_list, new_user_list): + if old_user != new_user: + pytest.fail() From 9363179e57fa5d1e71f4bb08e3ea4ce24b4798ae Mon Sep 17 00:00:00 2001 From: "pierre-francois.duc" Date: Sat, 4 Jun 2022 00:25:59 +0200 Subject: [PATCH 14/33] Add a equality operator to core classes To compare two user instances and two appliances instances --- ramp/core/constants.py | 43 +++++++++++++++ ramp/core/core.py | 116 +++++++++++++++++++++++++--------------- ramp/core/initialise.py | 2 +- 3 files changed, 118 insertions(+), 43 deletions(-) diff --git a/ramp/core/constants.py b/ramp/core/constants.py index e3115079..8051c672 100644 --- a/ramp/core/constants.py +++ b/ramp/core/constants.py @@ -39,3 +39,46 @@ } NEW_TO_OLD_MAPPING = {value: key for (key, value) in OLD_TO_NEW_MAPPING.items()} + + +APPLIANCE_ATTRIBUTES = ( + "name", + "number", + "power", + "num_windows", + "func_time", + "time_fraction_random_variability", + "func_cycle", + "fixed", + "fixed_cycle", + "occasional_use", + "flat", + "thermal_p_var", + "pref_index", + "wd_we_type", + "p_11", + "t_11", + "cw11", + "p_12", + "t_12", + "cw12", + "r_c1", + "p_21", + "t_21", + "cw21", + "p_22", + "t_22", + "cw22", + "r_c2", + "p_31", + "t_31", + "cw31", + "p_32", + "t_32", + "cw32", + "r_c3", + "window_1", + "window_2", + "window_3", + "random_var_w", + ) \ No newline at end of file diff --git a/ramp/core/core.py b/ramp/core/core.py index 4dd12124..0702dfa3 100644 --- a/ramp/core/core.py +++ b/ramp/core/core.py @@ -4,7 +4,7 @@ import numpy as np import numpy.ma as ma import pandas as pd -from ramp.core.constants import NEW_TO_OLD_MAPPING, OLD_TO_NEW_MAPPING +from ramp.core.constants import NEW_TO_OLD_MAPPING, APPLIANCE_ATTRIBUTES from ramp.core.utils import read_input_file #%% Definition of Python classes that constitute the model architecture @@ -137,6 +137,48 @@ def save(self, filename=None): else: return answer + def __eq__(self, other_user): + """Compare two users + + ensure they have the same properties + ensure they have the same appliances + """ + answer = np.array([]) + for attribute in ("user_name", "num_users", "user_preference"): + if hasattr(self, attribute) and hasattr(other_user, attribute): + np.append( + answer, [getattr(self, attribute) == getattr(other_user, attribute)] + ) + else: + print(f"Problem with {attribute} of user") + np.append(answer, False) + answer = answer.all() + + if answer is True: + # user attributes match, continue to compare each appliance + if len(self.App_list) == len(other_user.App_list): + answer = np.array([]) + for my_appliance, their_appliance in zip( + self.App_list, other_user.App_list + ): + temp = my_appliance == their_appliance + answer = np.append(answer, temp) + if len(answer) > 0: + answer = answer.all() + else: + + if len(self.App_list) > 0: + answer = False + else: + # both users have no appliances + answer = True + else: + print( + f"The user {self.user_name} and {other_user.user_name} do not have the same number of appliances" + ) + answer = False + return answer + def export_to_dataframe(self): return self.save() @@ -177,6 +219,8 @@ def Appliance( p_series=P_series, name=name, ) + + class Appliance: def __init__( self, @@ -262,47 +306,7 @@ def save(self): dm = {} for user_attribute in ("user_name", "num_users", "user_preference"): dm[user_attribute] = getattr(self.user, user_attribute) - for attribute in ( - "name", - "number", - "power", - "num_windows", - "func_time", - "time_fraction_random_variability", - "func_cycle", - "fixed", - "fixed_cycle", - "occasional_use", - "flat", - "thermal_p_var", - "pref_index", - "wd_we_type", - "p_11", - "t_11", - "cw11", - "p_12", - "t_12", - "cw12", - "r_c1", - "p_21", - "t_21", - "cw21", - "p_22", - "t_22", - "cw22", - "r_c2", - "p_31", - "t_31", - "cw31", - "p_32", - "t_32", - "cw32", - "r_c3", - "window_1", - "window_2", - "window_3", - "random_var_w", - ): + for attribute in APPLIANCE_ATTRIBUTES: if hasattr(self, attribute): if "window_" in attribute or "cw" in attribute: @@ -346,6 +350,34 @@ def save(self): def export_to_dataframe(self): return self.save() + def __eq__(self, other_appliance): + """Compare two appliances + + ensure they have the same attributes + ensure all their attributes have the same value + """ + answer = np.array([]) + for attribute in APPLIANCE_ATTRIBUTES: + if hasattr(self, attribute) and hasattr(other_appliance, attribute): + np.append( + answer, + [getattr(self, attribute) == getattr(other_appliance, attribute)], + ) + elif ( + hasattr(self, attribute) is False + and hasattr(other_appliance, attribute) is False + ): + np.append(answer, True) + else: + if hasattr(self, attribute) is False: + print(f"{attribute} of appliance {self.name} is not assigned") + else: + print( + f"{attribute} of appliance {other_appliance.name} is not assigned" + ) + np.append(answer, False) + return answer.all() + def windows(self, window_1 = np.array([0,0]), window_2 = np.array([0,0]),random_var_w = 0, window_3 = np.array([0,0])): self.window_1 = window_1 #array of start and ending time for window of use #1 self.window_2 = window_2 #array of start and ending time for window of use #2 diff --git a/ramp/core/initialise.py b/ramp/core/initialise.py index dc0cd7f7..09456083 100644 --- a/ramp/core/initialise.py +++ b/ramp/core/initialise.py @@ -25,7 +25,7 @@ def user_defined_inputs(j=None, fname=None): ''' # Back compatibility with old code if j is not None: - file_module = importlib.import_module(f'input_files.input_file_{j}') + file_module = importlib.import_module(f'ramp.input_files.input_file_{j}') User_list = file_module.User_list if fname is not None: From 7c3971af287d2a4f12418ea74191ebbea3350afc Mon Sep 17 00:00:00 2001 From: "pierre-francois.duc" Date: Sat, 4 Jun 2022 00:28:28 +0200 Subject: [PATCH 15/33] Reformat example input file So that the test rewrites them without introducing changes in them --- ramp/input_files/input_file_1.py | 3 - ramp/input_files/input_file_2.py | 4 - ramp/input_files/input_file_3.py | 149 +++++++++++++++---------------- 3 files changed, 74 insertions(+), 82 deletions(-) diff --git a/ramp/input_files/input_file_1.py b/ramp/input_files/input_file_1.py index 84b54529..963769a1 100644 --- a/ramp/input_files/input_file_1.py +++ b/ramp/input_files/input_file_1.py @@ -239,6 +239,3 @@ S_Stereo = School.Appliance(School,1,150,2,90,0.1,5, occasional_use = 0.33) S_Stereo.windows([510,750],[810,1080],0.35) - - - diff --git a/ramp/input_files/input_file_2.py b/ramp/input_files/input_file_2.py index 512700b5..4103426a 100644 --- a/ramp/input_files/input_file_2.py +++ b/ramp/input_files/input_file_2.py @@ -24,7 +24,3 @@ #High-Income HH_shower = HH.Appliance(HH,1,HH_shower_P,2,15,0.1,3, thermal_P_var = 0.2, P_series=True) HH_shower.windows([390,540],[1080,1200],0.2) - - - - diff --git a/ramp/input_files/input_file_3.py b/ramp/input_files/input_file_3.py index 037cf9bc..ff7ecf7e 100644 --- a/ramp/input_files/input_file_3.py +++ b/ramp/input_files/input_file_3.py @@ -1,75 +1,74 @@ -# -*- coding: utf-8 -*- - -#%% Definition of the inputs -''' -Input data definition -''' - -from ramp.core.core import User, np, pd -User_list = [] - - -''' -This example input file represents a single household user whose only loads -are the "cooking" activities. The example showcases how to model electric cooking loads by means of -the Prefence Index and User Preference attributes. -''' - -#Create new user classes -HH = User("generic household",1,3) -User_list.append(HH) - -#Create new appliances - -#Create Cooking appliances - -HH_lunch1_soup = HH.Appliance(HH,1,1800,2,70,0.15,60, thermal_P_var = 0.2, pref_index =1, fixed_cycle=1) -HH_lunch1_soup.windows([12*60,15*60],[0,0],0.15) -HH_lunch1_soup.specific_cycle_1(1800,10,750,60,0.15) -HH_lunch1_soup.cycle_behaviour([12*60,15*60],[0,0]) - -HH_lunch2_rice = HH.Appliance(HH,1,1800,2,25,0.15,20, thermal_P_var = 0.2, pref_index = 2, fixed_cycle=1) -HH_lunch2_rice.windows([12*60,15*60],[0,0],0.15) -HH_lunch2_rice.specific_cycle_1(1800,10,750,15,0.15) -HH_lunch2_rice.cycle_behaviour([12*60,15*60],[0,0]) - -HH_lunch2_egg = HH.Appliance(HH,1,1200,2,3,0.2,3, thermal_P_var = 0.2 , pref_index = 2) -HH_lunch2_egg.windows([12*60,15*60],[0,0],0.15) - -HH_lunch2_platano = HH.Appliance(HH,1,1800,2,10,0.15,5, thermal_P_var = 0.2, pref_index = 2, fixed_cycle=1) -HH_lunch2_platano.windows([12*60,15*60],[0,0],0.15) -HH_lunch2_platano.specific_cycle_1(1800,5,1200,5,0.15) -HH_lunch2_platano.cycle_behaviour([12*60,15*60],[0,0]) - -HH_lunch2_meat = HH.Appliance(HH,1,1200,2,7,0.15,3, thermal_P_var = 0.2, pref_index = 2) -HH_lunch2_meat.windows([12*60,15*60],[0,0],0.15) - -HH_lunch3_beansnrice = HH.Appliance(HH,1,1800,2,45,0.2,30, thermal_P_var =0.2 , pref_index = 3, fixed_cycle=1) -HH_lunch3_beansnrice.windows([12*60,15*60],[0,0],0.15) -HH_lunch3_beansnrice.specific_cycle_1(1800,10,750,35,0.2) -HH_lunch3_beansnrice.cycle_behaviour([12*60,15*60],[0,0]) - -HH_lunch3_meat = HH.Appliance(HH,1,1200,2,10,0.2,5, thermal_P_var = 0.2, pref_index = 3) -HH_lunch3_meat.windows([12*60,15*60],[0,0],0.15) - -HH_lunch_yuca = HH.Appliance(HH,1,1800,1,25,0.15,10, thermal_P_var = 0.2, pref_index =0, fixed_cycle=1) -HH_lunch_yuca.windows([13*60,14*60],[0,0],0.15) -HH_lunch_yuca.specific_cycle_1(1800,10,750,15,0.15) -HH_lunch_yuca.cycle_behaviour([12*60,15*60],[0,0]) - -HH_breakfast_huminta = HH.Appliance(HH,1,1800,1,65,0.15,50, thermal_P_var = 0.2, pref_index =0, fixed_cycle=1) -HH_breakfast_huminta.windows([6*60,9*60],[0,0],0.15) -HH_breakfast_huminta.specific_cycle_1(1800,5,750,60,0.15) -HH_breakfast_huminta.cycle_behaviour([6*60,9*60],[0,0]) - -HH_breakfast_bread = HH.Appliance(HH,1,1800,1,15,0.15,10, thermal_P_var = 0.2, pref_index =0, fixed_cycle=1) -HH_breakfast_bread.windows([6*60,9*60],[0,0],0.15) -HH_breakfast_bread.specific_cycle_1(1800,10,1200,5,0.15) -HH_breakfast_bread.cycle_behaviour([6*60,9*60],[0,0]) - -HH_breakfast_coffee = HH.Appliance(HH,1,1800,1,5,0.15,2, thermal_P_var = 0.2, pref_index =0) -HH_breakfast_coffee.windows([6*60,9*60],[0,0],0.15) - -HH_mate = HH.Appliance(HH,1,1800,1,30,0.3,2, thermal_P_var = 0.2, pref_index =0) -HH_mate.windows([7*60,20*60],[0,0],0.15) - +# -*- coding: utf-8 -*- + +#%% Definition of the inputs +''' +Input data definition +''' + +from ramp.core.core import User, np, pd +User_list = [] + + +''' +This example input file represents a single household user whose only loads +are the "cooking" activities. The example showcases how to model electric cooking loads by means of +the Prefence Index and User Preference attributes. +''' + +#Create new user classes +HH = User("generic household",1,3) +User_list.append(HH) + +#Create new appliances + +#Create Cooking appliances + +HH_lunch1_soup = HH.Appliance(HH,1,1800,2,70,0.15,60, thermal_P_var = 0.2, pref_index =1, fixed_cycle=1) +HH_lunch1_soup.windows([12*60,15*60],[0,0],0.15) +HH_lunch1_soup.specific_cycle_1(1800,10,750,60,0.15) +HH_lunch1_soup.cycle_behaviour([12*60,15*60],[0,0]) + +HH_lunch2_rice = HH.Appliance(HH,1,1800,2,25,0.15,20, thermal_P_var = 0.2, pref_index = 2, fixed_cycle=1) +HH_lunch2_rice.windows([12*60,15*60],[0,0],0.15) +HH_lunch2_rice.specific_cycle_1(1800,10,750,15,0.15) +HH_lunch2_rice.cycle_behaviour([12*60,15*60],[0,0]) + +HH_lunch2_egg = HH.Appliance(HH,1,1200,2,3,0.2,3, thermal_P_var = 0.2 , pref_index = 2) +HH_lunch2_egg.windows([12*60,15*60],[0,0],0.15) + +HH_lunch2_platano = HH.Appliance(HH,1,1800,2,10,0.15,5, thermal_P_var = 0.2, pref_index = 2, fixed_cycle=1) +HH_lunch2_platano.windows([12*60,15*60],[0,0],0.15) +HH_lunch2_platano.specific_cycle_1(1800,5,1200,5,0.15) +HH_lunch2_platano.cycle_behaviour([12*60,15*60],[0,0]) + +HH_lunch2_meat = HH.Appliance(HH,1,1200,2,7,0.15,3, thermal_P_var = 0.2, pref_index = 2) +HH_lunch2_meat.windows([12*60,15*60],[0,0],0.15) + +HH_lunch3_beansnrice = HH.Appliance(HH,1,1800,2,45,0.2,30, thermal_P_var =0.2 , pref_index = 3, fixed_cycle=1) +HH_lunch3_beansnrice.windows([12*60,15*60],[0,0],0.15) +HH_lunch3_beansnrice.specific_cycle_1(1800,10,750,35,0.2) +HH_lunch3_beansnrice.cycle_behaviour([12*60,15*60],[0,0]) + +HH_lunch3_meat = HH.Appliance(HH,1,1200,2,10,0.2,5, thermal_P_var = 0.2, pref_index = 3) +HH_lunch3_meat.windows([12*60,15*60],[0,0],0.15) + +HH_lunch_yuca = HH.Appliance(HH,1,1800,1,25,0.15,10, thermal_P_var = 0.2, pref_index =0, fixed_cycle=1) +HH_lunch_yuca.windows([13*60,14*60],[0,0],0.15) +HH_lunch_yuca.specific_cycle_1(1800,10,750,15,0.15) +HH_lunch_yuca.cycle_behaviour([12*60,15*60],[0,0]) + +HH_breakfast_huminta = HH.Appliance(HH,1,1800,1,65,0.15,50, thermal_P_var = 0.2, pref_index =0, fixed_cycle=1) +HH_breakfast_huminta.windows([6*60,9*60],[0,0],0.15) +HH_breakfast_huminta.specific_cycle_1(1800,5,750,60,0.15) +HH_breakfast_huminta.cycle_behaviour([6*60,9*60],[0,0]) + +HH_breakfast_bread = HH.Appliance(HH,1,1800,1,15,0.15,10, thermal_P_var = 0.2, pref_index =0, fixed_cycle=1) +HH_breakfast_bread.windows([6*60,9*60],[0,0],0.15) +HH_breakfast_bread.specific_cycle_1(1800,10,1200,5,0.15) +HH_breakfast_bread.cycle_behaviour([6*60,9*60],[0,0]) + +HH_breakfast_coffee = HH.Appliance(HH,1,1800,1,5,0.15,2, thermal_P_var = 0.2, pref_index =0) +HH_breakfast_coffee.windows([6*60,9*60],[0,0],0.15) + +HH_mate = HH.Appliance(HH,1,1800,1,30,0.3,2, thermal_P_var = 0.2, pref_index =0) +HH_mate.windows([7*60,20*60],[0,0],0.15) From abdbfdbf73f6045b2373904db416fcf07a39e589 Mon Sep 17 00:00:00 2001 From: "pierre-francois.duc" Date: Sat, 4 Jun 2022 00:28:45 +0200 Subject: [PATCH 16/33] Update requirements To read the xlsx files --- requirements.txt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 44974cf4..816bf3bf 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,4 @@ pandas numpy -matplotlib \ No newline at end of file +matplotlib +openpyxl \ No newline at end of file From 99abb79356f08bc4d0b29d57ae1b85143b91f6e8 Mon Sep 17 00:00:00 2001 From: "pierre-francois.duc" Date: Tue, 7 Jun 2022 22:11:32 +0200 Subject: [PATCH 17/33] Specify openpyxl as engine for .xlsx io operations --- ramp/core/core.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ramp/core/core.py b/ramp/core/core.py index 0702dfa3..f2c9aa7e 100644 --- a/ramp/core/core.py +++ b/ramp/core/core.py @@ -30,7 +30,7 @@ def add_user(self, user): def save(self, filename=None): answer = pd.concat([user.save() for user in self.users], ignore_index=True) if filename is not None: - answer.to_excel(f"{filename}.xlsx", index=False) + answer.to_excel(f"{filename}.xlsx", index=False, engine="openpyxl") else: return answer @@ -133,7 +133,7 @@ def add_appliance(self, *args, **kwargs): def save(self, filename=None): answer = pd.concat([app.save() for app in self.App_list], ignore_index=True) if filename is not None: - answer.to_excel(f"{filename}.xlsx") + answer.to_excel(f"{filename}.xlsx", engine="openpyxl") else: return answer From 3fe272799aafe83047b1ab962814206c8b6abb79 Mon Sep 17 00:00:00 2001 From: "pierre-francois.duc" Date: Fri, 10 Jun 2022 21:35:49 +0200 Subject: [PATCH 18/33] Improve input parameters documentation --- docs/input_parameters.md | 61 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 docs/input_parameters.md diff --git a/docs/input_parameters.md b/docs/input_parameters.md new file mode 100644 index 00000000..8d16f358 --- /dev/null +++ b/docs/input_parameters.md @@ -0,0 +1,61 @@ +## Excel input file parameter description +The table below displays the input parameters of RAMP. If NA is displayed in the table below, it means that the corresponding column is not applicable to the parameter. + +TODO: specify difference between xlsx and python input (future also pack the windows param in the add, appliance method) + +When filling .xlsx input file, you can simply leave the cell empty if the parameter is not mandatory and the default value will be automatically used. + +The "allowed values" column provide information about the format one should provide when: + +* {1,2,3} is a set and the allowed value are either 1, 2 or 3; +* in [0-1440] is a range and the allowed value must lie between 0 and 1440. 0 and 1440 are also possible values. + +| Name | Unit | Allowed values | Description | Coding type | Is this value mandatory? | Default value | +|:---------------------------------|:--------|:-----------------|:----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:---------------|:---------------------------|:----------------| +| user_name | NA | NA | Name of user type | string | yes | NA | +| num_users | NA | \>=0 | Number of users within the resprective user-type | integer | yes | 0 | +| user_preference | NA | {0,1,2,3} | Related to cooking behaviour, how many types of meal a user wants a day (number of user preferences has to be defined here and will be further specified with pref_index parameter) | integer | no | 0 | +| name | NA | NA | Appliance name | string | yes | NA | +| number | NA | \>=0 | Number of appliances | integer | yes | 0 | +| power | Watt | \>=0 | Power rating of appliance (average) | Float or array | yes | 0 | +| num_windows | NA | {1,2,3} | Number of distinct time windows, e.g. if an appliance is running 24 h the num_windows is 1. If num_windows is set to x then you have to fill in the window_x_start, window_x_end and random_var_w parameters) | integer | yes | 1 | +| func_time | minutes | in [0,1440] | Total time an appliance is running in a day (not dependant on windows) | integer | yes | 0 | +| time_fraction_random_variability | % | in [0,1] | For time (not for windows), randomizes the total time the appliance is on | float | no | 0 | +| func_cycle | minutes | in [0,1440] | Running time: time the appliance is on (after switching it on) | float | yes | 1 | +| fixed | NA | {yes,no} | All appliances of the same kind (e.g. street lights) are switched on at the same time (if fixed=yes) | boolean | no | no | +| fixed_cycle | NA | {0,1,2,3} | Number of duty cycle, 0 means continuous power, if not 0 you have to fill the cw (cycle window) parameter (you may define up to 3 cws) | integer | no | 0 | +| occasional_use | % | in [0,1] | Defines how often the appliance is used, e.g. every second day will be 0.5 | float | no | 1 | +| flat | NA | {yes,no} | no variability in the time of usage, similar to fixed, but does not account for all appliances of the same type at the same time, no, if switched on and off at different times for different days | boolean | no | no | +| thermal_p_var | % | in [0,1] | Range of change of the power of the appliance (e.g. shower not taken at same temparature) or for the power of duty cycles (e.g. for a cooker, AC, heater if external temperature is different…) | float | no | 0 | +| pref_index | NA | {0,1,2,3} | This number must be smaller or equal to the value input in user_preference | integer | no | 0 | +| wd_we_type | NA | {0,1,2} | Specify whether the appliance is used only on weekdays (0), weekend (1) or the whole week (2) | integer | no | 2 | +| p_i1 | Watt | \>=0 | Power rating for first part of ith duty cycle. Only necessary if fixed_cycle is set to I or greater | float | no | 0 | +| t_i1 | minutes | in [0,1440] | Duration of first part of ith duty cycle. Only necessary if fixed_cycle is set to I or greater | float | no | 0 | +| cwi1_start | minutes | in [0,1440] | Window start time for the first part of ith specific duty cycle number (not neccessarily linked to the overall time window) | float | no | 0 | +| cwi1_end | minutes | in [0,1440] | Window end time for the first part of ith specific duty cycle number (not neccessarily linked to the overall time window) | float | no | 0 | +| p_i2 | Watt | \>=0 | Power rating for second part of ith duty cycle number. Only necessary if fixed_cycle is set to i or greater | float | no | 0 | +| t_i2 | minutes | in [0,1440] | Duration second part of ith duty cycle number. Only necessary if fixed_cycle is set to I or greater | float | no | 0 | +| cwi2_start | minutes | in [0,1440] | Window start time for the second part of ith duty cycle number (not neccessarily linked to the overall time window) | float | no | 0 | +| cwi2_end | minutes | in [0,1440] | Window end time for the second part of ith duty cycle number (not neccessarily linked to the overall time window) | float | no | 0 | +| r_ci | % | in [0,1] | randomization of the duty cycle parts’ duration. There will be a uniform random variation around t_i1 and t_i2. If this parameter is set to 0.1, then t_i1 and t_i2 will be randomly reassigned between 90% and 110% of their initial value; 0 means no randomisation | float | no | 0 | +| window_j_start | minutes | in [0,1440] | Start time of time-window j. Only necessary if num_windows is set to j or greater | integer | yes | 0 | +| window_j_end | minutes | in [0,1440] | End time of time-window j. Only necessary if num_windows is set to j or greater | integer | yes | 0 | +| random_var_w | % | in [0,1] | variability of the windows in percent, the same for all windows | float | no | 0 | + +## Python input file parameter description + +A new instance of class `User` need the parameters `user_name`, `num_users`, `user_preference` from the table above. +To add an appliance use the method `add_appliance` with at least the mandatory parameters listed in the table above (except the 3 first parameters which belong to the user class and are already assigned in this case) +and with any of the non-mandatory ones. + +If no window parameter (`window_j_start`, `window_j_end`) is provided to the `add_appliance` method of the user, then one must then call the `window` method of the appliance to provide up to 3 windows : `window_1`, `window_2`, `window_3` as well as `random_var_w` +The parameters to describe a window of time should simply directly be provided as a numpy array ( for example `window_j = np.array([window_j_start, window_j_end])`) (where j is an integer smaller or equal to the provided value of `num_windows`). + +If no duty cycle parameter is provided to the `add_appliance` method of the user, then one can then enable up to 3 duty cycle by calling the method `specific_cycle_i` of the appliance (where i is an integer smaller or equal to the provided value of `fixed_cycle`) +The parameters to describe the ith duty cycle are the following: `p_i1`, `t_i1`, `p_i2`, `t_i2`, `r_ci`, `cwi1` and `cwi2` +It is also possible to provide the parameters `cwi1` and `cwi2` using the method `cycle_behaviour` of the appliance. + +The legacy way to create an appliance instance is by using the `Appliance` method of the user (note that the names of input parameters are the old ones). This way of creating an appliance is to keep a back compatibility of the legacy input files, using the `add_appliance` method of the user should be preferred +Note that with the legacy way, one must then call the `window` method of the appliance to provide at least one windows. And one can add duty cycles only via the method `specific_cycle_i` of the appliance. + + From 7ca26dddf761cff8ca51c3f52e4ae311c3a07d3c Mon Sep 17 00:00:00 2001 From: "pierre-francois.duc" Date: Sat, 11 Jun 2022 00:06:34 +0200 Subject: [PATCH 19/33] Add tests for different initialisation of appliances --- tests/test_input_file_conversion.py | 41 ++++++++++++++++++++++++++++- 1 file changed, 40 insertions(+), 1 deletion(-) diff --git a/tests/test_input_file_conversion.py b/tests/test_input_file_conversion.py index 02f768e3..69634dc9 100644 --- a/tests/test_input_file_conversion.py +++ b/tests/test_input_file_conversion.py @@ -1,6 +1,8 @@ import os import pytest +import numpy as np +from ramp.core.core import User, Appliance from ramp.core.initialise import Initialise_inputs from ramp.ramp_convert_old_input_files import convert_old_user_input_file @@ -17,7 +19,7 @@ def load_usecase(j=None, fname=None): return User_list -class TestTransformerClass: +class TestConversion: def setup_method(self): self.input_files_to_run = [1, 2, 3] self.file_suffix = "_test" @@ -64,3 +66,40 @@ def test_convert_py_to_xlsx(self): for old_user, new_user in zip(old_user_list, new_user_list): if old_user != new_user: pytest.fail() + + +def test_define_appliance_window_directly_equivalent_to_use_windows_method(): + user = User("test user", 1) + + params = dict(number=1, power=200, num_windows=1, func_time=0) + win_start = 390 + win_stop = 540 + appliance1 = user.add_appliance(**params) + appliance1.windows(window_1=[win_start, win_stop]) + + params.update({"window_1": np.array([win_start, win_stop])}) + appliance2 = user.add_appliance(**params) + + assert appliance1 == appliance2 + + +def test_define_appliance_duty_cycle_directly_equivalent_to_use_specific_cycle_method(): + user = User("test user", 1) + + params = dict( + number=1, + power=200, + num_windows=1, + func_time=0, + window_1=[390, 540], + fixed_cycle=1, + ) + + appliance1 = user.add_appliance(**params) + cycle_params = {"p_11": 20, "t_11": 10, "cw11": np.array([400, 500])} + appliance1.specific_cycle_1(**cycle_params) + + params.update(cycle_params) + appliance2 = user.add_appliance(**params) + + assert appliance1 == appliance2 From a6dec80aa275a0e4e28fda35989e72fdbd58080b Mon Sep 17 00:00:00 2001 From: "pierre-francois.duc" Date: Sat, 11 Jun 2022 00:07:54 +0200 Subject: [PATCH 20/33] Move parameter names to constants.py --- ramp/core/constants.py | 28 +++++++++++++++++++++++++++- ramp/core/core.py | 30 +++--------------------------- 2 files changed, 30 insertions(+), 28 deletions(-) diff --git a/ramp/core/constants.py b/ramp/core/constants.py index 8051c672..c83cf9cf 100644 --- a/ramp/core/constants.py +++ b/ramp/core/constants.py @@ -81,4 +81,30 @@ "window_2", "window_3", "random_var_w", - ) \ No newline at end of file + ) + +APPLIANCE_ARGS = ( + "number", + "power", + "p_series", + "num_windows", + "func_time", + "time_fraction_random_variability", + "func_cycle", + "fixed", + "fixed_cycle", + "occasional_use", + "flat", + "thermal_p_var", + "pref_index", + "wd_we_type", + "name", + ) + +MAX_WINDOWS = 3 +WINDOWS_PARAMETERS = ("window_1", "window_2", "window_3", "random_var_w") +DUTY_CYCLE_PARAMETERS = ( + ("p_11", "t_11", "cw11", "p_12", "t_12", "cw12", "r_c1"), + ("p_21", "t_21", "cw21", "p_22", "t_22", "cw22", "r_c2"), + ("p_31", "t_31", "cw31", "p_32", "t_32", "cw32", "r_c3"), +) \ No newline at end of file diff --git a/ramp/core/core.py b/ramp/core/core.py index f2c9aa7e..fe5e0239 100644 --- a/ramp/core/core.py +++ b/ramp/core/core.py @@ -4,7 +4,7 @@ import numpy as np import numpy.ma as ma import pandas as pd -from ramp.core.constants import NEW_TO_OLD_MAPPING, APPLIANCE_ATTRIBUTES +from ramp.core.constants import NEW_TO_OLD_MAPPING, APPLIANCE_ATTRIBUTES, APPLIANCE_ARGS, WINDOWS_PARAMETERS, MAX_WINDOWS, DUTY_CYCLE_PARAMETERS from ramp.core.utils import read_input_file #%% Definition of Python classes that constitute the model architecture @@ -40,30 +40,6 @@ def export_to_dataframe(self): def load(self, filename): """Open an .xlsx file which was produces via the save method and create instances of Users and Appliances""" - appliance_args = ( - "name", - "number", - "power", - "p_series", - "num_windows", - "func_time", - "time_fraction_random_variability", - "func_cycle", - "fixed", - "fixed_cycle", - "occasional_use", - "flat", - "thermal_p_var", - "pref_index", - "wd_we_type", - ) - windows_args = ("window_1", "window_2", "window_3", "random_var_w") - cycle_args = ( - ("p_11", "t_11", "cw11", "p_12", "t_12", "cw12", "r_c1"), - ("p_21", "t_21", "cw21", "p_22", "t_22", "cw22", "r_c2"), - ("p_31", "t_31", "cw31", "p_32", "t_32", "cw32", "r_c3"), - ) - df = read_input_file(filename=filename) for user_name in df.user_name.unique(): user_df = df.loc[df.user_name == user_name] @@ -90,12 +66,12 @@ def load(self, filename): :, ~user_df.columns.isin(["user_name", "num_users", "user_preference"]) ].to_dict(orient="records"): # assign Appliance arguments - appliance_parameters = {k: row[k] for k in appliance_args} appliance = user.add_appliance(**appliance_parameters) + appliance_parameters = {k: row[k] for k in APPLIANCE_ARGS} # assign windows arguments windows_parameters = {} - for k in windows_args: + for k in WINDOWS_PARAMETERS: if "window" in k: windows_parameters[k] = np.array( [row.get(k + "_start"), row.get(k + "_end")] From 4e9669f1546b4df5b24156125e67e546053c55a9 Mon Sep 17 00:00:00 2001 From: "pierre-francois.duc" Date: Sat, 11 Jun 2022 00:10:51 +0200 Subject: [PATCH 21/33] Refactor load and add_appliance methods --- ramp/core/core.py | 56 ++++++++++++++++++++++++++++++++++------------- 1 file changed, 41 insertions(+), 15 deletions(-) diff --git a/ramp/core/core.py b/ramp/core/core.py index fe5e0239..505c595b 100644 --- a/ramp/core/core.py +++ b/ramp/core/core.py @@ -66,31 +66,29 @@ def load(self, filename): :, ~user_df.columns.isin(["user_name", "num_users", "user_preference"]) ].to_dict(orient="records"): # assign Appliance arguments - appliance = user.add_appliance(**appliance_parameters) appliance_parameters = {k: row[k] for k in APPLIANCE_ARGS} # assign windows arguments - windows_parameters = {} for k in WINDOWS_PARAMETERS: if "window" in k: - windows_parameters[k] = np.array( + appliance_parameters[k] = np.array( [row.get(k + "_start"), row.get(k + "_end")] ) else: - windows_parameters[k] = row.get(k) - appliance.windows(**windows_parameters) + appliance_parameters[k] = row.get(k) - # assign cycles arguments - for i in range(appliance.fixed_cycle): - cycle_parameters ={} - for k in cycle_args[i]: + # assign duty cycles arguments + for duty_cycle_params in DUTY_CYCLE_PARAMETERS: + for k in duty_cycle_params: if "cw" in k: - cycle_parameters[k] = np.array( - [row.get(k + "_start"), row.get(k + "_end")],dtype=np.intc + appliance_parameters[k] = np.array( + [row.get(k + "_start"), row.get(k + "_end")], dtype=np.intc ) else: - cycle_parameters[k] = row.get(k) - appliance.specific_cycle(cycle_num=i+1, **cycle_parameters) + appliance_parameters[k] = row.get(k) + + user.add_appliance(**appliance_parameters) + class User: @@ -103,8 +101,36 @@ def __init__(self, user_name="", num_users=1, user_preference=0): ) # each instance of User (i.e. each user class) has its own list of Appliances def add_appliance(self, *args, **kwargs): - # I would add the appliance explicitely here, unless the appliance works only if a windows is defined - return Appliance(self, *args, **kwargs) + + # parse the args into the kwargs + if len(args) > 0: + for a_name, a_val in zip(APPLIANCE_ARGS, args): + kwargs[a_name] = a_val + + # collects windows arguments + windows_args = {} + for k in WINDOWS_PARAMETERS: + if k in kwargs: + windows_args[k] = kwargs.pop(k) + + # collects duty cycles arguments + duty_cycle_parameters = {} + for i, duty_cycle_params in enumerate(DUTY_CYCLE_PARAMETERS): + cycle_parameters = {} + for k in duty_cycle_params: + if k in kwargs: + cycle_parameters[k] = kwargs.pop(k) + if cycle_parameters: + duty_cycle_parameters[i+1] = cycle_parameters + + app = Appliance(self, **kwargs) + + if windows_args: + app.windows(**windows_args) + for i in duty_cycle_parameters: + app.specific_cycle(i, **duty_cycle_parameters[i]) + + return app def save(self, filename=None): answer = pd.concat([app.save() for app in self.App_list], ignore_index=True) From 8090bf3f708396f14b07890cdec217c3a51e5e1c Mon Sep 17 00:00:00 2001 From: "pierre-francois.duc" Date: Sat, 11 Jun 2022 00:11:53 +0200 Subject: [PATCH 22/33] Assign default values to appliance attributes --- ramp/core/core.py | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/ramp/core/core.py b/ramp/core/core.py index 505c595b..0d30d392 100644 --- a/ramp/core/core.py +++ b/ramp/core/core.py @@ -271,26 +271,26 @@ def __init__( self.power = power * np.ones(365) # treat the power as single value for the entire year # attributes initialized by self.windows - self.random_var_w = None + self.random_var_w = 0 self.daily_use = None self.daily_use_masked = None # attributes used for specific fixed and random cycles - self.p_11 = None - self.p_12 = None - self.t_11 = None - self.t_12 = None - self.r_c1 = None - self.p_21 = None - self.p_22 = None - self.t_21 = None - self.t_22 = None - self.r_c2 = None - self.p_31 = None - self.p_32 = None - self.t_31 = None - self.t_32 = None - self.r_c3 = None + self.p_11 = 0 + self.p_12 = 0 + self.t_11 = 0 + self.t_12 = 0 + self.r_c1 = 0 + self.p_21 = 0 + self.p_22 = 0 + self.t_21 = 0 + self.t_22 = 0 + self.r_c2 = 0 + self.p_31 = 0 + self.p_32 = 0 + self.t_31 = 0 + self.t_32 = 0 + self.r_c3 = 0 # attribute used for cycle_behaviour self.cw11 = np.array([0, 0]) From 636724c58a2daa96c91335c211a19cdae6ea3c69 Mon Sep 17 00:00:00 2001 From: "pierre-francois.duc" Date: Mon, 13 Jun 2022 22:20:03 +0200 Subject: [PATCH 23/33] Add test to define minimal number of windows --- tests/test_input_file_conversion.py | 62 +++++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) diff --git a/tests/test_input_file_conversion.py b/tests/test_input_file_conversion.py index 69634dc9..0c04a2e3 100644 --- a/tests/test_input_file_conversion.py +++ b/tests/test_input_file_conversion.py @@ -103,3 +103,65 @@ def test_define_appliance_duty_cycle_directly_equivalent_to_use_specific_cycle_m appliance2 = user.add_appliance(**params) assert appliance1 == appliance2 + + +def test_provide_only_one_appliance_window_when_declaring_two(): + user = User("test user", 1) + + params = dict(number=1, power=200, num_windows=2, func_time=0) + win_start = 390 + win_stop = 540 + with pytest.raises(ValueError): + appliance1 = user.add_appliance(**params) + appliance1.windows(window_1=[win_start, win_stop]) + with pytest.raises(ValueError): + params.update({"window_1": np.array([win_start, win_stop])}) + user.add_appliance(**params) + + +def test_provide_no_appliance_window_when_declaring_one(): + user = User("test user", 1) + + params = dict(number=1, power=200, num_windows=1, func_time=0) + with pytest.warns(UserWarning): + appliance1 = user.add_appliance(**params) + appliance1.windows() + with pytest.warns(UserWarning): + params.update({"window_1": None}) + user.add_appliance(**params) + + +def test_A(): + user = User("test user", 1) + + old_params = dict(n=1, P=200, w=1, t=0) + win_start = 390 + win_stop = 540 + appliance1 = user.Appliance(user, **old_params) + appliance1.windows(window_1=[win_start, win_stop]) + + params = dict(number=1, power=200, num_windows=1, func_time=0, window_1=np.array([win_start, win_stop])) + appliance2 = user.add_appliance(**params) + + assert appliance1 == appliance2 + +def test_B(): + user = User("test user", 1) + + params = dict( + number=1, + power=200, + num_windows=1, + func_time=0, + window_1=[390, 540], + fixed_cycle=1, + ) + + appliance1 = user.add_appliance(**params) + cycle_params = {"p_11": 20, "t_11": 10, "cw11": np.array([400, 500])} + appliance1.specific_cycle_1(**cycle_params) + + params.update(cycle_params) + appliance2 = user.add_appliance(**params) + + assert appliance1 == appliance2 \ No newline at end of file From 84310b4446628b97205f9e50ae0a250999925a78 Mon Sep 17 00:00:00 2001 From: "pierre-francois.duc" Date: Mon, 13 Jun 2022 22:21:10 +0200 Subject: [PATCH 24/33] Fix the test by adjusting windows default and allowed values --- ramp/core/core.py | 69 ++++++++++++++++++++++++++++++++++------------- 1 file changed, 51 insertions(+), 18 deletions(-) diff --git a/ramp/core/core.py b/ramp/core/core.py index 0d30d392..3776b785 100644 --- a/ramp/core/core.py +++ b/ramp/core/core.py @@ -4,6 +4,7 @@ import numpy as np import numpy.ma as ma import pandas as pd +import warnings from ramp.core.constants import NEW_TO_OLD_MAPPING, APPLIANCE_ATTRIBUTES, APPLIANCE_ARGS, WINDOWS_PARAMETERS, MAX_WINDOWS, DUTY_CYCLE_PARAMETERS from ramp.core.utils import read_input_file @@ -71,21 +72,31 @@ def load(self, filename): # assign windows arguments for k in WINDOWS_PARAMETERS: if "window" in k: - appliance_parameters[k] = np.array( - [row.get(k + "_start"), row.get(k + "_end")] - ) + w_start = row.get(k + "_start") + w_end = row.get(k + "_end") + if w_start is not None and w_end is not None: + appliance_parameters[k] = np.array( + [w_start, w_end], dtype=np.intc + ) else: - appliance_parameters[k] = row.get(k) + val = row.get(k) + if val is not None: + appliance_parameters[k] = val # assign duty cycles arguments for duty_cycle_params in DUTY_CYCLE_PARAMETERS: for k in duty_cycle_params: if "cw" in k: - appliance_parameters[k] = np.array( - [row.get(k + "_start"), row.get(k + "_end")], dtype=np.intc - ) + cw_start= row.get(k + "_start") + cw_end = row.get(k + "_end") + if cw_start is not None and cw_end is not None: + appliance_parameters[k] = np.array( + [cw_start, cw_end], dtype=np.intc + ) else: - appliance_parameters[k] = row.get(k) + val = row.get(k) + if val is not None: + appliance_parameters[k] = val user.add_appliance(**appliance_parameters) @@ -272,6 +283,12 @@ def __init__( # attributes initialized by self.windows self.random_var_w = 0 + self.window_1 = np.array([0, 0]) + self.window_2 = np.array([0, 0]) + self.window_3 = np.array([0, 0]) + self.random_var_1 = 0 + self.random_var_2 = 0 + self.random_var_3 = 0 self.daily_use = None self.daily_use_masked = None @@ -380,19 +397,35 @@ def __eq__(self, other_appliance): np.append(answer, False) return answer.all() - def windows(self, window_1 = np.array([0,0]), window_2 = np.array([0,0]),random_var_w = 0, window_3 = np.array([0,0])): - self.window_1 = window_1 #array of start and ending time for window of use #1 - self.window_2 = window_2 #array of start and ending time for window of use #2 - self.window_3 = window_3 #array of start and ending time for window of use #3 + def windows(self, window_1=None, window_2=None, random_var_w=0, window_3=None): + print(window_1, window_2, window_3) + if window_1 is None: + warnings.warn(UserWarning("No windows is declared, default window of 24 hours is selected")) + self.window_1 = np.array([0, 1440]) + else: + self.window_1 = window_1 + + if window_2 is None: + if self.num_windows >= 2: + raise ValueError("Windows 2 is not provided although 2 windows were declared") + else: + self.window_2 = window_2 + + if window_3 is None: + if self.num_windows == 3: + raise ValueError("Windows 3 is not provided although 3 windows were declared") + else: + self.window_3 = window_3 + print(self.window_1, self.window_2, self.window_3) self.random_var_w = random_var_w #percentage of variability in the start and ending times of the windows self.daily_use = np.zeros(1440) #create an empty daily use profile - self.daily_use[window_1[0]:(window_1[1])] = np.full(np.diff(window_1),0.001) #fills the daily use profile with infinitesimal values that are just used to identify the functioning windows - self.daily_use[window_2[0]:(window_2[1])] = np.full(np.diff(window_2),0.001) #same as above for window2 - self.daily_use[window_3[0]:(window_3[1])] = np.full(np.diff(window_3),0.001) #same as above for window3 + self.daily_use[self.window_1[0]:(self.window_1[1])] = np.full(np.diff(self.window_1),0.001) #fills the daily use profile with infinitesimal values that are just used to identify the functioning windows + self.daily_use[self.window_2[0]:(self.window_2[1])] = np.full(np.diff(self.window_2),0.001) #same as above for window2 + self.daily_use[self.window_3[0]:(self.window_3[1])] = np.full(np.diff(self.window_3),0.001) #same as above for window3 self.daily_use_masked = np.zeros_like(ma.masked_not_equal(self.daily_use,0.001)) #apply a python mask to the daily_use array to make only functioning windows 'visibile' - self.random_var_1 = int(random_var_w*np.diff(window_1)) #calculate the random variability of window1, i.e. the maximum range of time they can be enlarged or shortened - self.random_var_2 = int(random_var_w*np.diff(window_2)) #same as above - self.random_var_3 = int(random_var_w*np.diff(window_3)) #same as above + self.random_var_1 = int(random_var_w*np.diff(self.window_1)) #calculate the random variability of window1, i.e. the maximum range of time they can be enlarged or shortened + self.random_var_2 = int(random_var_w*np.diff(self.window_2)) #same as above + self.random_var_3 = int(random_var_w*np.diff(self.window_3)) #same as above self.user.App_list.append(self) #automatically appends the appliance to the user's appliance list if self.fixed_cycle == 1: From 84a53054bc417adb0ee02a4de174050c3b1f2cf8 Mon Sep 17 00:00:00 2001 From: "pierre-francois.duc" Date: Tue, 14 Jun 2022 15:12:18 +0200 Subject: [PATCH 25/33] Edit input parameter description --- docs/input_parameters.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/input_parameters.md b/docs/input_parameters.md index 8d16f358..a6d181c2 100644 --- a/docs/input_parameters.md +++ b/docs/input_parameters.md @@ -48,7 +48,7 @@ A new instance of class `User` need the parameters `user_name`, `num_users`, `us To add an appliance use the method `add_appliance` with at least the mandatory parameters listed in the table above (except the 3 first parameters which belong to the user class and are already assigned in this case) and with any of the non-mandatory ones. -If no window parameter (`window_j_start`, `window_j_end`) is provided to the `add_appliance` method of the user, then one must then call the `window` method of the appliance to provide up to 3 windows : `window_1`, `window_2`, `window_3` as well as `random_var_w` +If no window parameter (`window_j_start`, `window_j_end`) is provided to the `add_appliance` method of the user, then one must then call the `windows` method of the appliance to provide up to 3 windows : `window_1`, `window_2`, `window_3` as well as `random_var_w` The parameters to describe a window of time should simply directly be provided as a numpy array ( for example `window_j = np.array([window_j_start, window_j_end])`) (where j is an integer smaller or equal to the provided value of `num_windows`). If no duty cycle parameter is provided to the `add_appliance` method of the user, then one can then enable up to 3 duty cycle by calling the method `specific_cycle_i` of the appliance (where i is an integer smaller or equal to the provided value of `fixed_cycle`) @@ -56,6 +56,6 @@ The parameters to describe the ith duty cycle are the following: `p_i1`, `t_i1`, It is also possible to provide the parameters `cwi1` and `cwi2` using the method `cycle_behaviour` of the appliance. The legacy way to create an appliance instance is by using the `Appliance` method of the user (note that the names of input parameters are the old ones). This way of creating an appliance is to keep a back compatibility of the legacy input files, using the `add_appliance` method of the user should be preferred -Note that with the legacy way, one must then call the `window` method of the appliance to provide at least one windows. And one can add duty cycles only via the method `specific_cycle_i` of the appliance. +Note that with the legacy way, one must then call the `windows` method of the appliance to provide at least one windows. And one can add duty cycles only via the method `specific_cycle_i` of the appliance. From fb35e4084aa559311af6fc39838bb96913bc0fd4 Mon Sep 17 00:00:00 2001 From: "pierre-francois.duc" Date: Tue, 14 Jun 2022 15:12:44 +0200 Subject: [PATCH 26/33] Add authors who contributed to parameter description --- AUTHORS | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/AUTHORS b/AUTHORS index dbc9d525..e8902d0e 100644 --- a/AUTHORS +++ b/AUTHORS @@ -4,4 +4,7 @@ Sylvain Quoilin, Université de Liège Emanuela Colombo, Politecnico di Milano Nicolò Stevanato, Politecnico di Milano William Clements, University of Bristol -Pierre-Francois Duc, Reiner Lemoine Institut \ No newline at end of file +Gokarna Dhungel, Reiner Lemoine Institut +Pierre-Francois Duc, Reiner Lemoine Institut +Avia Linke, Reiner Lemoine Institut +Katrin Lammers, Reiner Lemoine Institut \ No newline at end of file From 9fa41fd415ee05b71fa35d12bdb1f9635e508fba Mon Sep 17 00:00:00 2001 From: "pierre-francois.duc" Date: Tue, 14 Jun 2022 15:16:22 +0200 Subject: [PATCH 27/33] Add how to run unit tests in RAMP --- CONTRIBUTING.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 972f8c9a..4bf3c62b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -52,6 +52,10 @@ This functionality is accessible via `test/test_run.py`. To run the qualitative Ideally, the difference between reference and new results should be minimal and just due to the stochastic nature of the code. If more pronounced, it should be fully explainable based on the changes made to the code and aligned to the expectations of the developers (i.e. it should reflect a change in the output *wanted* and precisely *sought* with the commit in question). +### Unit tests + +Install `pytest` (`pip install pytest`) and run `pytest tests/` form the root of the repository to run the unit tests + ## Attribution The layout and content of this document is partially based on [calliope](https://github.com/calliope-project/calliope/blob/master/CONTRIBUTING.md)'s equivalent document. \ No newline at end of file From f97ef9d47a5ded6e6600dba4a71cc89dc95a254d Mon Sep 17 00:00:00 2001 From: "pierre-francois.duc" Date: Sat, 18 Jun 2022 23:48:59 +0200 Subject: [PATCH 28/33] Replace None by Nan in input file loader --- ramp/core/core.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/ramp/core/core.py b/ramp/core/core.py index 3776b785..6875994e 100644 --- a/ramp/core/core.py +++ b/ramp/core/core.py @@ -72,30 +72,30 @@ def load(self, filename): # assign windows arguments for k in WINDOWS_PARAMETERS: if "window" in k: - w_start = row.get(k + "_start") - w_end = row.get(k + "_end") - if w_start is not None and w_end is not None: + w_start = row.get(k + "_start", np.NaN) + w_end = row.get(k + "_end", np.NaN) + if not np.isnan(w_start) and not np.isnan(w_end): appliance_parameters[k] = np.array( [w_start, w_end], dtype=np.intc ) else: - val = row.get(k) - if val is not None: + val = row.get(k, np.NaN) + if not np.isnan(val): appliance_parameters[k] = val # assign duty cycles arguments for duty_cycle_params in DUTY_CYCLE_PARAMETERS: for k in duty_cycle_params: if "cw" in k: - cw_start= row.get(k + "_start") - cw_end = row.get(k + "_end") - if cw_start is not None and cw_end is not None: + cw_start = row.get(k + "_start", np.NaN) + cw_end = row.get(k + "_end", np.NaN) + if not np.isnan(cw_start) and not np.isnan(cw_end): appliance_parameters[k] = np.array( [cw_start, cw_end], dtype=np.intc ) else: - val = row.get(k) - if val is not None: + val = row.get(k, np.NaN) + if not np.isnan(val): appliance_parameters[k] = val user.add_appliance(**appliance_parameters) From b6cd817e0a1178d1ca5fef8c5748ccc306272f0f Mon Sep 17 00:00:00 2001 From: "pierre-francois.duc" Date: Wed, 29 Jun 2022 12:54:57 +0200 Subject: [PATCH 29/33] Add environment.yml file --- environment.yml | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 environment.yml diff --git a/environment.yml b/environment.yml new file mode 100644 index 00000000..a556f539 --- /dev/null +++ b/environment.yml @@ -0,0 +1,5 @@ +name: ramp_env +dependencies: +- python=3.8 +- pip: + - -r requirements.txt \ No newline at end of file From a094edf87967be2c39b9b91064c263f1e1399baa Mon Sep 17 00:00:00 2001 From: "pierre-francois.duc" Date: Wed, 29 Jun 2022 13:25:04 +0200 Subject: [PATCH 30/33] Split ramp older version from newer To run with excel files use python ramp.py from the repository core, ramp_run.py is now similar to the previous version, so that users can call the code from python IDE --- ramp.py | 65 +++++++++++++++++++++++++++++++++++++++++++ ramp/ramp_run.py | 72 +++++++++--------------------------------------- 2 files changed, 78 insertions(+), 59 deletions(-) create mode 100644 ramp.py diff --git a/ramp.py b/ramp.py new file mode 100644 index 00000000..5a904a6c --- /dev/null +++ b/ramp.py @@ -0,0 +1,65 @@ +import argparse + +from ramp.ramp_run import run_usecase + + +parser = argparse.ArgumentParser( + prog="python ramp_run.py", description="Execute RAMP code" +) +parser.add_argument( + "-i", + dest="fname_path", + nargs="+", + type=str, + help="path to the (xlsx) input files (including filename). If not provided, then legacy .py input files will be fetched", +) +parser.add_argument( + "-n", + dest="num_profiles", + nargs="+", + type=int, + help="number of profiles to be generated", +) + + +if __name__ == "__main__": + + args = vars(parser.parse_args()) + fnames = args["fname_path"] + num_profiles = args["num_profiles"] + # Define which input files should be considered and run. + + + if fnames is None: + print("Please provide path to input file with option -i, \n\nDefault to old version of RAMP input files\n") + # Files are specified as numbers in a list (e.g. [1,2] will consider input_file_1.py and input_file_2.py) + from ramp.ramp_run import input_files_to_run + + if num_profiles is not None: + if len(num_profiles) == 1: + num_profiles = num_profiles * len(input_files_to_run) + else: + if len(num_profiles) != len(input_files_to_run): + raise ValueError("The number of profiles parameters should match the number of input files provided") + else: + num_profiles = [None] * len(input_files_to_run) + + for i, j in enumerate(input_files_to_run): + run_usecase(j=j, num_profiles=num_profiles[i]) + else: + if num_profiles is not None: + if len(num_profiles) == 1: + num_profiles = num_profiles * len(fnames) + else: + if len(num_profiles) != len(fnames): + raise ValueError( + "The number of profiles parameters should match the number of input files provided") + else: + num_profiles = [None] * len(fnames) + + for i, fname in enumerate(fnames): + run_usecase(fname=fname, num_profiles=num_profiles[i]) + + + + diff --git a/ramp/ramp_run.py b/ramp/ramp_run.py index 689b2b35..ee5f7d93 100644 --- a/ramp/ramp_run.py +++ b/ramp/ramp_run.py @@ -25,30 +25,14 @@ import sys,os sys.path.append('../') -import argparse - - -from core.stochastic_process import Stochastic_Process -from post_process import post_process as pp - - -parser = argparse.ArgumentParser( - prog="python ramp_run.py", description="Execute RAMP code" -) -parser.add_argument( - "-i", - dest="fname_path", - nargs="+", - type=str, - help="path to the (xlsx) input files (including filename). If not provided, then legacy .py input files will be fetched", -) -parser.add_argument( - "-n", - dest="num_profiles", - nargs="+", - type=int, - help="number of profiles to be generated", -) + +try: + from .core.stochastic_process import Stochastic_Process + from .post_process import post_process as pp +except ImportError: + from core.stochastic_process import Stochastic_Process + from post_process import post_process as pp + def run_usecase(j=None, fname=None, num_profiles=None): # Calls the stochastic process and saves the result in a list of stochastic profiles @@ -63,44 +47,14 @@ def run_usecase(j=None, fname=None, num_profiles=None): if len(Profiles_list) > 1: # if more than one daily profile is generated, also cloud plots are shown pp.Profile_cloud_plot(Profiles_list, Profiles_avg) +input_files_to_run = [1, 2, 3] if __name__ == "__main__": - args = vars(parser.parse_args()) - fnames = args["fname_path"] - num_profiles = args["num_profiles"] - # Define which input files should be considered and run. - - - if fnames is None: - print("Please provide path to input file with option -i, \n\nDefault to old version of RAMP input files\n") - # Files are specified as numbers in a list (e.g. [1,2] will consider input_file_1.py and input_file_2.py) - input_files_to_run = [1, 2, 3] - - if num_profiles is not None: - if len(num_profiles) == 1: - num_profiles = num_profiles * len(input_files_to_run) - else: - if len(num_profiles) != len(input_files_to_run): - raise ValueError("The number of profiles parameters should match the number of input files provided") - else: - num_profiles = [None] * len(input_files_to_run) - - for i, j in enumerate(input_files_to_run): - run_usecase(j=j, num_profiles=num_profiles[i]) - else: - if num_profiles is not None: - if len(num_profiles) == 1: - num_profiles = num_profiles * len(fnames) - else: - if len(num_profiles) != len(fnames): - raise ValueError( - "The number of profiles parameters should match the number of input files provided") - else: - num_profiles = [None] * len(fnames) - - for i, fname in enumerate(fnames): - run_usecase(fname=fname, num_profiles=num_profiles[i]) + + + for i, j in enumerate(input_files_to_run): + run_usecase(j=j) From 4e0edb404639bb81f05ca21fd9b171b5a56a54d2 Mon Sep 17 00:00:00 2001 From: "pierre-francois.duc" Date: Wed, 29 Jun 2022 13:25:25 +0200 Subject: [PATCH 31/33] Fix input files path --- ramp/input_files/input_file_2.py | 7 +++++-- ramp/post_process/post_process.py | 6 ++++-- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/ramp/input_files/input_file_2.py b/ramp/input_files/input_file_2.py index 4103426a..2e1c917a 100644 --- a/ramp/input_files/input_file_2.py +++ b/ramp/input_files/input_file_2.py @@ -5,9 +5,11 @@ Input data definition ''' +import os + from ramp.core.core import User, np, pd User_list = [] - +INPUT_PATH = os.path.dirname(os.path.abspath(__file__)) ''' This example input file represents a single household user whose only load @@ -19,7 +21,8 @@ HH = User("generic households",1) User_list.append(HH) -HH_shower_P = pd.read_csv('input_files/time_series/shower_P.csv') + +HH_shower_P = pd.read_csv(os.path.join(INPUT_PATH, 'time_series/shower_P.csv')) #High-Income HH_shower = HH.Appliance(HH,1,HH_shower_P,2,15,0.1,3, thermal_P_var = 0.2, P_series=True) diff --git a/ramp/post_process/post_process.py b/ramp/post_process/post_process.py index 3a13df7b..a3dee56e 100644 --- a/ramp/post_process/post_process.py +++ b/ramp/post_process/post_process.py @@ -7,6 +7,8 @@ import numpy as np import pandas as pd +BASE_PATH = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + #%% Post-processing ''' Just some additional code lines to calculate useful indicators and generate plots @@ -69,6 +71,6 @@ def Profile_series_plot(stoch_profiles_series): def export_series(stoch_profiles_series, j=None, fname=None): series_frame = pd.DataFrame(stoch_profiles_series) if j is not None: - series_frame.to_csv('results/output_file_%d.csv' % (j)) + series_frame.to_csv(os.path.join(BASE_PATH, 'results', 'output_file_%d.csv' % (j))) if fname is not None: - series_frame.to_csv(f'results/output_file_{os.path.split(fname)[-1]}.csv') + series_frame.to_csv(os.path.join(BASE_PATH, 'results', f'output_file_{os.path.split(fname)[-1]}.csv')) From 59ca5be022b909b69f1ea940973d2201992f99c4 Mon Sep 17 00:00:00 2001 From: "pierre-francois.duc" Date: Wed, 29 Jun 2022 13:26:17 +0200 Subject: [PATCH 32/33] Remove TODO --- docs/input_parameters.md | 2 -- ramp.py | 5 ----- ramp/core/core.py | 1 - ramp/ramp_run.py | 4 ++-- 4 files changed, 2 insertions(+), 10 deletions(-) diff --git a/docs/input_parameters.md b/docs/input_parameters.md index a6d181c2..3454b323 100644 --- a/docs/input_parameters.md +++ b/docs/input_parameters.md @@ -1,8 +1,6 @@ ## Excel input file parameter description The table below displays the input parameters of RAMP. If NA is displayed in the table below, it means that the corresponding column is not applicable to the parameter. -TODO: specify difference between xlsx and python input (future also pack the windows param in the add, appliance method) - When filling .xlsx input file, you can simply leave the cell empty if the parameter is not mandatory and the default value will be automatically used. The "allowed values" column provide information about the format one should provide when: diff --git a/ramp.py b/ramp.py index 5a904a6c..eb8852a0 100644 --- a/ramp.py +++ b/ramp.py @@ -29,7 +29,6 @@ num_profiles = args["num_profiles"] # Define which input files should be considered and run. - if fnames is None: print("Please provide path to input file with option -i, \n\nDefault to old version of RAMP input files\n") # Files are specified as numbers in a list (e.g. [1,2] will consider input_file_1.py and input_file_2.py) @@ -59,7 +58,3 @@ for i, fname in enumerate(fnames): run_usecase(fname=fname, num_profiles=num_profiles[i]) - - - - diff --git a/ramp/core/core.py b/ramp/core/core.py index 6875994e..323205c2 100644 --- a/ramp/core/core.py +++ b/ramp/core/core.py @@ -398,7 +398,6 @@ def __eq__(self, other_appliance): return answer.all() def windows(self, window_1=None, window_2=None, random_var_w=0, window_3=None): - print(window_1, window_2, window_3) if window_1 is None: warnings.warn(UserWarning("No windows is declared, default window of 24 hours is selected")) self.window_1 = np.array([0, 1440]) diff --git a/ramp/ramp_run.py b/ramp/ramp_run.py index ee5f7d93..37b0c5bf 100644 --- a/ramp/ramp_run.py +++ b/ramp/ramp_run.py @@ -47,11 +47,11 @@ def run_usecase(j=None, fname=None, num_profiles=None): if len(Profiles_list) > 1: # if more than one daily profile is generated, also cloud plots are shown pp.Profile_cloud_plot(Profiles_list, Profiles_avg) -input_files_to_run = [1, 2, 3] -if __name__ == "__main__": +input_files_to_run = [1, 2, 3] +if __name__ == "__main__": for i, j in enumerate(input_files_to_run): run_usecase(j=j) From bd5a936e4e75c660fac9d64dbb1334951ad23e15 Mon Sep 17 00:00:00 2001 From: "pierre-francois.duc" Date: Wed, 29 Jun 2022 13:35:43 +0200 Subject: [PATCH 33/33] Update README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 3f04e3a5..6777080c 100644 --- a/README.md +++ b/README.md @@ -41,7 +41,7 @@ The `user preference` defines how many types of meal are available for each user This input file is used in [this publication](https://doi.org/10.1109/PTC.2019.8810571) ### Spreadsheet input files -It is also possible to use spreadsheets as input files with the option `-i`: `python ramp_run.py -i `. If you already know how many profile you want to simulate you can indicate it with the `-n` option: `python ramp_run.py -i -n 10` will simulate 10 profiles. Note that you can use this option without providing a `.xlsx` input file, this will then be equivalent to running `python ramp_run.py` without being prompted for the number of profile within the console. +It is also possible to use spreadsheets as input files. To do so you need to run the `ramp.py` file which is at the root of the repository with the option `-i`: `python ramp.py -i `. If you already know how many profile you want to simulate you can indicate it with the `-n` option: `python ramp.py -i -n 10` will simulate 10 profiles. Note that you can use this option without providing a `.xlsx` input file with the `-i` option, this will then be equivalent to running `python ramp_run.py` from the `ramp` folder without being prompted for the number of profile within the console. ### Convert python input files to xlsx If you have existing python input files, you can convert them to spreadsheet. To do so, go to `ramp` folder and run