From 045fff10966b8003c56242e8c774b6e71c3adf6d Mon Sep 17 00:00:00 2001 From: Silke Verbruggen <40292967+SilkeVerbruggen@users.noreply.github.com> Date: Mon, 17 Feb 2020 17:02:02 +0100 Subject: [PATCH 1/8] Sharing Of Appliances Currently the load per appliance is calculated for each person and just added together. This means that for example for the fridge the load is added multiple times. The load for the fridge will therefore be overestimated. To allow for sharing: the load calculation in 'residential.py' is adapted for each type of appliance. An extra function was added in 'stats.py'. --- residential.py | 815 +++++++++++++++++++++++++++++++++++++++++++++++++ stats.py | 131 ++++++++ 2 files changed, 946 insertions(+) create mode 100644 residential.py create mode 100644 stats.py diff --git a/residential.py b/residential.py new file mode 100644 index 0000000..26dddb2 --- /dev/null +++ b/residential.py @@ -0,0 +1,815 @@ +# -*- coding: utf-8 -*- +""" +Created on Mon October 07 16:16:10 2013 + +@author: Ruben Baetens +""" + +import sys +import random +import numpy as np +import time +import datetime +import calendar +import ast +import os +import cPickle +import itertools + +import stats +import data + +sys.path.append("..") +from Data.Households import households +from Data.Appliances import set_appliances + +#changes for new cold-appliance fix ####################################### +# Based on 10000 runs, these new values combined with rule-based fix (see in def appliances) +# lead to the same overall ownership as the original values. +# We change it here so that the original remain in the Appliances file. +set_appliances['Refrigerator']['owner']=0.27 # original: 0.430 +set_appliances['FridgeFreezer']['owner']=0.40 # original: 0.651 +set_appliances['ChestFreezer']['owner']=0.19 # original: 0.163 +set_appliances['UprightFreezer']['owner']=0.31 # original: 0.291 + +#################################################################### + +class Household(object): + ''' + The Household class is the main class of ProclivityPy, defining the + household composition as input or randomized based on statistics and + allowing simulation of the household for building energy simulations. + + Main functions are: + - __init__(), which ... + - self.parameterize(), which ... + - self.simulate(), which ... + ''' + + def __init__(self, name, **kwargs): + ''' + Initiation of Household object. + ''' + # input ############################################################### + # check on correct parameter input for use of functions as name should + # be a string. + try: + if not isinstance(name, str): + raise ValueError('Given name %d is not a string' % str(name)) + except: + raise TypeError('give another name') + # first define the name of the household object + self.creation = time.asctime() + self.name = name + self.parameterize(**kwargs) + + def parameterize(self, **kwargs): + ''' + Get a household definition for occupants and present appliances based + on average statistics or the given kwargs. + ''' + + def members(**kwargs): + ''' + Define the employment type of all household members based on time + use survey statistics or the given kwargs. + ''' + members = [] + # First we check if membertypes are given as **kwargs + if kwargs.has_key('members'): + if isinstance(kwargs['members'], list): + members = kwargs['members'] + else: + raise TypeError('Given membertypes is no List of strings.') + # If no types are given, random statististics are applied + else: + key = random.randint(1, len(households)) + members = households[key] + # And return the members as list fo strings + return members + + def appliances(): + ''' + Define the pressent household appliances based on average national + statistics independent of household member composition. + ''' + # Loop through all appliances and pick randomly based on the + # rate of ownership. + app_n = [] + for app in set_appliances: + if set_appliances[app]['type'] == 'appliance': + obj = Equipment(**set_appliances[app]) + owner = obj.owner >= random.random() + app_n.append(app) if owner else None + + # Cold appliances fix: ############################################### + if not ('FridgeFreezer' in app_n) and not ('Refrigerator' in app_n): # if there was none of the two-> add one of the two. + # Find probability of household to own FF instead of R: (FF ownership over sum of two ownerships-> scale to 0-1 interval) + prob=set_appliances['FridgeFreezer']['owner']/(set_appliances['FridgeFreezer']['owner']+set_appliances['Refrigerator']['owner']) + # if random number is below prob, then the household will own a FF, otherwise a R -> add it + app_n.append('FridgeFreezer') if prob >= random.random() else app_n.append('Refrigerator') + + if 'FridgeFreezer' in app_n and 'ChestFreezer' in app_n and 'UprightFreezer' in app_n: #if there were 3 freezers-> remove a freezer-only + #find probability of household to own CF instead of UF: (CF ownership over sum of two ownerships-> scale to 0-1 interval) + prob=set_appliances['ChestFreezer']['owner']/(set_appliances['ChestFreezer']['owner']+set_appliances['UprightFreezer']['owner']) + # if random number is below prob, then the household will own a CF, otherwise an UF-> remove the other + app_n.remove('UprightFreezer') if prob >= random.random() else app_n.remove('ChestFreezer') #remove the one you don't own + + ######################################################################### + return app_n + + def tappings(): + ''' + Define the present household tapping types. + ''' + tap_n = ['shortFlow', 'mediumFlow', 'bathFlow', 'showerFlow'] + return tap_n + + def clusters(members): + ''' + Allocate each household member to the correct cluster based on the + members occupation in time use survey data. + ''' + clusters = [] + # loop for every individual in the household + for ind in members: + if ind != 'U12': + clu_i = data.get_clusters(ind) + clusters.append(clu_i) + # and return the list of clusters + return clusters + # and run all + self.members = members(**kwargs) + self.apps = appliances() + self.taps = tappings() + self.clusters = clusters(self.members) + # and return + print ('Household-object created and parameterized.') + print (' - Employment types are %s' % str(self.members)) + summary = [] #loop dics and remove dubbles + for member in self.clusters: + summary += member.values() + print (' - Set of clusters is %s' % str(list(set(summary)))) + + return None + + def simulate(self, year=2013, ndays=365): + ''' + The simulate function includes the simulation of the household + occupancies, plug loads, lighting loads and hot water tappings. + ''' + + self.year = year + self.__chronology__(year, ndays) + self.__occupancy__() + self.__plugload__() + self.__dhwload__() + self.__shsetting__() + + def __chronology__(self, year, ndays=None): + ''' + A basic internal calendar is made, storing the days and months of the + depicted simulating year. + - Monday == 0 ... Sunday == 6 + ''' + # first we determine the first week of the depicted year + fdoy = datetime.datetime(year,1,1).weekday() + fweek = range(7)[fdoy:] + # whereafter we fill the complete year + if ndays: + nday = ndays + else: + nday = 366 if calendar.isleap(year) else 365 + + day_of_week = (fweek+53*range(7))[:nday] + # and return the day_of_week for the entire year + self.dow = day_of_week + self.nday = nday + return None + + def __occupancy__(self, min_form = True, min_time = False): + ''' + Simulation of a number of days based on cluster 'BxDict'. + - Including weekend days, + - starting from a regular monday at 4:00 AM. + ''' + def check(occday, min_form = True, min_time = False): + ''' + We set a check which becomes True if the simulated day behaves + according to the cluster, as a safety measure for impossible + solutions. + ''' + + # script 1 ######################################################## + # First we check if the simulated occ-chain has the same shape + shape = True + if min_form: + location = np.zeros(1, dtype=int) + reduction = occday[0]*np.ones(1, dtype=int) + for i in range(len(occday)-1): + if occday[i+1] != occday[i]: + location = np.append(location, i+1) + reduction = np.append(reduction,occday[i+1]) +# shape = np.array_equal(reduction, RED) + + # script 2 ######################################################## + # And second we see if the chain has nu sub-30 min differences + length = True + if min_time: + minlength = 99 + for i in location: + j = 0 + while occday[i+j] == occday[i] and i+j < len(occday)-1: + j = j+1 + if j < minlength: + minlength = j + # and we neglect the very short presences of 20 min or less + length = not minlength < 3 + + # output ########################################################## + # both have to be true to allow continuation, and we return boolean + return shape and length + + def dayrun(start, cluster): + ''' + Simulation of a single day according to start state 'start' + and the stochastics stored in cluster 'BxDict'and daytype 'Bx'. + ''' + + # script ########################################################## + # we set the default daycheck at False for the first run and loop + # creating days while False, meaning while the simulated day does + # not correspond to the agreed-on rules in check(). + daycheck = False + end = datetime.datetime.utcnow() + datetime.timedelta(seconds = 10) + # defin the corresponding MCSA object from stats.py depicting a + # Monte Carlo Survival Analysis object. + SA = stats.MCSA(cluster) + # and then keep simulating a day until True + while daycheck == False: + # set start state conditions + tbin = 0 + occs = np.zeros(144, dtype=int) + occs[0] = start + t48 = np.array(sorted(list(range(1, 49)) * 3)) + dt = SA.duration(start, t48[0]) + # and loop sequentially transition and duration functions + while tbin < 143: + tbin += 1 + if dt == 0: + occs[tbin] = SA.transition(occs[tbin-1], t48[tbin]) + dt = SA.duration(occs[tbin], t48[tbin]) - 1 + # -1 is necessary, as the occupancy state already started + else: + occs[tbin] = occs[tbin - 1] + dt += -1 + # whereafer we control if this day is ok + daycheck = check(occs) + # and we include a break if the while-loop takes to long until + # check()-conditions are fulfilled. + if datetime.datetime.utcnow() > end: + break + + # ouput ########################################################### + # return occupants array if daycheck is ok according to Bx + return occs + + def merge(occ): + ''' + Merge the occupancy profiles of all household members to a single + profile denoting the most active state of all members. + ''' + # scirpt ########################################################## + # We start defining an array of correct length filled with the + # least active state and loop to see if more-active people are + # present at the depicted moment. + occs = int(3)*np.ones(len(occ[0])) + for member in occ: + for to in range(len(member)): + if member[to] < occs[to]: + occs[to] = member[to] + + # ouput ########################################################### + # return the merge occupancy states + return occs + + # script ############################################################## + # We change the directory to to location where the data is stored, + # and run the three type of days, ie. wkdy, sat and son succesively + # by which we can create a typical week. + cdir = os.getcwd() + occ_week = [] + for member in self.clusters: + startstate = 2 #4.00 AM + wkdy = dayrun(startstate, member['wkdy']) + sat = dayrun(wkdy[-1], member['sat']) + son = dayrun(sat[-1], member['son']) + # and concatenate + week = np.concatenate((np.tile(wkdy, 5), sat, son)) + occ_week.append(week) + # A merge occupancy is created depicted the most active state of all + # household members, later-on used for set-point temperatures. + occ_merg = merge(occ_week) + # and combine the weekly occupancy states for the entire year by + # repeating them every week and correcting for the first day of year, + # including for the merged occupancy. + bins = 144 + tstart = bins*self.dow[0] + tstop = tstart + bins*self.nday + occ_year = [] + for line in range(len(occ_week)): + occ_year.append(np.tile(occ_week,54)[line][tstart:tstop]) + occ_merged = [] + occ_merged.append(np.tile(occ_merg,54)[tstart:tstop]) + + # output ############################################################## + # chdir back to original and return the occupancy states to the class + # object. + os.chdir(cdir) + self.occ = occ_year + self.occ_m = occ_merged + # and print statements + presence = [to for to in self.occ_m[0] if to < 2] + hours = len(presence)/6. + print (' - Total presence time is {0:.1f} out of {1} hours'.format(hours, self.nday*24)) + print ('\tbeing {:.1f} percent)'.format(hours*100/(self.nday*24))) + return None + + def __plugload__(self): + ''' + Simulation of the electric load based on the occupancies, cluster + and the present appliances. + - Including weekend days, + - starting from a regular monday at 4:00 AM. + ''' + + def receptacles(self): + ''' + Simulation of the receptacle loads. + ''' + + # define number of minutes + nmin = self.nday * 1440 + # determine all transitions of the appliances depending on the appliance + # basic properties, ie. stochastic versus cycling power profile + power = np.zeros(nmin+1) + radi = np.zeros(nmin+1) + conv = np.zeros(nmin+1) + nday = self.nday + dow = self.dow + result_n = dict() + counter = range(len(self.clusters)) + for app in self.apps: + # create the equipment object with data from dataset.py + eq = Equipment(**set_appliances[app]) + r_app = dict() + n_app = 0 + # loop for all household mmembers + print (app) + for i in counter: + # if appliances are not shared the load of using the appliance can be summed for each householdmember + if app in ('CordlessPhone','PC'): # it is assumed that only the cordlessphone and PC are not shared. Each person in the household (>12) has its own phone or PC. + r_appi, n_appi = eq.simulate(nday, dow, self.clusters[i], self.occ[i]) + r_app = stats.sum_dict(r_app, r_appi) + n_app += n_appi + else: + # if appliances are shared the load of using the appliance can be maximal the cycle load. + # if the appliance is an appliance that is working continously the load is simulated for only 1 person + if app in ('FridgeFreezer', 'Refrigerator', 'ChestFreezer', 'UprightFreezer', 'Clock'): + if i == 0: + r_appi, n_appi = eq.simulate(nday, dow, self.clusters[i], self.occ[i]) + r_app = stats.sum_dict(r_app, r_appi) + n_app += n_appi + # if the appliance is not continously used, but sometimes shared, the cycle load is assigned when minimal one person is using it, when nobody is using it the stanby load is assigned + else: + r_appi, n_appi = eq.simulate(nday, dow, self.clusters[i], self.occ[i]) + r_app = stats.sum_dict2(r_app, r_appi) + n_app += n_appi + # and sum + result_n.update({app:n_app}) + power += r_app['P'] + radi += r_app['QRad'] + conv += r_app['QCon'] + # a new time axis for power output is to be created as a different + # time step is used in comparison to occupancy + time = 4*60*60 + np.arange(0, (nmin+1)*60, 60) + + react = np.zeros(nmin+1) + + result = {'time':time, 'occ':None, 'P':power, 'Q':react, + 'QRad':radi, 'QCon':conv, 'Wknds':None, 'mDHW':None} + + self.r_receptacles = result + self.n_receptacles = result_n + + # output ########################################################## + # only the power load is returned + load = int(np.sum(result['P'])/60/1000) + print (' - Receptacle load is %s kWh' % str(load)) + + return None + + def lightingload(self): + ''' + Simulate use of lighting for residential buildings based on the + model of J. Widén et al (2009) + ''' + + # parameters ###################################################### + # Simulation of lighting load requires information on irradiance + # levels which determine the need for lighting if occupant. + # The loaded solar data represent the global horizontal radiation + # at a time-step of 1-minute for Uccle, Belgium + # Since the data starts at midnight, a shift to 4am is necessary + # so that it coincides with the occupancy data!!! + # the first 4 h are moved to the end. + os.chdir(r'../Data') + file = open('Climate/irradiance.txt','r') + data_pickle = file.read() + file.close() + irr = cPickle.loads(data_pickle) + irr = np.roll(irr,-240) # brings first 4h to end + # script ########################################################## + # a yearly simulation is basic, also in a unittest + nday = self.nday + nbin = 144 + minutes = self.nday * 1440 + occ_m = self.occ_m[0] + # the model is found on an ideal power level power_id depending on + # irradiance level and occupancy (but not on light switching + # behavior of occupants itself) + time = 4*60*60 + np.arange(0, (minutes+1)*60, 60) + to = -1 # time counter for occupancy + tl = -1 # time counter for minutes in lighting load + power_max = 200 + irr_max = 200 + pow_id = np.zeros(minutes+1) + prob_adj = 0.1 # hourly probability to adjust + pow_adj = 40 # power by which is adjusted + P = np.zeros(minutes+1) + Q = np.zeros(minutes+1) + for doy, step in itertools.product(range(nday), range(nbin)): + to += 1 + for run in range(0, 10): + tl += 1 + if occ_m[to] > int(1) or (irr[tl] >= irr_max): + pow_id[tl] = 0 + else: + pow_id[tl] = power_max*(1 - irr[tl]/irr_max) + # determine all transitions of appliances depending on + # the appliance basic properties, ie. stochastic versus + # cycling power profile + if occ_m[to] > int(1): + P[tl] = pow_id[tl] + elif random.random() <= prob_adj: + delta = P[tl-1] - pow_id[tl] + delta_min = np.abs(delta - pow_adj) + delta_plus = np.abs(delta + pow_adj) + if delta > 0 and delta_min < np.abs(delta) : + P[tl] = P[tl-1]-pow_adj + elif delta < 0 and delta_plus < np.abs(delta): + P[tl] = P[tl-1]+pow_adj + else: + P[tl] = P[tl-1] + else: + P[tl] = P[tl-1] + + radi = P*0.55 + conv = P*0.45 + + result = {'time':time, 'P':P, 'Q':Q, 'QRad':radi, 'QCon':conv} + + self.r_lighting = result + # output ########################################################## + # only the power load is returned + load = int(np.sum(result['P'])/60/1000) + print (' - Lighting load is %s kWh' % str(load)) + + return None + + receptacles(self) + lightingload(self) + + return None + + def __dhwload__(self): + ''' + Simulate use of domestic hot water based on Jordan & Vajen (2001) and + J.Widen (2009). + ''' + + # define number of minutes + nmin = self.nday * 1440 + # determine all transitions of the appliances depending on the appliance + # basic properties, ie. stochastic versus cycling power profile + flow = np.zeros(nmin+1) + clusterDict = self.clusters[0] + nday = self.nday + dow = self.dow + occ_m = self.occ_m[0] + result_n = dict() + for tap in self.taps: + # create the equipment object with data from dataset.py + eq = Equipment(**set_appliances[tap]) + r_tap, n_tap = eq.simulate(nday, dow, clusterDict, occ_m) + result_n.update({tap:n_tap}) + flow += r_tap['mDHW'] + # a new time axis for power output is to be created as a different + # time step is used in comparison to occupancy + time = 4*60*60 + np.arange(0, (nmin+1)*60, 60) + + result = {'time':time, 'occ':None, 'P':None, 'Q':None, + 'QRad':None, 'QCon':None, 'Wknds':None, 'mDHW':flow} + + self.r_flows = result + self.n_flows = result_n + + + # output ########################################################## + # only the power load is returned + load = np.sum(result['mDHW']) + loadpppd = int(load/self.nday/len(self.clusters)) + print (' - Draw-off is %s l/pp.day' % str(loadpppd)) + + return None + + def __shsetting__(self): + ''' + Simulation of the space heating setting points. + - Including weekend days, + - starting from a regular monday at 4:00 AM. + ''' + + ####################################################################### + # we define setting types based on their setpoint temperatures + # when being active (1), sleeping (2) or absent (3). + types = dict() + types.update({'2' : {1:18.5, 2:15.0, 3:18.5}}) + types.update({'3' : {1:20.0, 2:15.0, 3:19.5}}) + types.update({'4' : {1:20.0, 2:11.0, 3:19.5}}) + types.update({'5' : {1:20.0, 2:14.5, 3:15.0}}) + types.update({'6' : {1:21.0, 2:20.5, 3:21.0}}) + types.update({'7' : {1:21.5, 2:15.5, 3:21.5}}) + # and the probabilities these types occur based on Dutch research, + # i.e. Leidelmeijer and van Grieken (2005). + types.update({'prob' : [0.16, 0.35, 0.08, 0.11, 0.05, 0.20]}) + # and given a type, denote which rooms are heated + given = dict() + given.update({'2' : [['dayzone','bathroom']]}) + given.update({'3' : [['dayzone'],['dayzone','bathroom'],['dayzone','nightzone']]}) + given.update({'4' : [['dayzone'],['dayzone','nightzone']]}) + given.update({'5' : [['dayzone']]}) + given.update({'6' : [['dayzone','bathroom','nightzone']]}) + given.update({'7' : [['dayzone','bathroom']]}) + + ####################################################################### + # select a type from the given types and probabilities + rnd = np.random.random() + shtype = str(1 + stats.get_probability(rnd, types['prob'], 'prob')) + if len(given[shtype]) != 1: + nr = np.random.randint(np.shape(given[shtype])[0]) + shrooms = given[shtype][nr] + else: + shrooms = given[shtype][0] + + ####################################################################### + # create a profile for the heated rooms + shnon = 12*np.ones(len(self.occ_m[0])+1) + shset = np.hstack((self.occ_m[0],self.occ_m[0][-1])) + for key in types[shtype].keys(): + for i in range(len(shset)): + if int(shset[i]) == key: + shset[i] = types[shtype][key] + + ####################################################################### + # and couple to the heated rooms + sh_settings = dict() + for room in ['dayzone', 'nightzone', 'bathroom']: + if room in shrooms: + sh_settings.update({room:shset}) + else: + sh_settings.update({room:shnon}) + # and store + self.sh_settings = sh_settings + print (' - Average comfort setting is %s Celsius' % str(round(np.average(sh_settings['dayzone']),2))) + return None + + def roundUp(self): + ''' + Round the simulation by wrapping all data and reduce storage size. + ''' + + ####################################################################### + # first we move and sumarize data to the most upper level. + self.sh_day = self.sh_settings['dayzone'] + self.sh_night = self.sh_settings['nightzone'] + self.sh_bath = self.sh_settings['bathroom'] + self.P = self.r_receptacles['P'] + self.r_lighting['P'] + self.Q = self.r_receptacles['Q'] + self.r_lighting['Q'] + self.QRad = self.r_receptacles['QRad'] + self.r_lighting['QRad'] + self.QCon = self.r_receptacles['QCon'] + self.r_lighting['QCon'] + self.mDHW = self.r_flows['mDHW'] + + ####################################################################### + # bring last 4 h to the front so that data starts at midnight + self.sh_day = np.roll(self.sh_day,24) + self.sh_night = np.roll(self.sh_night,24) + self.sh_bath = np.roll(self.sh_bath,24) + self.P = np.roll(self.P,240) + self.Q = np.roll(self.Q,240) + self.QRad = np.roll(self.QRad,240) + self.QCon = np.roll(self.QCon,240) + self.mDHW = np.roll(self.mDHW,240) + + ####################################################################### + # then we delete the old data structure to save space + del self.sh_settings + del self.r_receptacles + del self.r_lighting + del self.r_flows + + ####################################################################### + # and end + return None + + def pickle(self): + ''' + Pickle the generated profile and its results for storing later. + ''' + cPickle.dump(self, open(self.name+'.p','wb')) + return None + +class Equipment(object): + ''' + Data records for appliance simulation based on generated activity and + occupancy profiles + ''' + # All object parameters are given in kwargs + def __init__(self, **kwargs): + # copy kwargs to object parameters + for (key, value) in kwargs.items(): + setattr(self, key, value) + + def simulate(self, nday, dow, cluster, occ): + + def stochastic_flow(self, nday, dow, clusterDict, occ): + ''' + Simulate non-cycling appliances based on occupancy and the model + and Markov state-space of Richardson et al. + ''' + # parameters ###################################################### + # First we check the required activity and load the respective + # stats.DTMC file with its data. + len_cycle = self.cycle_length + act = self.activity + if self.activity not in ('None','Presence'): + actdata = stats.DTMC(clusterDict=clusterDict) + else: + actdata = None + + # script ########################################################## + # a yearly simulation is basic, also in a unittest + nbin = 144 + minutes = nday * 1440 + to = -1 # time counter for occupancy + tl = -1 # time counter for load + left = -1 # time counter for appliance duration + n_fl = 0 + flow = np.zeros(minutes+1) + for doy, step in itertools.product(range(nday), range(nbin)): + dow_i = dow[doy] + to += 1 + for run in range(0, 10): + tl += 1 + # check if this appliance is already on + if left <= 0: + # determine possibilities + if self.activity == 'None': + prob = 1 + elif self.activity == 'Presence': + prob = 1 if occ[to] == 1 else 0 + else: + occs = 1 if occ[to] == 1 else 0 + prob = occs * actdata.get_var(dow_i, act, step) + # check if there is a statechange in the appliance + if random.random() < prob * self.cal: + n_fl += 1 + left = random.gauss(len_cycle, len_cycle/10) + flow[tl] += self.standby_flow + else: + left += -1 + flow[tl] += self.cycle_flow + + r_fl = {'time':time, 'occ':None, 'P':None, 'Q':None, 'QRad':None, + 'QCon':None, 'Wknds':None, 'mDHW':flow} + + return r_fl, n_fl + + def stochastic_load(self, nday, dow, clusterDict, occ): + ''' + Simulate non-cycling appliances based on occupancy and the model + and Markov state-space of Richardson et al. + ''' + + # parameters ###################################################### + # First we check the required activity and load the respective + # stats.DTMC file with its data. + len_cycle = self.cycle_length + act = self.activity + if self.activity not in ('None','Presence'): + actdata = stats.DTMC(clusterDict=clusterDict) + else: + actdata = None + + # script ########################################################## + # a yearly simulation is basic, also in a unittest + nbin = 144 + minutes = nday * 1440 + to = -1 # time counter for occupancy + tl = -1 # time counter for load + left = -1 # time counter for appliance duration + n_eq = 0 + P = np.zeros(minutes+1) + Q = np.zeros(minutes+1) + for doy, step in itertools.product(range(nday), range(nbin)): + dow_i = dow[doy] + to += 1 + for run in range(10): + tl += 1 + # check if this appliance is already on + if left <= 0: + # determine possibilities + if self.activity == 'None': + prob = 1 + elif self.activity == 'Presence': + prob = 1 if occ[to] == 1 else 0 + else: + occs = 1 if occ[to] == 1 else 0 + prob = occs * actdata.get_var(dow_i, act, step) + # check if there is a statechange in the appliance + if random.random() < prob * self.cal: + n_eq += 1 + left = random.gauss(len_cycle, len_cycle/10) + P[tl] += self.standby_power + else: + left += -1 + P[tl] += self.cycle_power + + r_eq = {'time':time, 'occ':None, 'P':P, 'Q':Q, 'QRad':P*self.frad, + 'QCon':P*self.fconv, 'Wknds':None, 'mDHW':None} + + return r_eq, n_eq + + def cycle_load(self, nday): + ''' + Simulate cycling appliances, eg. fridges and freezers based on + average clycle length and delay between cycles + ''' + + nbin = nday*24*60 + P = np.zeros(nbin+1) + Q = np.zeros(nbin+1) #currently no data included (remains zero) + n_eq = 0 # number of cycles for calibration of `cal` parameter of appliance + + # define length of cycles (same for entire year, assumed to depend on appliance) + len_cycle=random.gauss(self.cycle_length, self.cycle_length/10) + # define duration of break between cycles (same for entire year) + delay=random.gauss(self.delay, self.delay/4) + + # start as OFF (assumption) + on=False #is it ON? + left = delay # time left until change of state + + for tl in range(nbin+1): # loop over every minute of the year + # if there is time LEFT until change of state, remain as is + # if time is up, change state: + if left <= 0: + on=not on # switch to opposite state ON/OFF + if on: # if switched ON + n_eq += 1 # add one to cycle counter + left = len_cycle #start counting 1 cycle length + else: # if switched OFF + left = delay #start counting time until next cycle + # either way, count downt the time until next change of state + left += -1 + # allocate correct power, depending on current state ON/OFF + if on: + P[tl] = self.cycle_power # instead of the average consumption, could be sampled from normal distribution as well + else: + P[tl] = self.standby_power + + r_eq = {'time':time, 'occ':None, 'P':P, 'Q':Q, 'QRad':P*self.frad, + 'QCon':P*self.fconv, 'Wknds':None, 'mDHW':None} + + return r_eq, n_eq + + if self.type == 'appliance': + #check if the equipment is an appliance instead of tapping point + if self.delay == 0: + r_app, n_app = stochastic_load(self, nday, dow, cluster, occ) + else: + r_app, n_app = cycle_load(self, nday) + else: + r_app, n_app = stochastic_flow(self, nday, dow, cluster, occ) + + return r_app, n_app diff --git a/stats.py b/stats.py new file mode 100644 index 0000000..e2dbae3 --- /dev/null +++ b/stats.py @@ -0,0 +1,131 @@ +# -*- coding: utf-8 -*- +""" +Created on Wed Oct 09 11:57:02 2013 + +@author: Ruben Baetens +""" + +import numpy as np +import random + +import data + + +def get_probability(rnd, prob, p_type='cum'): + ''' + Find the x-value in a given comulative probability 'prob_cum' based on a + given random y-value 'rnd'. + ''' + if p_type != 'cum': + prob = np.cumsum(prob) + prob /= max(prob) + idx = 1 + while rnd >= prob[idx-1]: + idx += 1 + return idx + +def sum_dict(dict_a, dict_b): + ''' + Sum the values stored under the same keys in python dictionaries. + ''' + # loop through the keys and sum both values given + if len(dict_a.keys()) == 0: + sum_dict = dict_b + elif len(dict_b.keys()) == 0: + sum_dict = dict_a + else: + sum_dict = dict() + for key in dict_a.keys(): + if dict_a[key] is None: + sum_dict.update({key:None}) + elif key != 'time': + sum_dict.update({key:dict_a[key]+dict_b[key]}) + else: + sum_dict.update({key:dict_a[key]}) + # and return + return sum_dict + +def sum_dict2(dict_a, dict_b): + ''' + Sum the values stored under the same keys in python dictionaries. Specifically for summing loads of shared appliances. + This sumation allows the set a maximum to the total load. + When one or more persons use the appliance the load is assigned (max) and when nobody is using the appliance the stanby load is assigned. + ''' + # loop through the keys and sum both values given + if len(dict_a.keys()) == 0: + sum_dict2 = dict_b + elif len(dict_b.keys()) == 0: + sum_dict2 = dict_a + else: + sum_dict2 = dict() + for key in dict_a.keys(): + if dict_a[key] is None: + sum_dict2.update({key:None}) + elif key != 'time': + sum_dict2.update({key:np.maximum(dict_a[key],dict_b[key])})#instead of summing both loads, take the maximum. + else: + sum_dict2.update({key:dict_a[key]}) + # and return + return sum_dict2 + +class MCSA(object): + ''' + The MCSA class defines a Monte Carlo Survival Analysis + ''' + # All object parameters are given in kwargs + def __init__(self, cluster, **kwargs): + # load the dataset of the cluster into ds + ds = data.get_occDict(cluster) + # and add them as class parameters + self.OSS = ds['ss'] + self.OPM = {1:ds['os_1'], 2:ds['os_2'], 3:ds['os_3']} + self.ODM = {1:ds['ol_1'], 2:ds['ol_2'], 3:ds['ol_3']} +# self.RED = ds['RED'] + + def startstate(self): + ''' + Get the startstate for first simulation day at 4:00 AM. + ''' + # we define the startstate based on the given probability + probs = [self.OSS['1'], self.OSS['2'], self.OSS['3']] + state = get_probability(random.random(), probs) + # and retrun the value + return int(state) + + def transition(self, state, timebin): + ''' + Get next occupancy state from current state ending at time. + ''' + # we define the new state based on the given probability + probs = self.OPM[state][str(timebin)] + newoc = get_probability(random.random(), probs) + # and retrun the value + return int(newoc) + + def duration(self, state, timebin): + ''' + Get the duration of current state started at time. + ''' + # we define the new duration based on the given probability + probs = self.ODM[state][str(timebin)] + durat = get_probability(random.random(), probs) + # and retrun the value + return durat + + +class DTMC(object): + ''' + The DTMC class defines a Discrete-Time Markov Chain + ''' + # All object parameters are given in kwargs + def __init__(self, clusterDict, **kwargs): + # load the dataset of the cluster into ds + self.ds = dict() + for i in range(5): + self.ds.update({i:data.get_actDict(clusterDict['wkdy'])}) + self.ds.update({5:data.get_actDict(clusterDict['sat'])}) + self.ds.update({6:data.get_actDict(clusterDict['son'])}) + def get_var(self, dow, act, step): + # get the probability of the given activity for daytype dow at step + return self.ds[dow][act][step] + From addbcf49d3895152644c626062253b1274642453 Mon Sep 17 00:00:00 2001 From: Silke Verbruggen <40292967+SilkeVerbruggen@users.noreply.github.com> Date: Mon, 17 Feb 2020 17:03:37 +0100 Subject: [PATCH 2/8] Delete residential.py --- residential.py | 815 ------------------------------------------------- 1 file changed, 815 deletions(-) delete mode 100644 residential.py diff --git a/residential.py b/residential.py deleted file mode 100644 index 26dddb2..0000000 --- a/residential.py +++ /dev/null @@ -1,815 +0,0 @@ -# -*- coding: utf-8 -*- -""" -Created on Mon October 07 16:16:10 2013 - -@author: Ruben Baetens -""" - -import sys -import random -import numpy as np -import time -import datetime -import calendar -import ast -import os -import cPickle -import itertools - -import stats -import data - -sys.path.append("..") -from Data.Households import households -from Data.Appliances import set_appliances - -#changes for new cold-appliance fix ####################################### -# Based on 10000 runs, these new values combined with rule-based fix (see in def appliances) -# lead to the same overall ownership as the original values. -# We change it here so that the original remain in the Appliances file. -set_appliances['Refrigerator']['owner']=0.27 # original: 0.430 -set_appliances['FridgeFreezer']['owner']=0.40 # original: 0.651 -set_appliances['ChestFreezer']['owner']=0.19 # original: 0.163 -set_appliances['UprightFreezer']['owner']=0.31 # original: 0.291 - -#################################################################### - -class Household(object): - ''' - The Household class is the main class of ProclivityPy, defining the - household composition as input or randomized based on statistics and - allowing simulation of the household for building energy simulations. - - Main functions are: - - __init__(), which ... - - self.parameterize(), which ... - - self.simulate(), which ... - ''' - - def __init__(self, name, **kwargs): - ''' - Initiation of Household object. - ''' - # input ############################################################### - # check on correct parameter input for use of functions as name should - # be a string. - try: - if not isinstance(name, str): - raise ValueError('Given name %d is not a string' % str(name)) - except: - raise TypeError('give another name') - # first define the name of the household object - self.creation = time.asctime() - self.name = name - self.parameterize(**kwargs) - - def parameterize(self, **kwargs): - ''' - Get a household definition for occupants and present appliances based - on average statistics or the given kwargs. - ''' - - def members(**kwargs): - ''' - Define the employment type of all household members based on time - use survey statistics or the given kwargs. - ''' - members = [] - # First we check if membertypes are given as **kwargs - if kwargs.has_key('members'): - if isinstance(kwargs['members'], list): - members = kwargs['members'] - else: - raise TypeError('Given membertypes is no List of strings.') - # If no types are given, random statististics are applied - else: - key = random.randint(1, len(households)) - members = households[key] - # And return the members as list fo strings - return members - - def appliances(): - ''' - Define the pressent household appliances based on average national - statistics independent of household member composition. - ''' - # Loop through all appliances and pick randomly based on the - # rate of ownership. - app_n = [] - for app in set_appliances: - if set_appliances[app]['type'] == 'appliance': - obj = Equipment(**set_appliances[app]) - owner = obj.owner >= random.random() - app_n.append(app) if owner else None - - # Cold appliances fix: ############################################### - if not ('FridgeFreezer' in app_n) and not ('Refrigerator' in app_n): # if there was none of the two-> add one of the two. - # Find probability of household to own FF instead of R: (FF ownership over sum of two ownerships-> scale to 0-1 interval) - prob=set_appliances['FridgeFreezer']['owner']/(set_appliances['FridgeFreezer']['owner']+set_appliances['Refrigerator']['owner']) - # if random number is below prob, then the household will own a FF, otherwise a R -> add it - app_n.append('FridgeFreezer') if prob >= random.random() else app_n.append('Refrigerator') - - if 'FridgeFreezer' in app_n and 'ChestFreezer' in app_n and 'UprightFreezer' in app_n: #if there were 3 freezers-> remove a freezer-only - #find probability of household to own CF instead of UF: (CF ownership over sum of two ownerships-> scale to 0-1 interval) - prob=set_appliances['ChestFreezer']['owner']/(set_appliances['ChestFreezer']['owner']+set_appliances['UprightFreezer']['owner']) - # if random number is below prob, then the household will own a CF, otherwise an UF-> remove the other - app_n.remove('UprightFreezer') if prob >= random.random() else app_n.remove('ChestFreezer') #remove the one you don't own - - ######################################################################### - return app_n - - def tappings(): - ''' - Define the present household tapping types. - ''' - tap_n = ['shortFlow', 'mediumFlow', 'bathFlow', 'showerFlow'] - return tap_n - - def clusters(members): - ''' - Allocate each household member to the correct cluster based on the - members occupation in time use survey data. - ''' - clusters = [] - # loop for every individual in the household - for ind in members: - if ind != 'U12': - clu_i = data.get_clusters(ind) - clusters.append(clu_i) - # and return the list of clusters - return clusters - # and run all - self.members = members(**kwargs) - self.apps = appliances() - self.taps = tappings() - self.clusters = clusters(self.members) - # and return - print ('Household-object created and parameterized.') - print (' - Employment types are %s' % str(self.members)) - summary = [] #loop dics and remove dubbles - for member in self.clusters: - summary += member.values() - print (' - Set of clusters is %s' % str(list(set(summary)))) - - return None - - def simulate(self, year=2013, ndays=365): - ''' - The simulate function includes the simulation of the household - occupancies, plug loads, lighting loads and hot water tappings. - ''' - - self.year = year - self.__chronology__(year, ndays) - self.__occupancy__() - self.__plugload__() - self.__dhwload__() - self.__shsetting__() - - def __chronology__(self, year, ndays=None): - ''' - A basic internal calendar is made, storing the days and months of the - depicted simulating year. - - Monday == 0 ... Sunday == 6 - ''' - # first we determine the first week of the depicted year - fdoy = datetime.datetime(year,1,1).weekday() - fweek = range(7)[fdoy:] - # whereafter we fill the complete year - if ndays: - nday = ndays - else: - nday = 366 if calendar.isleap(year) else 365 - - day_of_week = (fweek+53*range(7))[:nday] - # and return the day_of_week for the entire year - self.dow = day_of_week - self.nday = nday - return None - - def __occupancy__(self, min_form = True, min_time = False): - ''' - Simulation of a number of days based on cluster 'BxDict'. - - Including weekend days, - - starting from a regular monday at 4:00 AM. - ''' - def check(occday, min_form = True, min_time = False): - ''' - We set a check which becomes True if the simulated day behaves - according to the cluster, as a safety measure for impossible - solutions. - ''' - - # script 1 ######################################################## - # First we check if the simulated occ-chain has the same shape - shape = True - if min_form: - location = np.zeros(1, dtype=int) - reduction = occday[0]*np.ones(1, dtype=int) - for i in range(len(occday)-1): - if occday[i+1] != occday[i]: - location = np.append(location, i+1) - reduction = np.append(reduction,occday[i+1]) -# shape = np.array_equal(reduction, RED) - - # script 2 ######################################################## - # And second we see if the chain has nu sub-30 min differences - length = True - if min_time: - minlength = 99 - for i in location: - j = 0 - while occday[i+j] == occday[i] and i+j < len(occday)-1: - j = j+1 - if j < minlength: - minlength = j - # and we neglect the very short presences of 20 min or less - length = not minlength < 3 - - # output ########################################################## - # both have to be true to allow continuation, and we return boolean - return shape and length - - def dayrun(start, cluster): - ''' - Simulation of a single day according to start state 'start' - and the stochastics stored in cluster 'BxDict'and daytype 'Bx'. - ''' - - # script ########################################################## - # we set the default daycheck at False for the first run and loop - # creating days while False, meaning while the simulated day does - # not correspond to the agreed-on rules in check(). - daycheck = False - end = datetime.datetime.utcnow() + datetime.timedelta(seconds = 10) - # defin the corresponding MCSA object from stats.py depicting a - # Monte Carlo Survival Analysis object. - SA = stats.MCSA(cluster) - # and then keep simulating a day until True - while daycheck == False: - # set start state conditions - tbin = 0 - occs = np.zeros(144, dtype=int) - occs[0] = start - t48 = np.array(sorted(list(range(1, 49)) * 3)) - dt = SA.duration(start, t48[0]) - # and loop sequentially transition and duration functions - while tbin < 143: - tbin += 1 - if dt == 0: - occs[tbin] = SA.transition(occs[tbin-1], t48[tbin]) - dt = SA.duration(occs[tbin], t48[tbin]) - 1 - # -1 is necessary, as the occupancy state already started - else: - occs[tbin] = occs[tbin - 1] - dt += -1 - # whereafer we control if this day is ok - daycheck = check(occs) - # and we include a break if the while-loop takes to long until - # check()-conditions are fulfilled. - if datetime.datetime.utcnow() > end: - break - - # ouput ########################################################### - # return occupants array if daycheck is ok according to Bx - return occs - - def merge(occ): - ''' - Merge the occupancy profiles of all household members to a single - profile denoting the most active state of all members. - ''' - # scirpt ########################################################## - # We start defining an array of correct length filled with the - # least active state and loop to see if more-active people are - # present at the depicted moment. - occs = int(3)*np.ones(len(occ[0])) - for member in occ: - for to in range(len(member)): - if member[to] < occs[to]: - occs[to] = member[to] - - # ouput ########################################################### - # return the merge occupancy states - return occs - - # script ############################################################## - # We change the directory to to location where the data is stored, - # and run the three type of days, ie. wkdy, sat and son succesively - # by which we can create a typical week. - cdir = os.getcwd() - occ_week = [] - for member in self.clusters: - startstate = 2 #4.00 AM - wkdy = dayrun(startstate, member['wkdy']) - sat = dayrun(wkdy[-1], member['sat']) - son = dayrun(sat[-1], member['son']) - # and concatenate - week = np.concatenate((np.tile(wkdy, 5), sat, son)) - occ_week.append(week) - # A merge occupancy is created depicted the most active state of all - # household members, later-on used for set-point temperatures. - occ_merg = merge(occ_week) - # and combine the weekly occupancy states for the entire year by - # repeating them every week and correcting for the first day of year, - # including for the merged occupancy. - bins = 144 - tstart = bins*self.dow[0] - tstop = tstart + bins*self.nday - occ_year = [] - for line in range(len(occ_week)): - occ_year.append(np.tile(occ_week,54)[line][tstart:tstop]) - occ_merged = [] - occ_merged.append(np.tile(occ_merg,54)[tstart:tstop]) - - # output ############################################################## - # chdir back to original and return the occupancy states to the class - # object. - os.chdir(cdir) - self.occ = occ_year - self.occ_m = occ_merged - # and print statements - presence = [to for to in self.occ_m[0] if to < 2] - hours = len(presence)/6. - print (' - Total presence time is {0:.1f} out of {1} hours'.format(hours, self.nday*24)) - print ('\tbeing {:.1f} percent)'.format(hours*100/(self.nday*24))) - return None - - def __plugload__(self): - ''' - Simulation of the electric load based on the occupancies, cluster - and the present appliances. - - Including weekend days, - - starting from a regular monday at 4:00 AM. - ''' - - def receptacles(self): - ''' - Simulation of the receptacle loads. - ''' - - # define number of minutes - nmin = self.nday * 1440 - # determine all transitions of the appliances depending on the appliance - # basic properties, ie. stochastic versus cycling power profile - power = np.zeros(nmin+1) - radi = np.zeros(nmin+1) - conv = np.zeros(nmin+1) - nday = self.nday - dow = self.dow - result_n = dict() - counter = range(len(self.clusters)) - for app in self.apps: - # create the equipment object with data from dataset.py - eq = Equipment(**set_appliances[app]) - r_app = dict() - n_app = 0 - # loop for all household mmembers - print (app) - for i in counter: - # if appliances are not shared the load of using the appliance can be summed for each householdmember - if app in ('CordlessPhone','PC'): # it is assumed that only the cordlessphone and PC are not shared. Each person in the household (>12) has its own phone or PC. - r_appi, n_appi = eq.simulate(nday, dow, self.clusters[i], self.occ[i]) - r_app = stats.sum_dict(r_app, r_appi) - n_app += n_appi - else: - # if appliances are shared the load of using the appliance can be maximal the cycle load. - # if the appliance is an appliance that is working continously the load is simulated for only 1 person - if app in ('FridgeFreezer', 'Refrigerator', 'ChestFreezer', 'UprightFreezer', 'Clock'): - if i == 0: - r_appi, n_appi = eq.simulate(nday, dow, self.clusters[i], self.occ[i]) - r_app = stats.sum_dict(r_app, r_appi) - n_app += n_appi - # if the appliance is not continously used, but sometimes shared, the cycle load is assigned when minimal one person is using it, when nobody is using it the stanby load is assigned - else: - r_appi, n_appi = eq.simulate(nday, dow, self.clusters[i], self.occ[i]) - r_app = stats.sum_dict2(r_app, r_appi) - n_app += n_appi - # and sum - result_n.update({app:n_app}) - power += r_app['P'] - radi += r_app['QRad'] - conv += r_app['QCon'] - # a new time axis for power output is to be created as a different - # time step is used in comparison to occupancy - time = 4*60*60 + np.arange(0, (nmin+1)*60, 60) - - react = np.zeros(nmin+1) - - result = {'time':time, 'occ':None, 'P':power, 'Q':react, - 'QRad':radi, 'QCon':conv, 'Wknds':None, 'mDHW':None} - - self.r_receptacles = result - self.n_receptacles = result_n - - # output ########################################################## - # only the power load is returned - load = int(np.sum(result['P'])/60/1000) - print (' - Receptacle load is %s kWh' % str(load)) - - return None - - def lightingload(self): - ''' - Simulate use of lighting for residential buildings based on the - model of J. Widén et al (2009) - ''' - - # parameters ###################################################### - # Simulation of lighting load requires information on irradiance - # levels which determine the need for lighting if occupant. - # The loaded solar data represent the global horizontal radiation - # at a time-step of 1-minute for Uccle, Belgium - # Since the data starts at midnight, a shift to 4am is necessary - # so that it coincides with the occupancy data!!! - # the first 4 h are moved to the end. - os.chdir(r'../Data') - file = open('Climate/irradiance.txt','r') - data_pickle = file.read() - file.close() - irr = cPickle.loads(data_pickle) - irr = np.roll(irr,-240) # brings first 4h to end - # script ########################################################## - # a yearly simulation is basic, also in a unittest - nday = self.nday - nbin = 144 - minutes = self.nday * 1440 - occ_m = self.occ_m[0] - # the model is found on an ideal power level power_id depending on - # irradiance level and occupancy (but not on light switching - # behavior of occupants itself) - time = 4*60*60 + np.arange(0, (minutes+1)*60, 60) - to = -1 # time counter for occupancy - tl = -1 # time counter for minutes in lighting load - power_max = 200 - irr_max = 200 - pow_id = np.zeros(minutes+1) - prob_adj = 0.1 # hourly probability to adjust - pow_adj = 40 # power by which is adjusted - P = np.zeros(minutes+1) - Q = np.zeros(minutes+1) - for doy, step in itertools.product(range(nday), range(nbin)): - to += 1 - for run in range(0, 10): - tl += 1 - if occ_m[to] > int(1) or (irr[tl] >= irr_max): - pow_id[tl] = 0 - else: - pow_id[tl] = power_max*(1 - irr[tl]/irr_max) - # determine all transitions of appliances depending on - # the appliance basic properties, ie. stochastic versus - # cycling power profile - if occ_m[to] > int(1): - P[tl] = pow_id[tl] - elif random.random() <= prob_adj: - delta = P[tl-1] - pow_id[tl] - delta_min = np.abs(delta - pow_adj) - delta_plus = np.abs(delta + pow_adj) - if delta > 0 and delta_min < np.abs(delta) : - P[tl] = P[tl-1]-pow_adj - elif delta < 0 and delta_plus < np.abs(delta): - P[tl] = P[tl-1]+pow_adj - else: - P[tl] = P[tl-1] - else: - P[tl] = P[tl-1] - - radi = P*0.55 - conv = P*0.45 - - result = {'time':time, 'P':P, 'Q':Q, 'QRad':radi, 'QCon':conv} - - self.r_lighting = result - # output ########################################################## - # only the power load is returned - load = int(np.sum(result['P'])/60/1000) - print (' - Lighting load is %s kWh' % str(load)) - - return None - - receptacles(self) - lightingload(self) - - return None - - def __dhwload__(self): - ''' - Simulate use of domestic hot water based on Jordan & Vajen (2001) and - J.Widen (2009). - ''' - - # define number of minutes - nmin = self.nday * 1440 - # determine all transitions of the appliances depending on the appliance - # basic properties, ie. stochastic versus cycling power profile - flow = np.zeros(nmin+1) - clusterDict = self.clusters[0] - nday = self.nday - dow = self.dow - occ_m = self.occ_m[0] - result_n = dict() - for tap in self.taps: - # create the equipment object with data from dataset.py - eq = Equipment(**set_appliances[tap]) - r_tap, n_tap = eq.simulate(nday, dow, clusterDict, occ_m) - result_n.update({tap:n_tap}) - flow += r_tap['mDHW'] - # a new time axis for power output is to be created as a different - # time step is used in comparison to occupancy - time = 4*60*60 + np.arange(0, (nmin+1)*60, 60) - - result = {'time':time, 'occ':None, 'P':None, 'Q':None, - 'QRad':None, 'QCon':None, 'Wknds':None, 'mDHW':flow} - - self.r_flows = result - self.n_flows = result_n - - - # output ########################################################## - # only the power load is returned - load = np.sum(result['mDHW']) - loadpppd = int(load/self.nday/len(self.clusters)) - print (' - Draw-off is %s l/pp.day' % str(loadpppd)) - - return None - - def __shsetting__(self): - ''' - Simulation of the space heating setting points. - - Including weekend days, - - starting from a regular monday at 4:00 AM. - ''' - - ####################################################################### - # we define setting types based on their setpoint temperatures - # when being active (1), sleeping (2) or absent (3). - types = dict() - types.update({'2' : {1:18.5, 2:15.0, 3:18.5}}) - types.update({'3' : {1:20.0, 2:15.0, 3:19.5}}) - types.update({'4' : {1:20.0, 2:11.0, 3:19.5}}) - types.update({'5' : {1:20.0, 2:14.5, 3:15.0}}) - types.update({'6' : {1:21.0, 2:20.5, 3:21.0}}) - types.update({'7' : {1:21.5, 2:15.5, 3:21.5}}) - # and the probabilities these types occur based on Dutch research, - # i.e. Leidelmeijer and van Grieken (2005). - types.update({'prob' : [0.16, 0.35, 0.08, 0.11, 0.05, 0.20]}) - # and given a type, denote which rooms are heated - given = dict() - given.update({'2' : [['dayzone','bathroom']]}) - given.update({'3' : [['dayzone'],['dayzone','bathroom'],['dayzone','nightzone']]}) - given.update({'4' : [['dayzone'],['dayzone','nightzone']]}) - given.update({'5' : [['dayzone']]}) - given.update({'6' : [['dayzone','bathroom','nightzone']]}) - given.update({'7' : [['dayzone','bathroom']]}) - - ####################################################################### - # select a type from the given types and probabilities - rnd = np.random.random() - shtype = str(1 + stats.get_probability(rnd, types['prob'], 'prob')) - if len(given[shtype]) != 1: - nr = np.random.randint(np.shape(given[shtype])[0]) - shrooms = given[shtype][nr] - else: - shrooms = given[shtype][0] - - ####################################################################### - # create a profile for the heated rooms - shnon = 12*np.ones(len(self.occ_m[0])+1) - shset = np.hstack((self.occ_m[0],self.occ_m[0][-1])) - for key in types[shtype].keys(): - for i in range(len(shset)): - if int(shset[i]) == key: - shset[i] = types[shtype][key] - - ####################################################################### - # and couple to the heated rooms - sh_settings = dict() - for room in ['dayzone', 'nightzone', 'bathroom']: - if room in shrooms: - sh_settings.update({room:shset}) - else: - sh_settings.update({room:shnon}) - # and store - self.sh_settings = sh_settings - print (' - Average comfort setting is %s Celsius' % str(round(np.average(sh_settings['dayzone']),2))) - return None - - def roundUp(self): - ''' - Round the simulation by wrapping all data and reduce storage size. - ''' - - ####################################################################### - # first we move and sumarize data to the most upper level. - self.sh_day = self.sh_settings['dayzone'] - self.sh_night = self.sh_settings['nightzone'] - self.sh_bath = self.sh_settings['bathroom'] - self.P = self.r_receptacles['P'] + self.r_lighting['P'] - self.Q = self.r_receptacles['Q'] + self.r_lighting['Q'] - self.QRad = self.r_receptacles['QRad'] + self.r_lighting['QRad'] - self.QCon = self.r_receptacles['QCon'] + self.r_lighting['QCon'] - self.mDHW = self.r_flows['mDHW'] - - ####################################################################### - # bring last 4 h to the front so that data starts at midnight - self.sh_day = np.roll(self.sh_day,24) - self.sh_night = np.roll(self.sh_night,24) - self.sh_bath = np.roll(self.sh_bath,24) - self.P = np.roll(self.P,240) - self.Q = np.roll(self.Q,240) - self.QRad = np.roll(self.QRad,240) - self.QCon = np.roll(self.QCon,240) - self.mDHW = np.roll(self.mDHW,240) - - ####################################################################### - # then we delete the old data structure to save space - del self.sh_settings - del self.r_receptacles - del self.r_lighting - del self.r_flows - - ####################################################################### - # and end - return None - - def pickle(self): - ''' - Pickle the generated profile and its results for storing later. - ''' - cPickle.dump(self, open(self.name+'.p','wb')) - return None - -class Equipment(object): - ''' - Data records for appliance simulation based on generated activity and - occupancy profiles - ''' - # All object parameters are given in kwargs - def __init__(self, **kwargs): - # copy kwargs to object parameters - for (key, value) in kwargs.items(): - setattr(self, key, value) - - def simulate(self, nday, dow, cluster, occ): - - def stochastic_flow(self, nday, dow, clusterDict, occ): - ''' - Simulate non-cycling appliances based on occupancy and the model - and Markov state-space of Richardson et al. - ''' - # parameters ###################################################### - # First we check the required activity and load the respective - # stats.DTMC file with its data. - len_cycle = self.cycle_length - act = self.activity - if self.activity not in ('None','Presence'): - actdata = stats.DTMC(clusterDict=clusterDict) - else: - actdata = None - - # script ########################################################## - # a yearly simulation is basic, also in a unittest - nbin = 144 - minutes = nday * 1440 - to = -1 # time counter for occupancy - tl = -1 # time counter for load - left = -1 # time counter for appliance duration - n_fl = 0 - flow = np.zeros(minutes+1) - for doy, step in itertools.product(range(nday), range(nbin)): - dow_i = dow[doy] - to += 1 - for run in range(0, 10): - tl += 1 - # check if this appliance is already on - if left <= 0: - # determine possibilities - if self.activity == 'None': - prob = 1 - elif self.activity == 'Presence': - prob = 1 if occ[to] == 1 else 0 - else: - occs = 1 if occ[to] == 1 else 0 - prob = occs * actdata.get_var(dow_i, act, step) - # check if there is a statechange in the appliance - if random.random() < prob * self.cal: - n_fl += 1 - left = random.gauss(len_cycle, len_cycle/10) - flow[tl] += self.standby_flow - else: - left += -1 - flow[tl] += self.cycle_flow - - r_fl = {'time':time, 'occ':None, 'P':None, 'Q':None, 'QRad':None, - 'QCon':None, 'Wknds':None, 'mDHW':flow} - - return r_fl, n_fl - - def stochastic_load(self, nday, dow, clusterDict, occ): - ''' - Simulate non-cycling appliances based on occupancy and the model - and Markov state-space of Richardson et al. - ''' - - # parameters ###################################################### - # First we check the required activity and load the respective - # stats.DTMC file with its data. - len_cycle = self.cycle_length - act = self.activity - if self.activity not in ('None','Presence'): - actdata = stats.DTMC(clusterDict=clusterDict) - else: - actdata = None - - # script ########################################################## - # a yearly simulation is basic, also in a unittest - nbin = 144 - minutes = nday * 1440 - to = -1 # time counter for occupancy - tl = -1 # time counter for load - left = -1 # time counter for appliance duration - n_eq = 0 - P = np.zeros(minutes+1) - Q = np.zeros(minutes+1) - for doy, step in itertools.product(range(nday), range(nbin)): - dow_i = dow[doy] - to += 1 - for run in range(10): - tl += 1 - # check if this appliance is already on - if left <= 0: - # determine possibilities - if self.activity == 'None': - prob = 1 - elif self.activity == 'Presence': - prob = 1 if occ[to] == 1 else 0 - else: - occs = 1 if occ[to] == 1 else 0 - prob = occs * actdata.get_var(dow_i, act, step) - # check if there is a statechange in the appliance - if random.random() < prob * self.cal: - n_eq += 1 - left = random.gauss(len_cycle, len_cycle/10) - P[tl] += self.standby_power - else: - left += -1 - P[tl] += self.cycle_power - - r_eq = {'time':time, 'occ':None, 'P':P, 'Q':Q, 'QRad':P*self.frad, - 'QCon':P*self.fconv, 'Wknds':None, 'mDHW':None} - - return r_eq, n_eq - - def cycle_load(self, nday): - ''' - Simulate cycling appliances, eg. fridges and freezers based on - average clycle length and delay between cycles - ''' - - nbin = nday*24*60 - P = np.zeros(nbin+1) - Q = np.zeros(nbin+1) #currently no data included (remains zero) - n_eq = 0 # number of cycles for calibration of `cal` parameter of appliance - - # define length of cycles (same for entire year, assumed to depend on appliance) - len_cycle=random.gauss(self.cycle_length, self.cycle_length/10) - # define duration of break between cycles (same for entire year) - delay=random.gauss(self.delay, self.delay/4) - - # start as OFF (assumption) - on=False #is it ON? - left = delay # time left until change of state - - for tl in range(nbin+1): # loop over every minute of the year - # if there is time LEFT until change of state, remain as is - # if time is up, change state: - if left <= 0: - on=not on # switch to opposite state ON/OFF - if on: # if switched ON - n_eq += 1 # add one to cycle counter - left = len_cycle #start counting 1 cycle length - else: # if switched OFF - left = delay #start counting time until next cycle - # either way, count downt the time until next change of state - left += -1 - # allocate correct power, depending on current state ON/OFF - if on: - P[tl] = self.cycle_power # instead of the average consumption, could be sampled from normal distribution as well - else: - P[tl] = self.standby_power - - r_eq = {'time':time, 'occ':None, 'P':P, 'Q':Q, 'QRad':P*self.frad, - 'QCon':P*self.fconv, 'Wknds':None, 'mDHW':None} - - return r_eq, n_eq - - if self.type == 'appliance': - #check if the equipment is an appliance instead of tapping point - if self.delay == 0: - r_app, n_app = stochastic_load(self, nday, dow, cluster, occ) - else: - r_app, n_app = cycle_load(self, nday) - else: - r_app, n_app = stochastic_flow(self, nday, dow, cluster, occ) - - return r_app, n_app From adc56ec4a492e19695a02db08bd3ef720fc172de Mon Sep 17 00:00:00 2001 From: Silke Verbruggen <40292967+SilkeVerbruggen@users.noreply.github.com> Date: Mon, 17 Feb 2020 17:03:49 +0100 Subject: [PATCH 3/8] Delete stats.py --- stats.py | 131 ------------------------------------------------------- 1 file changed, 131 deletions(-) delete mode 100644 stats.py diff --git a/stats.py b/stats.py deleted file mode 100644 index e2dbae3..0000000 --- a/stats.py +++ /dev/null @@ -1,131 +0,0 @@ -# -*- coding: utf-8 -*- -""" -Created on Wed Oct 09 11:57:02 2013 - -@author: Ruben Baetens -""" - -import numpy as np -import random - -import data - - -def get_probability(rnd, prob, p_type='cum'): - ''' - Find the x-value in a given comulative probability 'prob_cum' based on a - given random y-value 'rnd'. - ''' - if p_type != 'cum': - prob = np.cumsum(prob) - prob /= max(prob) - idx = 1 - while rnd >= prob[idx-1]: - idx += 1 - return idx - -def sum_dict(dict_a, dict_b): - ''' - Sum the values stored under the same keys in python dictionaries. - ''' - # loop through the keys and sum both values given - if len(dict_a.keys()) == 0: - sum_dict = dict_b - elif len(dict_b.keys()) == 0: - sum_dict = dict_a - else: - sum_dict = dict() - for key in dict_a.keys(): - if dict_a[key] is None: - sum_dict.update({key:None}) - elif key != 'time': - sum_dict.update({key:dict_a[key]+dict_b[key]}) - else: - sum_dict.update({key:dict_a[key]}) - # and return - return sum_dict - -def sum_dict2(dict_a, dict_b): - ''' - Sum the values stored under the same keys in python dictionaries. Specifically for summing loads of shared appliances. - This sumation allows the set a maximum to the total load. - When one or more persons use the appliance the load is assigned (max) and when nobody is using the appliance the stanby load is assigned. - ''' - # loop through the keys and sum both values given - if len(dict_a.keys()) == 0: - sum_dict2 = dict_b - elif len(dict_b.keys()) == 0: - sum_dict2 = dict_a - else: - sum_dict2 = dict() - for key in dict_a.keys(): - if dict_a[key] is None: - sum_dict2.update({key:None}) - elif key != 'time': - sum_dict2.update({key:np.maximum(dict_a[key],dict_b[key])})#instead of summing both loads, take the maximum. - else: - sum_dict2.update({key:dict_a[key]}) - # and return - return sum_dict2 - -class MCSA(object): - ''' - The MCSA class defines a Monte Carlo Survival Analysis - ''' - # All object parameters are given in kwargs - def __init__(self, cluster, **kwargs): - # load the dataset of the cluster into ds - ds = data.get_occDict(cluster) - # and add them as class parameters - self.OSS = ds['ss'] - self.OPM = {1:ds['os_1'], 2:ds['os_2'], 3:ds['os_3']} - self.ODM = {1:ds['ol_1'], 2:ds['ol_2'], 3:ds['ol_3']} -# self.RED = ds['RED'] - - def startstate(self): - ''' - Get the startstate for first simulation day at 4:00 AM. - ''' - # we define the startstate based on the given probability - probs = [self.OSS['1'], self.OSS['2'], self.OSS['3']] - state = get_probability(random.random(), probs) - # and retrun the value - return int(state) - - def transition(self, state, timebin): - ''' - Get next occupancy state from current state ending at time. - ''' - # we define the new state based on the given probability - probs = self.OPM[state][str(timebin)] - newoc = get_probability(random.random(), probs) - # and retrun the value - return int(newoc) - - def duration(self, state, timebin): - ''' - Get the duration of current state started at time. - ''' - # we define the new duration based on the given probability - probs = self.ODM[state][str(timebin)] - durat = get_probability(random.random(), probs) - # and retrun the value - return durat - - -class DTMC(object): - ''' - The DTMC class defines a Discrete-Time Markov Chain - ''' - # All object parameters are given in kwargs - def __init__(self, clusterDict, **kwargs): - # load the dataset of the cluster into ds - self.ds = dict() - for i in range(5): - self.ds.update({i:data.get_actDict(clusterDict['wkdy'])}) - self.ds.update({5:data.get_actDict(clusterDict['sat'])}) - self.ds.update({6:data.get_actDict(clusterDict['son'])}) - def get_var(self, dow, act, step): - # get the probability of the given activity for daytype dow at step - return self.ds[dow][act][step] - From c770624fbf1d9e7be784e7a1dee922464f835423 Mon Sep 17 00:00:00 2001 From: Silke Verbruggen <40292967+SilkeVerbruggen@users.noreply.github.com> Date: Mon, 17 Feb 2020 17:06:13 +0100 Subject: [PATCH 4/8] Sharing Of Appliances Currently the load per appliance is calculated for each person and just added together. But this means that for example for the fridge the load is added multiple times. This does not represent the reality. Changes are made in residential.py and stats.py. In residential.py the load of appliances is calculated based on the type of appliance (shared or not, and continuously used or not). For this an extra function was added in stats.py. --- Corpus/residential.py | 138 ++++++++++++++++++++++++++++++++---------- Corpus/stats.py | 29 ++++++++- 2 files changed, 132 insertions(+), 35 deletions(-) diff --git a/Corpus/residential.py b/Corpus/residential.py index d106f83..26dddb2 100644 --- a/Corpus/residential.py +++ b/Corpus/residential.py @@ -23,6 +23,17 @@ from Data.Households import households from Data.Appliances import set_appliances +#changes for new cold-appliance fix ####################################### +# Based on 10000 runs, these new values combined with rule-based fix (see in def appliances) +# lead to the same overall ownership as the original values. +# We change it here so that the original remain in the Appliances file. +set_appliances['Refrigerator']['owner']=0.27 # original: 0.430 +set_appliances['FridgeFreezer']['owner']=0.40 # original: 0.651 +set_appliances['ChestFreezer']['owner']=0.19 # original: 0.163 +set_appliances['UprightFreezer']['owner']=0.31 # original: 0.291 + +#################################################################### + class Household(object): ''' The Household class is the main class of ProclivityPy, defining the @@ -72,7 +83,7 @@ def members(**kwargs): raise TypeError('Given membertypes is no List of strings.') # If no types are given, random statististics are applied else: - key = random.randint(0, len(households)) + key = random.randint(1, len(households)) members = households[key] # And return the members as list fo strings return members @@ -90,6 +101,21 @@ def appliances(): obj = Equipment(**set_appliances[app]) owner = obj.owner >= random.random() app_n.append(app) if owner else None + + # Cold appliances fix: ############################################### + if not ('FridgeFreezer' in app_n) and not ('Refrigerator' in app_n): # if there was none of the two-> add one of the two. + # Find probability of household to own FF instead of R: (FF ownership over sum of two ownerships-> scale to 0-1 interval) + prob=set_appliances['FridgeFreezer']['owner']/(set_appliances['FridgeFreezer']['owner']+set_appliances['Refrigerator']['owner']) + # if random number is below prob, then the household will own a FF, otherwise a R -> add it + app_n.append('FridgeFreezer') if prob >= random.random() else app_n.append('Refrigerator') + + if 'FridgeFreezer' in app_n and 'ChestFreezer' in app_n and 'UprightFreezer' in app_n: #if there were 3 freezers-> remove a freezer-only + #find probability of household to own CF instead of UF: (CF ownership over sum of two ownerships-> scale to 0-1 interval) + prob=set_appliances['ChestFreezer']['owner']/(set_appliances['ChestFreezer']['owner']+set_appliances['UprightFreezer']['owner']) + # if random number is below prob, then the household will own a CF, otherwise an UF-> remove the other + app_n.remove('UprightFreezer') if prob >= random.random() else app_n.remove('ChestFreezer') #remove the one you don't own + + ######################################################################### return app_n def tappings(): @@ -118,12 +144,12 @@ def clusters(members): self.taps = tappings() self.clusters = clusters(self.members) # and return - print 'Household-object created and parameterized.' - print ' - Employment types are %s' % str(self.members) + print ('Household-object created and parameterized.') + print (' - Employment types are %s' % str(self.members)) summary = [] #loop dics and remove dubbles for member in self.clusters: summary += member.values() - print ' - Set of clusters is %s' % str(list(set(summary))) + print (' - Set of clusters is %s' % str(list(set(summary)))) return None @@ -305,8 +331,8 @@ def merge(occ): # and print statements presence = [to for to in self.occ_m[0] if to < 2] hours = len(presence)/6. - print ' - Total presence time is {0:.1f} out of {1} hours'.format(hours, self.nday*24) - print '\tbeing {:.1f} percent)'.format(hours*100/(self.nday*24)) + print (' - Total presence time is {0:.1f} out of {1} hours'.format(hours, self.nday*24)) + print ('\tbeing {:.1f} percent)'.format(hours*100/(self.nday*24))) return None def __plugload__(self): @@ -339,10 +365,26 @@ def receptacles(self): r_app = dict() n_app = 0 # loop for all household mmembers + print (app) for i in counter: - r_appi, n_appi = eq.simulate(nday, dow, self.clusters[i], self.occ[i]) - r_app = stats.sum_dict(r_app, r_appi) - n_app += n_appi + # if appliances are not shared the load of using the appliance can be summed for each householdmember + if app in ('CordlessPhone','PC'): # it is assumed that only the cordlessphone and PC are not shared. Each person in the household (>12) has its own phone or PC. + r_appi, n_appi = eq.simulate(nday, dow, self.clusters[i], self.occ[i]) + r_app = stats.sum_dict(r_app, r_appi) + n_app += n_appi + else: + # if appliances are shared the load of using the appliance can be maximal the cycle load. + # if the appliance is an appliance that is working continously the load is simulated for only 1 person + if app in ('FridgeFreezer', 'Refrigerator', 'ChestFreezer', 'UprightFreezer', 'Clock'): + if i == 0: + r_appi, n_appi = eq.simulate(nday, dow, self.clusters[i], self.occ[i]) + r_app = stats.sum_dict(r_app, r_appi) + n_app += n_appi + # if the appliance is not continously used, but sometimes shared, the cycle load is assigned when minimal one person is using it, when nobody is using it the stanby load is assigned + else: + r_appi, n_appi = eq.simulate(nday, dow, self.clusters[i], self.occ[i]) + r_app = stats.sum_dict2(r_app, r_appi) + n_app += n_appi # and sum result_n.update({app:n_app}) power += r_app['P'] @@ -350,7 +392,7 @@ def receptacles(self): conv += r_app['QCon'] # a new time axis for power output is to be created as a different # time step is used in comparison to occupancy - time = 4*60*600 + np.arange(0, (nmin+1)*60, 60) + time = 4*60*60 + np.arange(0, (nmin+1)*60, 60) react = np.zeros(nmin+1) @@ -363,7 +405,7 @@ def receptacles(self): # output ########################################################## # only the power load is returned load = int(np.sum(result['P'])/60/1000) - print ' - Receptacle load is %s kWh' % str(load) + print (' - Receptacle load is %s kWh' % str(load)) return None @@ -378,12 +420,15 @@ def lightingload(self): # levels which determine the need for lighting if occupant. # The loaded solar data represent the global horizontal radiation # at a time-step of 1-minute for Uccle, Belgium + # Since the data starts at midnight, a shift to 4am is necessary + # so that it coincides with the occupancy data!!! + # the first 4 h are moved to the end. os.chdir(r'../Data') file = open('Climate/irradiance.txt','r') data_pickle = file.read() file.close() irr = cPickle.loads(data_pickle) - + irr = np.roll(irr,-240) # brings first 4h to end # script ########################################################## # a yearly simulation is basic, also in a unittest nday = self.nday @@ -393,7 +438,7 @@ def lightingload(self): # the model is found on an ideal power level power_id depending on # irradiance level and occupancy (but not on light switching # behavior of occupants itself) - time = np.arange(0, (minutes+1)*60, 60) + time = 4*60*60 + np.arange(0, (minutes+1)*60, 60) to = -1 # time counter for occupancy tl = -1 # time counter for minutes in lighting load power_max = 200 @@ -414,7 +459,7 @@ def lightingload(self): # determine all transitions of appliances depending on # the appliance basic properties, ie. stochastic versus # cycling power profile - if occ_m[to] == 0: + if occ_m[to] > int(1): P[tl] = pow_id[tl] elif random.random() <= prob_adj: delta = P[tl-1] - pow_id[tl] @@ -438,7 +483,7 @@ def lightingload(self): # output ########################################################## # only the power load is returned load = int(np.sum(result['P'])/60/1000) - print ' - Lighting load is %s kWh' % str(load) + print (' - Lighting load is %s kWh' % str(load)) return None @@ -471,7 +516,7 @@ def __dhwload__(self): flow += r_tap['mDHW'] # a new time axis for power output is to be created as a different # time step is used in comparison to occupancy - time = 4*60*600 + np.arange(0, (nmin+1)*60, 60) + time = 4*60*60 + np.arange(0, (nmin+1)*60, 60) result = {'time':time, 'occ':None, 'P':None, 'Q':None, 'QRad':None, 'QCon':None, 'Wknds':None, 'mDHW':flow} @@ -484,7 +529,7 @@ def __dhwload__(self): # only the power load is returned load = np.sum(result['mDHW']) loadpppd = int(load/self.nday/len(self.clusters)) - print ' - Draw-off is %s l/pp.day' % str(loadpppd) + print (' - Draw-off is %s l/pp.day' % str(loadpppd)) return None @@ -546,7 +591,7 @@ def __shsetting__(self): sh_settings.update({room:shnon}) # and store self.sh_settings = sh_settings - print ' - Average comfort setting is %s Celsius' % str(round(np.average(sh_settings['dayzone']),2)) + print (' - Average comfort setting is %s Celsius' % str(round(np.average(sh_settings['dayzone']),2))) return None def roundUp(self): @@ -565,6 +610,17 @@ def roundUp(self): self.QCon = self.r_receptacles['QCon'] + self.r_lighting['QCon'] self.mDHW = self.r_flows['mDHW'] + ####################################################################### + # bring last 4 h to the front so that data starts at midnight + self.sh_day = np.roll(self.sh_day,24) + self.sh_night = np.roll(self.sh_night,24) + self.sh_bath = np.roll(self.sh_bath,24) + self.P = np.roll(self.P,240) + self.Q = np.roll(self.Q,240) + self.QRad = np.roll(self.QRad,240) + self.QCon = np.roll(self.QCon,240) + self.mDHW = np.roll(self.mDHW,240) + ####################################################################### # then we delete the old data structure to save space del self.sh_settings @@ -639,7 +695,7 @@ def stochastic_flow(self, nday, dow, clusterDict, occ): if random.random() < prob * self.cal: n_fl += 1 left = random.gauss(len_cycle, len_cycle/10) - flow[tl] += self.standby_flow + flow[tl] += self.standby_flow else: left += -1 flow[tl] += self.cycle_flow @@ -694,7 +750,7 @@ def stochastic_load(self, nday, dow, clusterDict, occ): if random.random() < prob * self.cal: n_eq += 1 left = random.gauss(len_cycle, len_cycle/10) - P[tl] += self.standby_power + P[tl] += self.standby_power else: left += -1 P[tl] += self.cycle_power @@ -707,22 +763,40 @@ def stochastic_load(self, nday, dow, clusterDict, occ): def cycle_load(self, nday): ''' Simulate cycling appliances, eg. fridges and freezers based on - average clycle length + average clycle length and delay between cycles ''' nbin = nday*24*60 P = np.zeros(nbin+1) - Q = np.zeros(nbin+1) - n_eq = 0 - left = random.gauss(self.delay, self.delay/4) - for tl in range(nbin+1): - if left <= 0: - n_eq += 1 - left += self.cycle_length - P[tl] = self.cycle_power - else: - left += -1 - P[tl] = self.standby_power + Q = np.zeros(nbin+1) #currently no data included (remains zero) + n_eq = 0 # number of cycles for calibration of `cal` parameter of appliance + + # define length of cycles (same for entire year, assumed to depend on appliance) + len_cycle=random.gauss(self.cycle_length, self.cycle_length/10) + # define duration of break between cycles (same for entire year) + delay=random.gauss(self.delay, self.delay/4) + + # start as OFF (assumption) + on=False #is it ON? + left = delay # time left until change of state + + for tl in range(nbin+1): # loop over every minute of the year + # if there is time LEFT until change of state, remain as is + # if time is up, change state: + if left <= 0: + on=not on # switch to opposite state ON/OFF + if on: # if switched ON + n_eq += 1 # add one to cycle counter + left = len_cycle #start counting 1 cycle length + else: # if switched OFF + left = delay #start counting time until next cycle + # either way, count downt the time until next change of state + left += -1 + # allocate correct power, depending on current state ON/OFF + if on: + P[tl] = self.cycle_power # instead of the average consumption, could be sampled from normal distribution as well + else: + P[tl] = self.standby_power r_eq = {'time':time, 'occ':None, 'P':P, 'Q':Q, 'QRad':P*self.frad, 'QCon':P*self.fconv, 'Wknds':None, 'mDHW':None} diff --git a/Corpus/stats.py b/Corpus/stats.py index 15b7e82..e2dbae3 100644 --- a/Corpus/stats.py +++ b/Corpus/stats.py @@ -26,7 +26,7 @@ def get_probability(rnd, prob, p_type='cum'): def sum_dict(dict_a, dict_b): ''' - Sum the values stored under the same keys in python dictionarys. + Sum the values stored under the same keys in python dictionaries. ''' # loop through the keys and sum both values given if len(dict_a.keys()) == 0: @@ -36,7 +36,7 @@ def sum_dict(dict_a, dict_b): else: sum_dict = dict() for key in dict_a.keys(): - if dict_a[key] == None: + if dict_a[key] is None: sum_dict.update({key:None}) elif key != 'time': sum_dict.update({key:dict_a[key]+dict_b[key]}) @@ -45,6 +45,29 @@ def sum_dict(dict_a, dict_b): # and return return sum_dict +def sum_dict2(dict_a, dict_b): + ''' + Sum the values stored under the same keys in python dictionaries. Specifically for summing loads of shared appliances. + This sumation allows the set a maximum to the total load. + When one or more persons use the appliance the load is assigned (max) and when nobody is using the appliance the stanby load is assigned. + ''' + # loop through the keys and sum both values given + if len(dict_a.keys()) == 0: + sum_dict2 = dict_b + elif len(dict_b.keys()) == 0: + sum_dict2 = dict_a + else: + sum_dict2 = dict() + for key in dict_a.keys(): + if dict_a[key] is None: + sum_dict2.update({key:None}) + elif key != 'time': + sum_dict2.update({key:np.maximum(dict_a[key],dict_b[key])})#instead of summing both loads, take the maximum. + else: + sum_dict2.update({key:dict_a[key]}) + # and return + return sum_dict2 + class MCSA(object): ''' The MCSA class defines a Monte Carlo Survival Analysis @@ -100,7 +123,7 @@ def __init__(self, clusterDict, **kwargs): self.ds = dict() for i in range(5): self.ds.update({i:data.get_actDict(clusterDict['wkdy'])}) - self.ds.update({5:data.get_actDict(clusterDict['son'])}) + self.ds.update({5:data.get_actDict(clusterDict['sat'])}) self.ds.update({6:data.get_actDict(clusterDict['son'])}) def get_var(self, dow, act, step): # get the probability of the given activity for daytype dow at step From b3b50b73a8908b34c4da555c0211cc0d4813db90 Mon Sep 17 00:00:00 2001 From: siverbru Date: Mon, 17 Feb 2020 18:20:26 +0100 Subject: [PATCH 5/8] Update residential.py --- Corpus/residential.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Corpus/residential.py b/Corpus/residential.py index 26dddb2..60d4871 100644 --- a/Corpus/residential.py +++ b/Corpus/residential.py @@ -364,8 +364,7 @@ def receptacles(self): eq = Equipment(**set_appliances[app]) r_app = dict() n_app = 0 - # loop for all household mmembers - print (app) + # loop for all household members for i in counter: # if appliances are not shared the load of using the appliance can be summed for each householdmember if app in ('CordlessPhone','PC'): # it is assumed that only the cordlessphone and PC are not shared. Each person in the household (>12) has its own phone or PC. From f8407495b0ff404f5b779dd1f58caf583f1fb326 Mon Sep 17 00:00:00 2001 From: siverbru Date: Wed, 19 Feb 2020 10:41:58 +0100 Subject: [PATCH 6/8] Update residential.py --- Corpus/residential.py | 61 +++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 59 insertions(+), 2 deletions(-) diff --git a/Corpus/residential.py b/Corpus/residential.py index 60d4871..8c4c781 100644 --- a/Corpus/residential.py +++ b/Corpus/residential.py @@ -486,8 +486,65 @@ def lightingload(self): return None + + def MetabolicHeat(self): + ''' + Simulation of the metabolic heat gains. + ''' + + # it is assumed that 50% of the produced heat is radiative + # and 50% is convective + fRad = 0.5 + fCon = 0.5 + counter = range(len(self.clusters)) + nday = self.nday + nbin = 144 + nmin = self.nday * 1440 + rad = np.zeros(nmin+1) + con = np.zeros(nmin+1) + for i in counter: + to = -1 # time counter for occupancy + tl = -1 # time counter for minutes + # the heat a person produces is dependent on the metabolism + met_sl = random.gauss(0.8,0.05) # metabolism when sleeping + met_act = random.gauss(2,0.1) # metabolism when active + x = [0.8, 1.25] + f = [70,110] + sleep = np.interp(met_sl, x, f) + active = np.interp(met_act, x, f) + Met = np.zeros(nmin+1) + for doy, step in itertools.product(range(nday), range(nbin)): + to += 1 + for run in range(0, 10): + tl += 1 + if self.occ[i][to]==1: + Met[tl] = active + elif self.occ[i][to]==2: + Met[tl] = sleep + # and sum + rad += fRad*Met + con += fCon*Met + + # a new time axis for power output is to be created as a different + # time step is used in comparison to occupancy + time = 4*60*60 + np.arange(0, (nmin+1)*60, 60) + + result = {'time':time, 'occ':None, 'P':None, 'Q':None, + 'QRad':rad, 'QCon':con, 'Wknds':None, 'mDHW':None} + + self.r_MetabolicHeat = result + + # output ########################################################## + # the total heat gains of the persons are returned + HeatG = int((np.sum(result['QCon'])/60/1000)+(np.sum(result['QRad'])/60/1000)) + print (' - Total metabolic heat gains are %s kWh' % str(HeatG)) + + + return None + receptacles(self) lightingload(self) + MetabolicHeat(self) return None @@ -605,8 +662,8 @@ def roundUp(self): self.sh_bath = self.sh_settings['bathroom'] self.P = self.r_receptacles['P'] + self.r_lighting['P'] self.Q = self.r_receptacles['Q'] + self.r_lighting['Q'] - self.QRad = self.r_receptacles['QRad'] + self.r_lighting['QRad'] - self.QCon = self.r_receptacles['QCon'] + self.r_lighting['QCon'] + self.QRad = self.r_receptacles['QRad'] + self.r_lighting['QRad'] + self.r_MetabolicHeat['QRad'] + self.QCon = self.r_receptacles['QCon'] + self.r_lighting['QCon'] + self.r_MetabolicHeat['QCon'] self.mDHW = self.r_flows['mDHW'] ####################################################################### From ad7d55b604f47539d14a6891be6449d45f50f4b4 Mon Sep 17 00:00:00 2001 From: siverbru Date: Wed, 19 Feb 2020 10:54:33 +0100 Subject: [PATCH 7/8] Update residential.py --- Corpus/residential.py | 28 ++++++---------------------- 1 file changed, 6 insertions(+), 22 deletions(-) diff --git a/Corpus/residential.py b/Corpus/residential.py index 8c4c781..564a5d2 100644 --- a/Corpus/residential.py +++ b/Corpus/residential.py @@ -364,26 +364,11 @@ def receptacles(self): eq = Equipment(**set_appliances[app]) r_app = dict() n_app = 0 - # loop for all household members + # loop for all household mmembers for i in counter: - # if appliances are not shared the load of using the appliance can be summed for each householdmember - if app in ('CordlessPhone','PC'): # it is assumed that only the cordlessphone and PC are not shared. Each person in the household (>12) has its own phone or PC. - r_appi, n_appi = eq.simulate(nday, dow, self.clusters[i], self.occ[i]) - r_app = stats.sum_dict(r_app, r_appi) - n_app += n_appi - else: - # if appliances are shared the load of using the appliance can be maximal the cycle load. - # if the appliance is an appliance that is working continously the load is simulated for only 1 person - if app in ('FridgeFreezer', 'Refrigerator', 'ChestFreezer', 'UprightFreezer', 'Clock'): - if i == 0: - r_appi, n_appi = eq.simulate(nday, dow, self.clusters[i], self.occ[i]) - r_app = stats.sum_dict(r_app, r_appi) - n_app += n_appi - # if the appliance is not continously used, but sometimes shared, the cycle load is assigned when minimal one person is using it, when nobody is using it the stanby load is assigned - else: - r_appi, n_appi = eq.simulate(nday, dow, self.clusters[i], self.occ[i]) - r_app = stats.sum_dict2(r_app, r_appi) - n_app += n_appi + r_appi, n_appi = eq.simulate(nday, dow, self.clusters[i], self.occ[i]) + r_app = stats.sum_dict(r_app, r_appi) + n_app += n_appi # and sum result_n.update({app:n_app}) power += r_app['P'] @@ -485,8 +470,7 @@ def lightingload(self): print (' - Lighting load is %s kWh' % str(load)) return None - - + def MetabolicHeat(self): ''' Simulation of the metabolic heat gains. @@ -541,7 +525,7 @@ def MetabolicHeat(self): return None - + receptacles(self) lightingload(self) MetabolicHeat(self) From 7a81cab38590cd555538ac851d501070686a3d46 Mon Sep 17 00:00:00 2001 From: siverbru Date: Wed, 19 Feb 2020 10:58:30 +0100 Subject: [PATCH 8/8] Update stats.py --- Corpus/stats.py | 23 ----------------------- 1 file changed, 23 deletions(-) diff --git a/Corpus/stats.py b/Corpus/stats.py index e2dbae3..e78d769 100644 --- a/Corpus/stats.py +++ b/Corpus/stats.py @@ -45,29 +45,6 @@ def sum_dict(dict_a, dict_b): # and return return sum_dict -def sum_dict2(dict_a, dict_b): - ''' - Sum the values stored under the same keys in python dictionaries. Specifically for summing loads of shared appliances. - This sumation allows the set a maximum to the total load. - When one or more persons use the appliance the load is assigned (max) and when nobody is using the appliance the stanby load is assigned. - ''' - # loop through the keys and sum both values given - if len(dict_a.keys()) == 0: - sum_dict2 = dict_b - elif len(dict_b.keys()) == 0: - sum_dict2 = dict_a - else: - sum_dict2 = dict() - for key in dict_a.keys(): - if dict_a[key] is None: - sum_dict2.update({key:None}) - elif key != 'time': - sum_dict2.update({key:np.maximum(dict_a[key],dict_b[key])})#instead of summing both loads, take the maximum. - else: - sum_dict2.update({key:dict_a[key]}) - # and return - return sum_dict2 - class MCSA(object): ''' The MCSA class defines a Monte Carlo Survival Analysis