From 4eefdc61089c9480b8f405e2c80ad04d3c3133ec Mon Sep 17 00:00:00 2001 From: trevorhardy Date: Fri, 2 Feb 2024 10:52:09 -0800 Subject: [PATCH] Add callback documentation Moved and expanded the existing callback documentation from the Pythonic page into a page of its own. --- docs/callbacks.md | 116 +++++++++++++++++++++++++++++++++++++ docs/pythonic-interface.md | 36 +----------- mkdocs.yml | 1 + 3 files changed, 118 insertions(+), 35 deletions(-) create mode 100644 docs/callbacks.md diff --git a/docs/callbacks.md b/docs/callbacks.md new file mode 100644 index 00000000..ddab0530 --- /dev/null +++ b/docs/callbacks.md @@ -0,0 +1,116 @@ +# Implementing HELICS Callbacks in Python + +There are several HELICS functionalities that allow for the definition of custom behavior through the use of custom callback functions. Two specific examples are the definition of the filter behavior when implementing a filter federate and the other is the response to a custom query. In both cases custom code needs to be written to define behavior when HELICS needs to perform a specific action (filter a message, respond to a query). There are a few steps to implement callbacks in PyHELICS + +## Define User Data +The callback function generally exists outside the scope of other code and thus, if the functionality defined in the callback needs data from, say, the federate, that data has to be carried into the callback through a custom class generically called "user data". This user data is defined as a class that is instantiated and filled as a part of federate operation. + +```python +# Store what ever data you'd like. +# A reference to this object is passed to the filter callback. +# You don't need to use this if you don't want to. +class UserData(object): + def __init__(self, iteration_count = None): + self.pi = 3.14 + self.e = 2.718 + self.interation_count = iteration_count +``` + +## Define the Callback +This is where the real C-to-Python magic happens, using the "cffi" library. As the HELICS library being used is C-based, there are several things that look weird in the Python world that we have to do to properly hook into that library. The biggest of these is adding a Python decorator to the callback in the form of a string that contains the C signature of the callback being implemented. For example: + +```python +# Filter callback +@h.ffi.callback("void logger(HelicsMessage, void* userData)") +def filter_callback(mess, userData): + # Filter operation code here + +# Query callback +@h.ffi.callback("void query(const char *query, int querySize, HelicsQueryBuffer buffer, void *user_data)") +def query_callback(query_ptr, size:int, query_buffer_ptr, user_data): + query_str = h.ffi.string(query_ptr,size).decode() + query_buffer = h.HelicsQueryBuffer(query_buffer_ptr) + # Query operation code here + +``` + +In the case of the query callback, you can see there are two other bits that need to be added in. + + 1 - The query string is passed in as a C pointer. If you've only worked in Python, you might wonder what a "pointer" is. So does Python; the "cffi" library is used to translate the data the pointer is referencing into something Python recognizes as a string. + 2 - The query response that will be created by the callback function must be put into a pre-constructed databuffer that is passed in when the callback is made ("HelicsQueryBuffer buffer" in the above C signature). HELICS will read this buffer to get the response of the callback. Again, pointers are involved so we use the "cffi" library to make them something Python can deal with. + +## Register the Callback +Last step, with the callback defined we need to "register" it so that HELICS knows which function to call when its time to execute the callback. This is done as part of setting up your federate and should be done as early as possible so that the federate is able to respond to any callbacks that come in early in the life of a federate. + + + +```python +# Filter callback federate code +def main(): + ... + f1 = h.helicsFederateRegisterFilter(fFed, h.HELICS_FILTER_TYPE_CUSTOM, "filter1") + userdata = UserData(iteration_count = 10) + user_data_handle = h.ffi.new_handle(userdata) + h.helicsFilterSetCustomCallback(f1, filter_callback, user_data_handle) + + +# Query callback federate code +def main(): + ... + fed = h.helicsCreateValueFederateFromConfig("math_fed.json") + user_data = UserData(iteration_count = 10) + user_data_handle = h.ffi.new_handle(user_data) + h.helicsFederateSetQueryCallback(fed, query_callback, user_data_handle) + +``` + +In both cases, the user data is defined, a "handle" to the user data is created, and the callback functions are registered using specific HELICS APIs. + +## Complete Examples +Here are the full code for completeness sake. As of this writing, there is not a running example for the filter callback but there is one for the [query callback](https://github.com/GMLC-TDC/HELICS-Examples/blob/53bece298f9be952002e2f9201f24922fabc73b4/user_guide_examples/advanced/advanced_connector/interface_creation/Charger.py) in the [HELICS Examples repository](https://github.com/GMLC-TDC/HELICS-Examples). + + +### Filter Federate Code +``` python + +class UserData(object): + def __init__(self, iteration_count = None): + self.pi = 3.14 + self.e = 2.718 + self.interation_count = iteration_count + +@h.ffi.callback("void logger(HelicsMessage, void* userData)") +def filter_callback(mess, userData): + # Filter operation code here + + +def main(): + fed = h.helicsCreateValueFederateFromConfig("math_fed.json") + f1 = h.helicsFederateRegisterFilter(fed, h.HELICS_FILTER_TYPE_CUSTOM, "filter1") + userdata = UserData(iteration_count = 10) + user_data_handle = h.ffi.new_handle(userdata) + h.helicsFilterSetCustomCallback(f1, filter_callback, user_data_handle) +``` + +### Query Response Code +```Python +class UserData(object): + def __init__(self, iteration_count = None): + self.pi = 3.14 + self.e = 2.718 + self.interation_count = iteration_count + +@h.ffi.callback("void query(const char *query, int querySize, HelicsQueryBuffer buffer, void *user_data)") +def query_callback(query_ptr, size:int, query_buffer_ptr, user_data): + query_str = h.ffi.string(query_ptr,size).decode() + query_buffer = h.HelicsQueryBuffer(query_buffer_ptr) + # Query operation code here + + +def main(): + fed = h.helicsCreateValueFederateFromConfig("math_fed.json") + user_data = UserData(iteration_count = 10) + user_data_handle = h.ffi.new_handle(user_data) + h.helicsFederateSetQueryCallback(fed, query_callback, user_data_handle) + +``` \ No newline at end of file diff --git a/docs/pythonic-interface.md b/docs/pythonic-interface.md index 70104dbf..01bff8da 100644 --- a/docs/pythonic-interface.md +++ b/docs/pythonic-interface.md @@ -52,38 +52,4 @@ print("""mFed.subscriptions["TestFederate/publication"].bytes: """, mFed.subscri assert mFed.subscriptions["TestFederate/publication"].bytes == b"first-time" print("Exiting...") -``` - -### Using a Filter Callbacks - -Here is a annotated snippet of how to use custom filter callbacks in Python. - -```python - -# Store what ever data you'd like. A reference to this object is passed to the filter callback. You don't need to use this if you don't want to. -class UserData(object): - def __init__(self, x = None): - self.x = x - -# Create the filter callback function -# This function is called when the message is transmitted -@h.ffi.callback("void logger(HelicsMessage, void* userData)") -def filterCallback(mess, userData): - m = h.HelicsMessage(mess) - time = h.helicsMessageGetTime(m) - # Change time here however you like. The following is an example of delaying it by 2.5 seconds. - h.helicsMessageSetTime(m, time + 2.5) - - -# your code -def main(): - ... - # Register a `HELICS_FILTER_TYPE_CUSTOM` type filter and store in `f1` - f1 = h.helicsFederateRegisterFilter(fFed, h.HELICS_FILTER_TYPE_CUSTOM, "filter1") - # optional user data if you need to. - userdata = UserData() # or userdata = None if you don't want to use it - # Create a handle to the user data - handle = h.ffi.new_handle(userdata) - # Set on `f1` the `filterCallback` function as the filter callback and pass handle to the userdata. - h.helicsFilterSetCustomCallback(f1, filterCallback, handle) -``` +``` \ No newline at end of file diff --git a/mkdocs.yml b/mkdocs.yml index d1d8b0d6..0404f9d9 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -9,6 +9,7 @@ nav: - Usage: usage.md - API: api/index.md - Pythonic API: pythonic-interface.md + - Callbacks: callbacks.md - CLI Interface: cli-interface.md - Web Interface (experimental): web-interface.md - Migration v2 -> v3: migration-helics2-helics3.md