def __init__(self, building, zones, lambda_val, start, end, forecasting_horizon, window, tstats, non_contrallable_data=None): """ :param building: :param zones: :param lambda_val: :param start: datetime with timezone :param end: datetime with timezone :param forecasting_horizon: :param window: :param tstats: :param non_contrallable_data: """ assert xsg.get_window_in_sec( forecasting_horizon) % xsg.get_window_in_sec(window) == 0 self.building = building self.zones = zones self.window = window self.lambda_val = lambda_val self.forecasting_horizon = forecasting_horizon self.delta_forecasting_horizon = datetime.timedelta( seconds=xsg.get_window_in_sec(forecasting_horizon)) self.delta_window = datetime.timedelta( seconds=xsg.get_window_in_sec(window)) # Simulation end is when current_time reaches end and end will become the end of our data. self.simulation_end = end end += self.delta_forecasting_horizon self.DataManager = DataManager(building, zones, start, end, window, non_contrallable_data) self.tstats = tstats # dictionary of simulator object with key zone. has functions: current_temperature, next_temperature(action) self.current_time = start self.current_time_step = 0 self.actions = {iter_zone: [] for iter_zone in self.zones} # {zone: [ints]} self.temperatures = { iter_zone: [self.tstats[iter_zone].temperature] for iter_zone in self.zones } # {zone: [floats]}
def get_actions(request, all_buildings, all_zones): """Returns temperatures for a given request or None. Guarantees that no Nan values in returned data exist.""" logging.info("received request:", request.building, request.zones, request.start, request.end, request.window, request.lambda_val, request.starting_temperatures, request.unit) duration = xsg.get_window_in_sec(request.window) request_length = [ len(request.building), len(request.zones), request.start, request.end, len(request.starting_temperatures), duration ] if request.building not in all_buildings: return None, "invalid request, building name is not valid." if any(v == 0 for v in request_length): return None, "invalid request, empty params" if request.end > int(time.time() * 1e9): return None, "invalid request, end date is in the future." if request.start >= request.end: return None, "invalid request, start date is after end date." if request.start < 0 or request.end < 0: return None, "invalid request, negative dates." if request.start + (duration * 1e9) > request.end: return None, "invalid request, start date + window is greater than end date." if request.unit != "F": return None, "invalid request, only Fahrenheit is supported as a unit." if not 0 <= request.lambda_val <= 1: return None, "invalid request, lambda_val needs to be between 0 and 1." if not all([ iter_zone in request.starting_temperatures for iter_zone in all_zones[request.building] ]): return None, "invalid request, missing zones in starting_temperatures." d_start = datetime.utcfromtimestamp(float(request.start / 1e9)).replace(tzinfo=pytz.utc) d_end = datetime.utcfromtimestamp(float(request.end / 1e9)).replace(tzinfo=pytz.utc) MPC_instance = MPC(request.building, request.zones, d_start, d_end, request.window, request.lambda_val) actions, err = MPC_instance.advise(request.starting_temperatures) if actions is None: return None, err return optimizer_pb2.Reply(actions=actions), None
def get_train_test(building, zone, start, end, prediction_window, raw_data_granularity, train_ratio, is_second_order, use_occupancy, curr_action_timesteps, prev_action_timesteps, check_data=True): """Create data set to train with. :param building: (string) building name :param zone: (string) zone name :param start: (datetime timezone aware) start of the dataset used :param end: (datetime timezone aware) start of the dataset used :param prediction_window: (str) number of seconds between predictions :param raw_data_granularity: (str) the window size of the raw data. needs to be less than prediction_window. :param train_ratio: (float) in (0, 1). the ratio in which to split train and test set from the given dataset. The train set comes before test set in time. :param is_second_order: (bool) Whether we are using second order in temperature. :param curr_action_timesteps: (int) The order of the current action. Set 0 if there should only be one action. :param prev_action_timesteps: (int) The order of the previous action. Set 0 if there should only be one prev action. Set -1 if it should not be used at all. :param method: (str) ["OLS", "random_forest", "LSTM"] are the available methods so far :param rmse_series: np.array the rmse of the forecasting procedure. :param num_forecasts: (int) The number of forecasts which contributed to the RMSE. :param forecasting_horizon: (int seconds) The horizon used when forecasting. :param check_data: If True (default), will enforce that training data has the right start/end times (recommended). If False, then data will be returns if it exists; However, the times may be different (allows model to be created faster by using previously prepocessed data) – useful when prototyping since the preprocessing does not have to be repeated. :return: trained sklearn.LinearRegression object. """ seconds_prediction_window = xsg.get_window_in_sec(prediction_window) # Get data # TODO add check that the data we have stored is at least as long and has right prediction_window # TODO Fix how we deal with nan's. some zone temperatures might get set to -1. loaded_data = load_data(building, zone) err = xsg.check_data(loaded_data, start, end, prediction_window, check_nan=True) if (loaded_data is None) or ((err is not None) and check_data): processed_data, err = pid.get_preprocessed_data( building, zone, start, end, prediction_window, raw_data_granularity) if err is not None: return None, None, None, None, err store_data(processed_data, building, zone) else: processed_data = loaded_data.loc[start:end] # add features processed_data = pid.indoor_data_cleaning(processed_data) if is_second_order: processed_data = pid.add_feature_last_temperature(processed_data) if curr_action_timesteps > 0 or prev_action_timesteps > 0: processed_data = pid.convert_categorical_action( processed_data, num_start=curr_action_timesteps, num_end=prev_action_timesteps, interval_thermal=seconds_prediction_window) # split data into training and test sets N = processed_data.shape[0] # number of datapoints train_data = processed_data.iloc[:int(N * train_ratio)] test_data = processed_data.iloc[int(N * train_ratio):] # which columns to drop for training and testing columns_to_drop = ["dt", "action_duration"] if curr_action_timesteps != 0: columns_to_drop.append("action") if prev_action_timesteps != 0 or prev_action_timesteps == -1: columns_to_drop.append( "action_prev" ) # TODO we might want to use this as a feature. so don't set to -1... if not use_occupancy: columns_to_drop.append("occ") # train data train_data = train_data[train_data["dt"] == seconds_prediction_window] train_data = train_data.drop(columns_to_drop, axis=1) if train_data.isna().values.any(): return None, None, None, None, "Nan values detected in training data." train_y = train_data[ "t_next"] # Note: We now assume that there are no Nan values in data. train_X = train_data.drop(["t_next"], axis=1) # test data if test_data.isna().values.any(): return None, None, None, None, "Nan values detected in test data." test_data = test_data[test_data["dt"] == seconds_prediction_window] test_data = test_data.drop(columns_to_drop, axis=1) test_y = test_data["t_next"] test_X = test_data.drop(["t_next"], axis=1) if train_X.shape[0] == 0: return None, None, None, None, "Not enough data to train the model." return train_X, train_y, test_X, test_y, None
def __init__(self, building, zones, start, end, window, non_controllable_data={}): """Exposes: - self.comfortband - self.do_not_exceed - self.occupancy - self.outdoor_temperature - self.discomfort_stub - self.hvac_consumption - self.price - self.all_zone_temperatures: pd.df with columns being zone names and values being F temperatures - self.start - self.unix_start - self.end - self.unix_end - self.window - self.building - self.zones :param building: :param zones: :param start: :param end: :param window: :param non_controllable_data: possible keys: ["comfortband", "do_not_exceed", "occupancy", "outdoor_temperature"] for each key the value needs to be a dictionary with {zone: data} for all zones in self.zones. Outdoor temperature is just data since it is data for the whole building. """ self.start = start self.unix_start = start.timestamp() * 1e9 self.end = end self.unix_end = end.timestamp() * 1e9 self.window = window # timedelta string self.building = building self.zones = zones if non_controllable_data is None: non_controllable_data = {} # TODO add error checking. check that the right zones are given in non_controllable_data and that the start/end/window are right. # Documentation: All data here is in timeseries starting exactly at start and every step corresponds to one # interval. The end is not inclusive. # temperature band temperature_band_stub = xsg.get_temperature_band_stub() if "comfortband" not in non_controllable_data: self.comfortband = { iter_zone: xsg.get_comfortband(temperature_band_stub, self.building, iter_zone, self.start, self.end, self.window) for iter_zone in self.zones} else: self.comfortband = non_controllable_data["comfortband"] err = check_data_zones(self.zones, self.comfortband, start, end, window) if err is not None: raise Exception("Bad comfortband given. " + err) if "do_not_exceed" not in non_controllable_data: self.do_not_exceed = { iter_zone: xsg.get_do_not_exceed(temperature_band_stub, self.building, iter_zone, self.start, self.end, self.window) for iter_zone in self.zones} else: self.do_not_exceed = non_controllable_data["do_not_exceed"] err = check_data_zones(self.zones, self.do_not_exceed, start, end, window) if err is not None: raise Exception("Bad DoNotExceed given. " + err) # occupancy if "occupancy" not in non_controllable_data: occupancy_stub = xsg.get_occupancy_stub() self.occupancy = { iter_zone: xsg.get_occupancy(occupancy_stub, self.building, iter_zone, self.start, self.end, self.window)["occupancy"] for iter_zone in self.zones} else: self.occupancy = non_controllable_data["occupancy"] err = check_data_zones(self.zones, self.occupancy, start, end, window) if err is not None: raise Exception("Bad occupancy given. " + err) # outdoor temperatures if "outdoor_temperature" not in non_controllable_data: outdoor_historic_stub = xsg.get_outdoor_temperature_historic_stub() self.outdoor_temperature = xsg.get_preprocessed_outdoor_temperature(outdoor_historic_stub, self.building, self.start, self.end, self.window)["temperature"] else: self.outdoor_temperature = non_controllable_data["outdoor_temperature"] err = xsg.check_data(self.outdoor_temperature, start, end, window, check_nan=True) if err is not None: raise Exception("Bad outdoor temperature given. " + err) # outdoor_prediction_channel = grpc.insecure_channel(OUTSIDE_PREDICTION) # outdoor_prediction_stub = outdoor_temperature_prediction_pb2_grpc.OutdoorTemperatureStub(outdoor_prediction_channel) # self.outdoor_temperatures = get_outside_temperature( # outdoor_historic_stub, outdoor_prediction_stub, self.building, self.start, self.end, self.window) # discomfort channel # self.discomfort_stub = xsg.get_discomfort_stub(secure=False) # HVAC Consumption TODO ERROR CHECK? hvac_consumption_stub = xsg.get_hvac_consumption_stub() self.hvac_consumption = {iter_zone: xsg.get_hvac_consumption(hvac_consumption_stub, building, iter_zone) for iter_zone in self.zones} if "energy_price" not in non_controllable_data: price_stub = xsg.get_price_stub() self.energy_price = xsg.get_price(price_stub, building, "ENERGY", start, end, window) else: self.energy_price = non_controllable_data["energy_price"] err = xsg.check_data(self.energy_price, start, end, window, check_nan=True) if err is not None: raise Exception("Bad energy prices given. " + err) self.indoor_temperature_prediction_stub = xsg.get_indoor_temperature_prediction_stub() indoor_historic_stub = xsg.get_indoor_historic_stub() # TEMPORARY -------- self.indoor_temperature_prediction = ThermalModel() # ------------ # +++++++++++++ TODO other zone temps uncomment when wanting to use real thermal model and delete uncommented section # # # get indoor temperature for other zones. # if "all_zone_temperature_data" not in non_controllable_data: # building_zone_names_stub = xsg.get_building_zone_names_stub() # all_zones = xsg.get_zones(building_zone_names_stub, building) # self.all_zone_temperature_data = {} # for iter_zone in all_zones: # # TODO there is an error with the indoor historic service where it doesn't return the full lenght of data. # zone_temperature = xsg.get_indoor_temperature_historic(indoor_historic_stub, building, iter_zone, start, end + datetime.timedelta(seconds=xsg.get_window_in_sec(window)), # window) # assert zone_temperature['unit'].values[0] == "F" # zone_temperature = zone_temperature["temperature"].squeeze() # self.all_zone_temperature_data[iter_zone] = zone_temperature.interpolate("time") # self.all_zone_temperature_data = pd.DataFrame(self.all_zone_temperature_data) # else: # self.all_zone_temperature_data = non_controllable_data["all_zone_temperature_data"] # err = check_data_zones(zones, self.all_zone_temperature_data, start, end, window, check_nan=True) # if err is not None: # if "Is missing zone" in err: # raise Exception("Bad indoor temperature data given. " + err) # else: # for iter_zone in zones: # err = xsg.check_data(self.all_zone_temperature_data[iter_zone], start, end, window, True) # if "Nan values in data." in err: # self.all_zone_temperature_data[iter_zone][:] = 70 # TODO only doing this if interpolation above does not work because everything is nan # else: # raise Exception("Bad indoor temperature data given. " + err) building_zone_names_stub = xsg.get_building_zone_names_stub() all_zones = xsg.get_zones(building_zone_names_stub, building) temp_pd = pd.Series(data=0, index = pd.date_range(start, end, freq=str(xsg.get_window_in_sec(window)) + "S")) self.all_zone_temperature_data = {iter_zone: temp_pd for iter_zone in all_zones} err = check_data_zones(zones, self.all_zone_temperature_data, start, end, window, check_nan=True) if err is not None: raise Exception("Bad indoor temperature data given. " + err) self.all_zone_temperature_data = pd.DataFrame(self.all_zone_temperature_data)
def timestep_to_datetime(self, timestep): return self.start + timestep * datetime.timedelta( seconds=xsg.get_window_in_sec(self.window))
def get_preprocessed_data(building, zone, start, end, window, raw_data_granularity="1m"): """Get training data to use for indoor temperature prediction. :param building: (str) building name :param zone: (str) zone name :param start: (datetime timezone aware) :param end: (datetime timezone aware) :param window: (str) the intervals in which to split data. :param raw_data_granularity: (str) the intervals in which to get raw indoor data. :return: pd.df index= start (inclusive) to end (not inclusive) with frequency given by window. col=["t_in", "action", "t_out", "t_next", "occ", "action_duration", "action_prev", "temperature_zone_...", "dt"]. TODO explain meaning of each feature somewhere. """ # get indoor temperature and action for current zone indoor_historic_stub = xsg.get_indoor_historic_stub() indoor_temperatures = _get_indoor_temperature_historic(indoor_historic_stub, building, zone, start, end, raw_data_granularity) assert indoor_temperatures['unit'].values[0] == "F" indoor_temperatures = indoor_temperatures["temperature"].squeeze() indoor_actions = _get_action_historic(indoor_historic_stub, building, zone, start, end, raw_data_granularity) indoor_actions = indoor_actions["action"].squeeze() # get indoor temperature for other zones. building_zone_names_stub = xsg.get_building_zone_names_stub() all_zones = xsg.get_zones(building_zone_names_stub, building) all_other_zone_temperature_data = {} for iter_zone in all_zones: if iter_zone != zone: other_zone_temperature = _get_indoor_temperature_historic(indoor_historic_stub, building, iter_zone, start, end, window) assert other_zone_temperature['unit'].values[0] == "F" other_zone_temperature = other_zone_temperature["temperature"].squeeze() all_other_zone_temperature_data[iter_zone] = other_zone_temperature # Preprocessing indoor data putting temperature and action data together. indoor_data = pd.concat([indoor_temperatures.to_frame(name="t_in"), indoor_actions.to_frame(name="action")], axis=1) preprocessed_data = preprocess_indoor_data(indoor_data, xsg.get_window_in_sec(window)) if preprocessed_data is None: return None, "No data left after preprocessing." # get historic outdoor temperatures outdoor_historic_stub = xsg.get_outdoor_temperature_historic_stub() outdoor_historic_temperatures = xsg.get_outdoor_temperature_historic(outdoor_historic_stub, building, start, end, window) # getting occupancy # get occupancy occupancy_stub = xsg.get_occupancy_stub() occupancy = xsg.get_occupancy(occupancy_stub, building, zone, start, end, window) # add outdoor and occupancy and other zone temperatures preprocessed_data["t_out"] = [outdoor_historic_temperatures.loc[ idx: idx + datetime.timedelta(seconds=xsg.get_window_in_sec(window))].mean() for idx in preprocessed_data.index] preprocessed_data["occ"] = [occupancy.loc[ idx: idx + datetime.timedelta(seconds=xsg.get_window_in_sec(window))].mean() for idx in preprocessed_data.index] for iter_zone, iter_data in all_other_zone_temperature_data.items(): preprocessed_data["temperature_zone_" + iter_zone] = [ iter_data.loc[idx: idx + datetime.timedelta(seconds=xsg.get_window_in_sec(window))].mean() for idx in preprocessed_data.index] # TODO check if we should drop nan values... or at least where they are coming from preprocessed_data = preprocessed_data.dropna(axis=0) return preprocessed_data, None
best_action[iter_zone])) self.actions[iter_zone].append(best_action[iter_zone]) return root def run(self): while self.current_time < self.simulation_end: self.step() if __name__ == "__main__": forecasting_horizon = "4h" end = datetime.datetime.utcnow().replace( tzinfo=pytz.utc) - datetime.timedelta( seconds=xsg.get_window_in_sec(forecasting_horizon)) end = end.replace(microsecond=0) start = end - datetime.timedelta(hours=6) print(start) print(start.timestamp()) building = "avenal-animal-shelter" zones = ["hvac_zone_shelter_corridor"] window = "15m" lambda_val = 0.995 tstats = {iter_zone: Tstat(building, iter_zone, 75) for iter_zone in zones} simulation = SimulationMPC(building, zones, lambda_val, start, end, forecasting_horizon, window, tstats) t = time.time()
def _create_timeseries_feature(data, aggregation_window, aggregation_type, forward_shifts, backward_shifts): """ Creates some features available to a generic time series: - For every datapoint, will aggregate the non-nan values within it's time plus aggregation_window by aggregation type. - Creates higher orders of the data. Shifts the aggregated data according to forward_shifts and backward_shifts. Will append to column name "+/-i" for an i forward/backward shift. :param data: (pd.series) with equally spaced timeseries Index. :param aggregation_window: (str) The timeframe length for which to aggregate. :param aggregation_type: (str) ["mean", "max", "min", "None"]. None does nothing to the data (as if aggregated assuming first value of aggregation window is constant throughout.) :param forward_shifts: (int, positive) Number of forward shifts. Shift window is the timedelta of the input data timeseries. :param backward_shifts: (int, positive) Number of backward shifts. Shift window is the timedelta of the input data timeseries. :return: (pd.df) columns=orginal_col_name+["_+/-i"] *same pd.df index* as given in data input. """ if not aggregation_type in ["mean", "max", "min"]: raise ValueError("Aggregation Type {} not valid/implemented.".format( aggregation_type)) assert forward_shifts >= 0 assert backward_shifts >= 0 # timeseries checks if isinstance(data, pd.DataFrame): if len(data.columns) != 1: raise ValueError( "Data with type pd.DataFrame is expected to have exactly one column." ) elif isinstance(data, pd.Series): data = pd.DataFrame(data, columns=[""]) else: raise ValueError( "Data is expected to be of time pd.Dataframe or pd.Series.") # check if equally spaced timeseries # TODO how long should the data be. Should we check that? # aggregation step if (aggregation_type != "None") and (data.shape[0] > 1): df_sec = (data.index[1] - data.index[0]).seconds agg_sec = xsg.get_window_in_sec(aggregation_window) if agg_sec % df_sec != 0: raise ValueError( "Aggregation window of {}S is not evenly divided by the time length of {}S between data points in the data." .format(agg_sec, df_sec)) if 24 * 60 * 60 % agg_sec != 0: raise ValueError( "Aggregation window of {}S does not evenly divide a day.". format(agg_sec)) resample_dfs = [] curr_base = 0 while curr_base < agg_sec: resample_dfs.append( data.resample("{}S".format(agg_sec), base=curr_base)) curr_base += df_sec if aggregation_type == "mean": aggregated_dfs = [ resample_df.mean() for resample_df in resample_dfs ] elif aggregation_type == "max": aggregated_dfs = [ resample_df.max() for resample_df in resample_dfs ] elif aggregation_type == "min": aggregated_dfs = [ resample_df.min() for resample_df in resample_dfs ] data = pd.concat(aggregated_dfs).sort_index().loc[data.index] # shift step data_name = data.columns[0] shifted_data = data.copy() shifted_data.columns = [data_name + "_0"] for i in range(1, forward_shifts + 1): shifted_data[data_name + "_+{}".format(i)] = shifted_data[data_name + "_0"].shift(periods=-i) for i in range(1, backward_shifts + 1): shifted_data[data_name + "_-{}".format(i)] = shifted_data[data_name + "_0"].shift(periods=+i) data = shifted_data return data
def __init__(self, env_config): self.DataManager = DataManager(env_config["building"], env_config["zones"], env_config["start"], env_config["end"], env_config["window"]) self.start = start self.unix_start = start.timestamp() * 1e9 self.end = end self.unix_end = end.timestamp() * 1e9 self.window = window # timedelta string self.building = building self.zones = zones self.lambda_val = env_config["lambda_val"] # assert self.zones == all zones in building. this is because of the thermal model needing other zone temperatures. self.curr_timestep = 0 self.indoor_starting_temperatures = env_config[ "indoor_starting_temperatures"] # to get starting temperatures [last, current] self.outdoor_starting_temperature = env_config[ "outdoor_starting_temperature"] self.tstats = {} for iter_zone in self.zones: self.tstats[iter_zone] = Tstat( self.building, iter_zone, self.indoor_starting_temperatures[iter_zone]["current"], last_temperature=self.indoor_starting_temperatures[iter_zone] ["last"]) assert 60 * 60 % xsg.get_window_in_sec( self.window) == 0 # window divides an hour assert (self.end - self.start).total_seconds() % xsg.get_window_in_sec( self.window) == 0 # window divides the timeframe # the number of timesteps self.num_timesteps = int((self.end - self.start).total_seconds() / xsg.get_window_in_sec(self.window)) self.unit = env_config["unit"] assert self.unit == "F" # all zones current and last temperature = 2*num_zones # building outside temperature -> make a class for how this behaves = 1 # timestep -> do one hot encoding of week, day, hour, window \approx 4 + 7 + 24 + 60*60 / window low_bound = [32] * 2 * len( self.zones ) # we could use parametric temperature bounds... for now we will give negative inft reward low_bound += [-100] # for outside temperature we cannot gurantee much high_bound = [100] * 2 * len(self.zones) high_bound += [200] # for outside temperature we cannot gurantee much low_bound += [0] * ( self.num_timesteps + 1 ) # total timesteps plus the final timestep which wont be executed high_bound += [1] * ( self.num_timesteps + 1 ) # total timesteps plus the final timestep which wont be executed self.observation_space = Box(low=np.array(low_bound), high=np.array(high_bound), dtype=np.float32) self.action_space = Tuple((Discrete(3), ) * len(self.zones)) self.reset()
def get_simulation(request, all_buildings, all_zones): """Returns simulation temperatures and actions.""" print("received request:", request.building, request.zones, request.start, request.end, request.window, request.forecasting_horizon, request.lambda_val, request.starting_temperatures, request.unit, request.num_runs) duration = xsg.get_window_in_sec(request.window) forecasting_horizon = xsg.get_window_in_sec(request.forecasting_horizon) request_length = [ len(request.building), len(request.zones), request.start, request.end, len(request.starting_temperatures), duration ] if request.building not in all_buildings: return None, "invalid request, building name is not valid." if any(v == 0 for v in request_length): return None, "invalid request, empty params" if request.end > int(time.time() * 1e9): return None, "invalid request, end date is in the future." if request.start >= request.end: return None, "invalid request, start date is after end date." if request.start < 0 or request.end < 0: return None, "invalid request, negative dates." if request.start + (duration * 1e9) > request.end: return None, "invalid request, start date + window is greater than end date." if request.start + (forecasting_horizon * 1e9) > request.end: return None, "invalid request, start date + forecasting_horizon is greater than end date." if request.unit != "F": return None, "invalid request, only Fahrenheit is supported as a unit." if not 0 <= request.lambda_val <= 1: return None, "invalid request, lambda_val needs to be between 0 and 1." if not all([ iter_zone in request.starting_temperatures for iter_zone in all_zones[request.building] ]): return None, "invalid request, missing zones in starting_temperatures." d_start = datetime.utcfromtimestamp(float(request.start / 1e9)).replace(tzinfo=pytz.utc) d_end = datetime.utcfromtimestamp(float(request.end / 1e9)).replace(tzinfo=pytz.utc) if request.num_runs != 1: return None, "invalid request, NotImplementedError: variables num_runs not working. Set num_runs to 1." # somewhat inefficient since this could be stored as class var... tstats = { iter_zone: Tstat(request.building, iter_zone, request.starting_temperatures[iter_zone], suppress_not_enough_data_error=True) for iter_zone in request.zones } Simulation_instance = SimulationMPC(request.building, request.zones, request.lambda_val, d_start, d_end, request.forecasting_horizon, request.window, tstats) Simulation_instance.run() actions = Simulation_instance.actions temperatures = Simulation_instance.temperatures if actions is None or temperatures is None: return None, "Unable to Simulate. Simulation returns None." print(actions) actions = { iter_zone: optimizer_pb2.ActionList(actions=actions[iter_zone]) for iter_zone in request.zones } temperatures = { iter_zone: optimizer_pb2.TemperatureList(temperatures=temperatures[iter_zone]) for iter_zone in request.zones } return optimizer_pb2.SimulationReply(simulation_results=[ optimizer_pb2.ActionTemperatureReply(actions=actions, temperatures=temperatures) ]), None