Skip to content

Commit d6d6ea2

Browse files
committed
update
2 parents fbc6799 + 9cef288 commit d6d6ea2

12 files changed

+584
-0
lines changed
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
This example federation is useful for testing filter federate performance and demonstrating its basic configuration. source_sink.py includes data collection and graphing functionality that displays the histogram of the transit times of HELICS messages to and from the echo federate and can include processing by the filter federate if included in the federation.
2+
3+
It also includes in echo.py, alternative implementations of the echo function, some of which work correctly in this federation and some do not. Thus, it is a good example of how imprecise implementations of simple federates can result in unexpected behavior especially when using rerouting filters and filter federates.
4+
5+
For more details examine the comments in echo.py to see how it has implemented its a message echoing function.
6+

python/no_filter_benchmark/echo.py

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
# -*- coding: utf-8 -*-
2+
"""
3+
Created on 5/3/2021
4+
5+
Test federate for evaluating performance of HELICS filter timing. Takes
6+
incoming messages and sends them back to their sender with an unaltered payload.
7+
8+
@author: Trevor Hardy
9+
10+
"""
11+
12+
import helics as h
13+
import logging
14+
import time
15+
16+
logger = logging.getLogger(__name__)
17+
logger.addHandler(logging.StreamHandler())
18+
logger.setLevel(logging.DEBUG)
19+
20+
def destroy_federate(fed):
21+
'''
22+
As part of ending a HELICS co-simulation it is good housekeeping to
23+
formally destroy a federate. Doing so informs the rest of the
24+
federation that it is no longer a part of the co-simulation and they
25+
should proceed without it (if applicable). Generally this is done
26+
when the co-simulation is complete and all federates end execution
27+
at more or less the same wall-clock time.
28+
29+
:param fed: Federate to be destroyed
30+
:return: (none)
31+
'''
32+
status = h.helicsFederateFinalize(fed)
33+
h.helicsFederateFree(fed)
34+
h.helicsCloseLibrary()
35+
logger.info('Federate finalized')
36+
37+
38+
39+
if __name__ == "__main__":
40+
41+
42+
############## Registering federate from json ##########################
43+
fed = h.helicsCreateCombinationFederateFromConfig("echo_config.json")
44+
federate_name = h.helicsFederateGetName(fed)
45+
endid = h.helicsFederateGetEndpointByIndex(fed, 0)
46+
endid_name = h.helicsEndpointGetName(endid)
47+
48+
49+
############## Entering Execution Mode ##################################
50+
h.helicsFederateEnterExecutingMode(fed)
51+
logger.info('Entered HELICS execution mode')
52+
53+
hours = 1
54+
total_interval = int(60 * 60 * hours)
55+
update_interval = int(h.helicsFederateGetTimeProperty(
56+
fed,
57+
h.HELICS_PROPERTY_TIME_PERIOD))
58+
grantedtime = 0
59+
60+
61+
# Blocking call for a time request at simulation time 0
62+
initial_time = total_interval
63+
logger.debug(f'Requesting initial time {initial_time}')
64+
grantedtime = h.helicsFederateRequestTime(fed, initial_time )
65+
logger.debug(f'Granted time {grantedtime}')
66+
67+
68+
default_dest = h.helicsEndpointGetDefaultDestination(endid)
69+
logger.debug(f'Default destination: {default_dest}')
70+
71+
########## Main co-simulation loop ########################################
72+
# As long as granted time is in the time range to be simulated...
73+
while grantedtime < total_interval:
74+
75+
# Time request for the next physical interval to be simulated
76+
logger.debug(f'Requesting time {total_interval}\n')
77+
grantedtime = h.helicsFederateRequestTime (fed, total_interval )
78+
logger.debug(f'Granted time {grantedtime}')
79+
80+
if h.helicsEndpointHasMessage(endid):
81+
msg = h.helicsEndpointGetMessage(endid)
82+
# This is where things can get tricky. The combination of the reroute
83+
# filter and the echo federate make Setting the parameters of this
84+
# message complex. The simplest solution for the echo federate is to
85+
# make a copy of the payload from the original message and create a
86+
# new message with that same payload.
87+
# payload = h.helicsMessageGetString(msg)
88+
# h.helicsEndpointSendBytesTo(endid, payload.encode(), "")
89+
90+
# Alternatively, working with the same message object, changing the
91+
# original source to this echo favorites and point in the original
92+
# destination to the default destination for this federate's endpoint
93+
# will also work.
94+
h.helicsMessageSetOriginalDestination(msg, default_dest)
95+
h.helicsMessageSetOriginalSource(msg, endid_name)
96+
97+
98+
# In the case of this Federation, changing just the destination and
99+
# source does not work as expected. If you look at no_filter.py,
100+
# you can see that D filter determines the correct destination of
101+
# the message based on its original destination. Using the API calls
102+
# below, that parameter is not changed and thus when this message
103+
# is intercepted by the filter federate, it will send it to its
104+
# original destination, which is the echo federates endpoint. This
105+
# results in the message looping continuously through the echo
106+
# federate and the filter federate.
107+
# h.helicsMessageSetDestination(msg, default_dest)
108+
# h.helicsMessageSetSource(msg, endid_name)
109+
h.helicsEndpointSendMessage(endid, msg)
110+
logger.debug(f'Echoing message at time {grantedtime}')
111+
112+
113+
114+
115+
116+
117+
# Cleaning up HELICS stuff once we've finished the co-simulation.
118+
destroy_federate(fed)
119+
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
{
2+
"name": "echo",
3+
"core_name": "echo_core",
4+
"log_level": 1,
5+
"core_type": "zmq",
6+
"period": 0.0000001,
7+
"uninterruptible": false,
8+
"force_logging_flush": true,
9+
"terminate_on_error": true,
10+
"endpoints": [
11+
{
12+
"name": "echo/ep",
13+
"destination": "source_sink/ep",
14+
"global": true
15+
}
16+
]
17+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
{
2+
"name": "filter",
3+
"core_name": "filter_core",
4+
"log_level": 1,
5+
"core_type": "zmq",
6+
"uninterruptible": false,
7+
"terminate_on_error": true,
8+
"force_logging_flush": true,
9+
"endpoints": [
10+
{
11+
"name": "filter/ep",
12+
"global": true
13+
}
14+
],
15+
"filters": [
16+
{
17+
"name": "filterFed",
18+
"sourcetargets": [
19+
"source_sink/ep",
20+
"echo/ep"
21+
],
22+
"operation": "reroute",
23+
"properties": {
24+
"name": "newdestination",
25+
"value": "filter/ep"
26+
}
27+
}
28+
]
29+
}
Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
#!/usr/bin/env python3
2+
# -*- coding: utf-8 -*-
3+
"""
4+
Created on Monday May 3 2021
5+
6+
Filter that doesn't filter messages at all. I sends on all messages that are
7+
routed to it without modification. It uses the original destination of the
8+
message to determine where the message should be sent on to; the reroute filter
9+
changes the simple `destination` field when it reroutes the message to the
10+
filter federate.
11+
12+
@author: hard312 (Trevor Hardy)
13+
"""
14+
15+
import argparse
16+
import logging
17+
import pprint
18+
import os
19+
import sys
20+
21+
22+
import helics as h
23+
import random
24+
from operator import itemgetter
25+
26+
# Setting up logging
27+
logger = logging.getLogger(__name__)
28+
logger.setLevel(logging.DEBUG)
29+
30+
# Adding custom logging level "DATA" to use for putting
31+
# all the simulation data on. "DATA" is between "DEBUG"
32+
# and "NOTSET" in terms of priority.
33+
DATA_LEVEL_NUM = 5
34+
logging.addLevelName(DATA_LEVEL_NUM, "DATA")
35+
36+
37+
def data(self, message, *args, **kws):
38+
if self.isEnabledFor(DATA_LEVEL_NUM):
39+
self._log(DATA_LEVEL_NUM, message, args, **kws)
40+
41+
42+
logging.DATA = DATA_LEVEL_NUM
43+
logging.Logger.data = data
44+
45+
# Setting up pretty printing, mostly for debugging.
46+
pp = pprint.PrettyPrinter(indent=4, )
47+
48+
49+
50+
def destroy_federate(fed):
51+
'''
52+
As part of ending a HELICS co-simulation it is good housekeeping to
53+
formally destroy a federate. Doing so informs the rest of the
54+
federation that it is no longer a part of the co-simulation and they
55+
should proceed without it (if applicable). Generally this is done
56+
when the co-simulation is complete and all federates end execution
57+
at more or less the same wall-clock time.
58+
59+
:param fed: Federate to be destroyed
60+
:return: (none)
61+
'''
62+
status = h.helicsFederateFinalize(fed)
63+
h.helicsFederateFree(fed)
64+
h.helicsCloseLibrary()
65+
logger.info('Federate finalized')
66+
67+
68+
def configure_federate():
69+
fed = h.helicsCreateMessageFederateFromConfig("filter_config.json")
70+
federate_name = h.helicsFederateGetName(fed)
71+
logger.info(f'Created federate {federate_name}')
72+
73+
# Diagnostics to confirm JSON config correctly added the required
74+
# endpoints
75+
endid = h.helicsFederateGetEndpointByIndex(fed, 0)
76+
end_name = h.helicsEndpointGetName(endid)
77+
return fed, endid, end_name
78+
79+
80+
def run_cosim(fed, endid, end_name, args):
81+
# The event queue ("eq") is the master list of events that the filter
82+
# federates works on. In this simple filter federate, each event
83+
# will be a dictionary with a few parameters:
84+
# 'dest' - destination of message
85+
# 'time' - time when the message should be sent on to its
86+
# intended destination
87+
# 'payload' - content of the message being sent
88+
#
89+
# When eq is empty, there are no messages being
90+
# filtered by the federate. When there are events in the queue it
91+
# indicates the filter federate has messages it is holding onto
92+
# that it needs to forward on (at the indicated time).
93+
#
94+
eq = []
95+
96+
# sub = h.helicsFederateRegisterSubscription(fed, "Charger/EV1_voltage", "")
97+
98+
logger.info('Attempting to enter execution mode')
99+
h.helicsFederateEnterExecutingMode(fed)
100+
logger.info('Entered HELICS execution mode')
101+
102+
hours = 24 # one week
103+
total_interval = int(60 * 60 * hours)
104+
105+
# Blocking call for a time request at max simulation time
106+
# fake_max_time = int(h.HELICS_TIME_MAXTIME / 1000)
107+
fake_max_time = 100000000
108+
starttime = fake_max_time
109+
#starttime = 0
110+
logger.debug(f'Requesting initial time {starttime}')
111+
grantedtime = h.helicsFederateRequestTime(fed, starttime)
112+
logger.debug(f'Granted time {grantedtime}')
113+
114+
while grantedtime < total_interval:
115+
116+
# value = h.helicsInputGetString(sub)
117+
# logger.debug(f'Got message {value} from random sub at time {grantedtime}.')
118+
119+
# In HELICS, when multiple messages arrive at an endpoint they
120+
# queue up and are popped off one-by-one with the
121+
# "helicsEndpointHasMessage" API call. When that API doesn't
122+
# return a message, you've processed them all.
123+
while h.helicsEndpointHasMessage(endid):
124+
msg = h.helicsEndpointGetMessage(endid)
125+
msg_str = h.helicsMessageGetString(msg)
126+
source = h.helicsMessageGetOriginalSource(msg)
127+
dest = h.helicsMessageGetOriginalDestination(msg)
128+
time = h.helicsMessageGetTime(msg)
129+
logger.debug(f'Received message from endpoint {source}'
130+
f' to endpoint {dest}'
131+
f' for delivery at time {time}'
132+
f' with payload \"{msg_str}\"')
133+
h.helicsMessageSetDestination(msg, dest)
134+
h.helicsEndpointSendMessage(endid, msg)
135+
logger.debug(f'Sent message from endpoint {end_name}'
136+
f' appearing to come from {source}'
137+
f' to endpoint {dest}'
138+
f' at time {grantedtime}'
139+
f' with payload \"{msg_str}\"')
140+
logger.debug(f'Requesting time {fake_max_time}')
141+
grantedtime = h.helicsFederateRequestTime(fed, fake_max_time)
142+
143+
144+
def _auto_run(args):
145+
"""This function executes when the script is called as a stand-alone
146+
executable.
147+
148+
A more complete description of this code can be found in the
149+
docstring at the beginning of this file.
150+
151+
Args:
152+
'-a' or '--auto_run_dir' - Path of the auto_run folder
153+
that contains examples of the files used in this script. Used
154+
for development purposes as well as models/examples/documentation
155+
of the format and contents expected in said files
156+
157+
Returns:
158+
(none)
159+
"""
160+
fed, endid, end_name = configure_federate()
161+
run_cosim(fed, endid, end_name, args)
162+
destroy_federate(fed)
163+
164+
165+
if __name__ == '__main__':
166+
# TDH: This slightly complex mess allows lower importance messages
167+
# to be sent to the log file and ERROR messages to additionally
168+
# be sent to the console as well. Thus, when bad things happen
169+
# the user will get an error message in both places which,
170+
# hopefully, will aid in trouble-shooting.
171+
fileHandle = logging.FileHandler("Filter.log", mode='w')
172+
fileHandle.setLevel(logging.DEBUG)
173+
streamHandle = logging.StreamHandler(sys.stdout)
174+
streamHandle.setLevel(logging.ERROR)
175+
logging.basicConfig(level=logging.DEBUG,
176+
handlers=[fileHandle, streamHandle])
177+
178+
parser = argparse.ArgumentParser(description='Demo HELICS filter federate')
179+
# TDH: Have to do a little bit of work to generate a good default
180+
# path for the auto_run folder (where the development test data is
181+
# held.
182+
script_path = os.path.dirname(os.path.realpath(__file__))
183+
auto_run_dir = os.path.join(script_path)
184+
parser.add_argument('-a',
185+
'--auto_run_dir',
186+
nargs='?',
187+
default=script_path)
188+
args = parser.parse_args()
189+
_auto_run(args)
557 KB
Binary file not shown.
557 KB
Binary file not shown.

0 commit comments

Comments
 (0)