Custom callback functions in Sinergymο
First letβs import the 5zone environment as an example for custom callbacks. We need add new variables to be used by the callback function.
[1]:
import gymnasium as gym
import numpy as np
import sinergym
new_variables = {
'outdoor_temperature': ('Site Outdoor Air Drybulb Temperature', 'Environment'),
'outdoor_humidity': ('Site Outdoor Air Relative Humidity', 'Environment'),
'wind_speed': ('Site Wind Speed', 'Environment'),
'wind_direction': ('Site Wind Direction', 'Environment'),
'diffuse_solar_radiation': (
'Site Diffuse Solar Radiation Rate per Area',
'Environment',
),
'direct_solar_radiation': (
'Site Direct Solar Radiation Rate per Area',
'Environment',
),
'air_temperature_SPACE1-1': ('Zone Air Temperature', 'SPACE1-1'),
'air_temperature_SPACE2-1': ('Zone Air Temperature', 'SPACE2-1'),
'air_temperature_SPACE3-1': ('Zone Air Temperature', 'SPACE3-1'),
'air_temperature_SPACE4-1': ('Zone Air Temperature', 'SPACE4-1'),
'air_temperature': ('Zone Air Temperature', 'SPACE5-1'),
'air_relative_humidity_SPACE1-1': ('Zone Air Relative Humidity', 'SPACE1-1'),
'air_relative_humidity_SPACE2-1': ('Zone Air Relative Humidity', 'SPACE2-1'),
'air_relative_humidity_SPACE3-1': ('Zone Air Relative Humidity', 'SPACE3-1'),
'air_relative_humidity_SPACE4-1': ('Zone Air Relative Humidity', 'SPACE4-1'),
'air_relative_humidity_SPACE5-1': ('Zone Air Relative Humidity', 'SPACE5-1'),
'HVAC_electricity_demand_rate': (
'Facility Total HVAC Electricity Demand Rate',
'Whole Building',
),
}
env = gym.make("Eplus-5zone-hot-continuous-stochastic-v1", variables=new_variables)
print(
'The current observation variables are:: {}'.format(
env.get_wrapper_attr('observation_variables')
)
)
#==============================================================================================#
[ENVIRONMENT] (INFO) : Creating Gymnasium environment.
[ENVIRONMENT] (INFO) : Name: Eplus-5zone-hot-continuous-stochastic-v1
#==============================================================================================#
[MODEL] (INFO) : Working directory created: /workspaces/sinergym/examples/Eplus-5zone-hot-continuous-stochastic-v1-res16
[MODEL] (INFO) : Model Config is correct.
[MODEL] (INFO) : Building model Output:Variable updated with defined variable names.
[MODEL] (INFO) : Updated building model Output:Meter with meter names.
[MODEL] (INFO) : Runperiod established.
[MODEL] (INFO) : Episode length (seconds): 31536000.0
[MODEL] (INFO) : timestep size (seconds): 900.0
[MODEL] (INFO) : timesteps per episode: 35040
[REWARD] (INFO) : Reward function initialized.
[ENVIRONMENT] (INFO) : Environment created successfully.
The current observation variables are:: ['month', 'day_of_month', 'hour', 'outdoor_temperature', 'outdoor_humidity', 'wind_speed', 'wind_direction', 'diffuse_solar_radiation', 'direct_solar_radiation', 'air_temperature_SPACE1-1', 'air_temperature_SPACE2-1', 'air_temperature_SPACE3-1', 'air_temperature_SPACE4-1', 'air_temperature', 'air_relative_humidity_SPACE1-1', 'air_relative_humidity_SPACE2-1', 'air_relative_humidity_SPACE3-1', 'air_relative_humidity_SPACE4-1', 'air_relative_humidity_SPACE5-1', 'HVAC_electricity_demand_rate', 'total_electricity_HVAC']
Define custom callbacksο
The callback _avg_zone_timestep computes a volume-weighted average air temperature across all five thermal zones of the building (SPACE1-1 through SPACE5-1) at every zone timestep and stores the result in _avg["value"].
How it works:
State dict ``_avg`` β A module-level dictionary holds EnergyPlus variable handles (
T1βT5) and zone air volumes (vol1βvol5), plus aneed_handlesflag and the runningvalue. This avoids re-fetching handles every timestep.Handle initialisation (first call only) β On the first timestep where the EnergyPlus API data is fully ready, the callback retrieves:
Variable handles for
Zone Air Temperaturein each zone viaget_variable_handle.Internal variable values for
Zone Air Volumein each zone viaget_internal_variable_handle/get_internal_variable_value. Zone volumes are static for a given building model, so they are read once and cached.
Volume-weighted mean temperature β Each subsequent timestep the callback reads the current zone temperatures and computes:
The result is written to _avg["value"], making it available to the logger wrapper.
Callback hook β The function is registered on the
callback_end_zone_timestep_before_zone_reportinghook, so it runs at the end of each zone timestep before zone-level reporting occurs, ensuring the temperature values are fresh for that timestep.
We first define the _avg dict to store handles and the computed value, then define the callback function that uses the EnergyPlus runtime API.
[ ]:
_avg: dict = {
"need_handles": True,
"T1": None,
"T2": None,
"T3": None,
"T4": None,
"T5": None,
"vol1": None,
"vol2": None,
"vol3": None,
"vol4": None,
"vol5": None,
"value": 0.0,
}
# define a callback function to compute the average zone air temperature at each timestep
def _avg_zone_timestep(state):
# get the energyplus simulator wrapper to access the exchange API
simulator = env.get_wrapper_attr("energyplus_simulator")
# check if the API data is fully ready before trying to access it
if not simulator.exchange.api_data_fully_ready(state):
return
# if we haven't already, get the variable handles and zone volumes to compute the average temperature
if _avg["need_handles"]:
_avg["T1"] = simulator.exchange.get_variable_handle(
state, "Zone Air Temperature", "SPACE1-1"
)
_avg["T2"] = simulator.exchange.get_variable_handle(
state, "Zone Air Temperature", "SPACE2-1"
)
_avg["T3"] = simulator.exchange.get_variable_handle(
state, "Zone Air Temperature", "SPACE3-1"
)
_avg["T4"] = simulator.exchange.get_variable_handle(
state, "Zone Air Temperature", "SPACE4-1"
)
_avg["T5"] = simulator.exchange.get_variable_handle(
state, "Zone Air Temperature", "SPACE5-1"
)
_avg["vol1"] = simulator.exchange.get_internal_variable_value(
state,
simulator.exchange.get_internal_variable_handle(
state, "Zone Air Volume", "SPACE1-1"
),
)
_avg["vol2"] = simulator.exchange.get_internal_variable_value(
state,
simulator.exchange.get_internal_variable_handle(
state, "Zone Air Volume", "SPACE2-1"
),
)
_avg["vol3"] = simulator.exchange.get_internal_variable_value(
state,
simulator.exchange.get_internal_variable_handle(
state, "Zone Air Volume", "SPACE3-1"
),
)
_avg["vol4"] = simulator.exchange.get_internal_variable_value(
state,
simulator.exchange.get_internal_variable_handle(
state, "Zone Air Volume", "SPACE4-1"
),
)
_avg["vol5"] = simulator.exchange.get_internal_variable_value(
state,
simulator.exchange.get_internal_variable_handle(
state, "Zone Air Volume", "SPACE5-1"
),
)
# we only need to get the variable handles and zone volumes once, so we can set this flag to False
_avg["need_handles"] = False
# now we can compute the average zone air temperature using the variable handles and zone volumes
T1 = simulator.exchange.get_variable_value(state, _avg["T1"])
T2 = simulator.exchange.get_variable_value(state, _avg["T2"])
T3 = simulator.exchange.get_variable_value(state, _avg["T3"])
T4 = simulator.exchange.get_variable_value(state, _avg["T4"])
T5 = simulator.exchange.get_variable_value(state, _avg["T5"])
# compute the weighted average of the zone air temperatures using the zone volumes as weights
num = (
T1 * _avg["vol1"]
+ T2 * _avg["vol2"]
+ T3 * _avg["vol3"]
+ T4 * _avg["vol4"]
+ T5 * _avg["vol5"]
)
den = _avg["vol1"] + _avg["vol2"] + _avg["vol3"] + _avg["vol4"] + _avg["vol5"]
_avg["value"] = num / den
Log avg_zone_temperature with CSVLoggerο
To record _avg["value"] into a CSV file, wrap the environment with a custom LoggerWrapper subclass that overrides calculate_custom_metrics, then apply CSVLogger. The value will be saved per-timestep to custom_metrics.csv inside the episode folder.
[ ]:
from sinergym.utils.wrappers import CSVLogger, LoggerWrapper
# define a custom LoggerWrapper to log the average zone temperature computed by the _avg_zone_timestep callback into CSVLogger's custom_metrics.csv
class AvgTempLoggerWrapper(LoggerWrapper):
"""Custom LoggerWrapper that records the volume-weighted average zone temperature
computed by the _avg_zone_timestep callback into CSVLogger's custom_metrics.csv."""
def __init__(self, env):
super().__init__(env)
self.custom_variables = ['avg_zone_temperature']
def calculate_custom_metrics(self, obs, action, reward, info, terminated, truncated):
return [_avg['value']]
# Now we can wrap our environment with the AvgTempLoggerWrapper and CSVLogger to log the average zone temperature at each timestep into a custom_metrics.csv file
env = AvgTempLoggerWrapper(env)
env = CSVLogger(env)
print('Environment wrapped with AvgTempLoggerWrapper and CSVLogger.')
[WRAPPER LoggerWrapper] (INFO) : Wrapper initialized.
[WRAPPER CSVLogger] (INFO) : Wrapper initialized.
Environment wrapped with AvgTempLoggerWrapper and CSVLogger.
Register callbacks to environmentο
We provide an API in the env called env.register_callback. The callback name should match the runtime method. Otherwise, the simulator will throw a ValueError. You can clear the callbacks using clear_callbacks.
[4]:
env.get_wrapper_attr('register_callback')(
'callback_end_zone_timestep_before_zone_reporting', _avg_zone_timestep
)
# print the current callbacks
print('The current callbacks are:: {}'.format(env.get_wrapper_attr('callbacks')))
[SIMULATOR] (INFO) : Registered custom callback 'callback_end_zone_timestep_before_zone_reporting' successfully.
The current callbacks are:: {'callback_end_zone_timestep_before_zone_reporting': ['_avg_zone_timestep']}
[5]:
for i in range(1):
obs, info = env.reset()
rewards = []
truncated = terminated = False
current_month = 0
while not (terminated or truncated):
a = env.action_space.sample()
obs, reward, terminated, truncated, info = env.step(a)
rewards.append(reward)
if info['month'] != current_month: # display results every month
current_month = info['month']
print('Reward: ', sum(rewards), info)
# close() flushes all CSVLogger files (including custom_metrics.csv) for the last episode
env.close()
#----------------------------------------------------------------------------------------------#
[ENVIRONMENT] (INFO) : Starting a new episode.
[ENVIRONMENT] (INFO) : Episode 1: Eplus-5zone-hot-continuous-stochastic-v1
#----------------------------------------------------------------------------------------------#
[MODEL] (INFO) : Episode directory created.
[MODEL] (INFO) : Weather file USA_AZ_Davis-Monthan.AFB.722745_TMY3.epw used.
[MODEL] (INFO) : Adapting weather to building model.
[MODEL] (INFO) : Weather noise applied to columns: ['Dry Bulb Temperature', 'Relative Humidity']
[ENVIRONMENT] (INFO) : Saving episode output path in /workspaces/sinergym/examples/Eplus-5zone-hot-continuous-stochastic-v1-res16/episode-1/output.
[SIMULATOR] (INFO) : handlers initialized.
[SIMULATOR] (INFO) : handlers are ready.
[SIMULATOR] (INFO) : System is ready.
[ENVIRONMENT] (INFO) : Episode 1 started.
Reward: -0.04287902203346494 {'time_elapsed(hours)': 0.5, 'month': 1, 'day': 1, 'hour': 0, 'is_raining': False, 'action': [16.848060607910156, 26.069730758666992], 'timestep': 1, 'energy_term': -0.005897377870609513, 'comfort_term': -0.03698164416285543, 'energy_penalty': -117.94755741219025, 'comfort_penalty': -0.07396328832571086, 'total_power_demand': 117.94755741219025, 'total_temperature_violation': 0.07396328832571086, 'reward_weight': 0.5}
Simulation Progress [Episode 1]: 10%|β | 10/100 [00:02<00:26, 3.41%/s, 10% completed] Reward: -1480.2231924196765 {'time_elapsed(hours)': 744.25, 'month': 2, 'day': 1, 'hour': 0, 'is_raining': False, 'action': [20.97361946105957, 24.608291625976562], 'timestep': 2976, 'energy_term': -0.09904933431744359, 'comfort_term': 0.0, 'energy_penalty': -1980.9866863488717, 'comfort_penalty': 0, 'total_power_demand': 1980.9866863488717, 'total_temperature_violation': 0, 'reward_weight': 0.5}
Simulation Progress [Episode 1]: 17%|ββ | 17/100 [00:04<00:20, 4.05%/s, 17% completed]Reward: -2646.2258305344885 {'time_elapsed(hours)': 1416.375, 'month': 3, 'day': 1, 'hour': 0, 'is_raining': False, 'action': [14.753031730651855, 29.141185760498047], 'timestep': 5664, 'energy_term': -0.005897377870609513, 'comfort_term': 0.0, 'energy_penalty': -117.94755741219025, 'comfort_penalty': 0, 'total_power_demand': 117.94755741219025, 'total_temperature_violation': 0, 'reward_weight': 0.5}
Simulation Progress [Episode 1]: 26%|βββ | 26/100 [00:06<00:20, 3.66%/s, 26% completed]Reward: -4017.263983592177 {'time_elapsed(hours)': 2160.25, 'month': 4, 'day': 1, 'hour': 0, 'is_raining': False, 'action': [13.849004745483398, 27.945350646972656], 'timestep': 8640, 'energy_term': -0.005897377870609513, 'comfort_term': 0.0, 'energy_penalty': -117.94755741219025, 'comfort_penalty': 0, 'total_power_demand': 117.94755741219025, 'total_temperature_violation': 0, 'reward_weight': 0.5}
Simulation Progress [Episode 1]: 34%|ββββ | 34/100 [00:09<00:21, 3.12%/s, 34% completed]Reward: -5631.547684178791 {'time_elapsed(hours)': 2880.25, 'month': 5, 'day': 1, 'hour': 0, 'is_raining': False, 'action': [22.14965057373047, 26.190317153930664], 'timestep': 11520, 'energy_term': -0.1083457587514961, 'comfort_term': 0.0, 'energy_penalty': -2166.915175029922, 'comfort_penalty': 0, 'total_power_demand': 2166.915175029922, 'total_temperature_violation': 0, 'reward_weight': 0.5}
Simulation Progress [Episode 1]: 42%|βββββ | 42/100 [00:12<00:17, 3.32%/s, 42% completed]Reward: -7449.764901747518 {'time_elapsed(hours)': 3624.25, 'month': 6, 'day': 1, 'hour': 0, 'is_raining': False, 'action': [21.538137435913086, 29.186525344848633], 'timestep': 14496, 'energy_term': -0.03644927670930085, 'comfort_term': -0.21133938304344824, 'energy_penalty': -728.9855341860169, 'comfort_penalty': -0.4226787660868965, 'total_power_demand': 728.9855341860169, 'total_temperature_violation': 0.4226787660868965, 'reward_weight': 0.5}
Simulation Progress [Episode 1]: 51%|βββββ | 51/100 [00:14<00:18, 2.60%/s, 51% completed]Reward: -8466.010635863455 {'time_elapsed(hours)': 4344.25, 'month': 7, 'day': 1, 'hour': 0, 'is_raining': False, 'action': [18.533937454223633, 24.87399673461914], 'timestep': 17376, 'energy_term': -0.0360937122820931, 'comfort_term': 0.0, 'energy_penalty': -721.874245641862, 'comfort_penalty': 0, 'total_power_demand': 721.874245641862, 'total_temperature_violation': 0, 'reward_weight': 0.5}
Simulation Progress [Episode 1]: 59%|ββββββ | 59/100 [00:17<00:14, 2.85%/s, 59% completed]Reward: -9575.308902929375 {'time_elapsed(hours)': 5088.25, 'month': 8, 'day': 1, 'hour': 0, 'is_raining': False, 'action': [18.66809844970703, 27.539033889770508], 'timestep': 20352, 'energy_term': -0.04008009651105924, 'comfort_term': -0.09550297102558858, 'energy_penalty': -801.6019302211847, 'comfort_penalty': -0.19100594205117716, 'total_power_demand': 801.6019302211847, 'total_temperature_violation': 0.19100594205117716, 'reward_weight': 0.5}
Simulation Progress [Episode 1]: 67%|βββββββ | 67/100 [00:20<00:10, 3.03%/s, 67% completed]Reward: -10673.964106430458 {'time_elapsed(hours)': 5832.25, 'month': 9, 'day': 1, 'hour': 0, 'is_raining': False, 'action': [20.86484146118164, 28.4190673828125], 'timestep': 23328, 'energy_term': -0.030015891611737705, 'comfort_term': -0.18425733973388425, 'energy_penalty': -600.3178322347541, 'comfort_penalty': -0.3685146794677685, 'total_power_demand': 600.3178322347541, 'total_temperature_violation': 0.3685146794677685, 'reward_weight': 0.5}
Simulation Progress [Episode 1]: 76%|ββββββββ | 76/100 [00:22<00:06, 3.74%/s, 76% completed]Reward: -11840.48791261703 {'time_elapsed(hours)': 6552.25, 'month': 10, 'day': 1, 'hour': 0, 'is_raining': False, 'action': [21.76607322692871, 29.912328720092773], 'timestep': 26208, 'energy_term': -0.03488829934324132, 'comfort_term': 0.0, 'energy_penalty': -697.7659868648265, 'comfort_penalty': 0, 'total_power_demand': 697.7659868648265, 'total_temperature_violation': 0, 'reward_weight': 0.5}
Simulation Progress [Episode 1]: 84%|βββββββββ | 84/100 [00:25<00:05, 2.75%/s, 84% completed]Reward: -13412.57832706892 {'time_elapsed(hours)': 7296.25, 'month': 11, 'day': 1, 'hour': 0, 'is_raining': False, 'action': [15.970210075378418, 26.904888153076172], 'timestep': 29184, 'energy_term': -0.010556678414792702, 'comfort_term': 0.0, 'energy_penalty': -211.13356829585402, 'comfort_penalty': 0, 'total_power_demand': 211.13356829585402, 'total_temperature_violation': 0, 'reward_weight': 0.5}
Simulation Progress [Episode 1]: 92%|ββββββββββ| 92/100 [00:27<00:02, 3.95%/s, 92% completed]Reward: -14757.808355656342 {'time_elapsed(hours)': 8016.25, 'month': 12, 'day': 1, 'hour': 0, 'is_raining': False, 'action': [12.337260246276855, 24.085851669311523], 'timestep': 32064, 'energy_term': -0.005897377870609513, 'comfort_term': 0.0, 'energy_penalty': -117.94755741219025, 'comfort_penalty': 0, 'total_power_demand': 117.94755741219025, 'total_temperature_violation': 0, 'reward_weight': 0.5}
[WRAPPER CSVLogger] (INFO) : Environment closed, data updated in monitor and progress.csv.
Simulation Progress [Episode 1]: 100%|ββββββββββ| 100/100 [00:33<00:00, 2.99%/s, 100% completed]
[ENVIRONMENT] (INFO) : Environment closed. [Eplus-5zone-hot-continuous-stochastic-v1]
[ ]: