Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/pythonpackage.yml
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ jobs:
CIBW_BEFORE_ALL_LINUX: >
mkdir /boost &&
pushd /boost &&
curl -L -o boost_1_76_0.tar.bz2 'https://sourceforge.net/projects/boost/files/boost/1.76.0/boost_1_76_0.tar.bz2' &&
curl -L -o boost_1_76_0.tar.bz2 'https://sourceforge.net/projects/boost/files/boost/1.76.0/boost_1_76_0.tar.bz2' &&
tar xf boost_1_76_0.tar.bz2 &&
popd
CIBW_ENVIRONMENT_LINUX: "${{ matrix.arch == 'aarch64' && 'BOOST_ROOT=/boost/boost_1_76_0 CMAKE_ARGS=-DCMAKE_POLICY_VERSION_MINIMUM=3.5' || 'BOOST_ROOT=/boost/boost_1_76_0' }}"
Expand All @@ -86,7 +86,7 @@ jobs:
- uses: actions/checkout@v7
with:
fetch-depth: 0 # for setuptools_scm to find tags

- name: Install boost
run:
mkdir /tmp/boost &&
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/update-helics.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7

- name: Set git config for commits
run: |
git config --global user.name "github-actions[bot]"
Expand Down
74 changes: 74 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
exclude: ^(helics-src/|helics-src\.downloaded/)

ci:
autoupdate_branch: "main"

repos:
- repo: https://github.com/psf/black-pre-commit-mirror
rev: 26.5.1
hooks:
- id: black
args:
[
"--line-length=100",
"--target-version=py311",
"--target-version=py312",
"--target-version=py313",
"--target-version=py314",
]

- repo: https://github.com/asottile/blacken-docs
rev: 1.20.0
hooks:
- id: blacken-docs
args:
[
"--line-length=100",
"--target-version=py311",
"--target-version=py312",
"--target-version=py313",
"--target-version=py314",
]
additional_dependencies: [black==26.5.1]

- repo: https://github.com/Lucas-C/pre-commit-hooks
rev: v1.5.6
hooks:
- id: remove-tabs

- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v6.0.0
hooks:
- id: check-added-large-files
- id: check-ast
- id: check-case-conflict
- id: check-json
- id: check-merge-conflict
- id: check-shebang-scripts-are-executable
- id: check-symlinks
- id: check-toml
- id: check-yaml
args: ["--allow-multiple-documents"]
- id: debug-statements
- id: detect-private-key
- id: end-of-file-fixer
- id: mixed-line-ending
- id: trailing-whitespace

- repo: https://github.com/abravalheri/validate-pyproject
rev: v0.25
hooks:
- id: validate-pyproject
additional_dependencies: ["validate-pyproject-schema-store[all]"]

- repo: https://github.com/mgedmin/check-manifest
rev: "0.51"
hooks:
- id: check-manifest
stages: [manual]
args: ["--no-build-isolation"]
additional_dependencies:
- build
- ninja
- scikit-build-core
- setuptools-scm
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ To use the project:

```python
import helics as h

h.helicsGetVersion()
```

Expand Down Expand Up @@ -54,4 +55,4 @@ $ python -c "import helics as h; import json; print(json.dumps(h.helicsGetSystem
```
# Citation
General citation for HELICS:
T. Hardy, B. Palmintier, P. Top, D. Krishnamurthy and J. Fuller, "HELICS: A Co-Simulation Framework for Scalable Multi-Domain Modeling and Analysis," in IEEE Access, doi: 10.1109/ACCESS.2024.3363615, available at [https://ieeexplore.ieee.org/document/10424422](https://ieeexplore.ieee.org/document/10424422/)
T. Hardy, B. Palmintier, P. Top, D. Krishnamurthy and J. Fuller, "HELICS: A Co-Simulation Framework for Scalable Multi-Domain Modeling and Analysis," in IEEE Access, doi: 10.1109/ACCESS.2024.3363615, available at [https://ieeexplore.ieee.org/document/10424422](https://ieeexplore.ieee.org/document/10424422/)
56 changes: 32 additions & 24 deletions docs/callbacks.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,14 @@
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.
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.
# 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:
def __init__(self, iteration_count = None):
def __init__(self, iteration_count=None):
self.pi = 3.14
self.e = 2.718
self.interation_count = iteration_count
Expand All @@ -24,19 +24,23 @@ This is where the real C-to-Python magic happens, using the "cffi" library. As t
@h.ffi.callback("void logger(HelicsMessage, void* userData)")
def filter_callback(mess, userData):
# Filter operation code here
pass


# 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()
@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

pass
```

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.

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
Expand All @@ -49,7 +53,7 @@ Last step, with the callback defined we need to "register" it so that HELICS kno
def main():
...
f1 = h.helicsFederateRegisterFilter(fFed, h.HELICS_FILTER_TYPE_CUSTOM, "filter1")
userdata = UserData(iteration_count = 10)
userdata = UserData(iteration_count=10)
user_data_handle = h.ffi.new_handle(userdata)
h.helicsFilterSetCustomCallback(f1, filter_callback, user_data_handle)

Expand All @@ -58,10 +62,9 @@ def main():
def main():
...
fed = h.helicsCreateValueFederateFromConfig("math_fed.json")
user_data = UserData(iteration_count = 10)
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.
Expand All @@ -72,44 +75,49 @@ Here are the full code for completeness sake. As of this writing, there is not a

### Filter Federate Code
``` python

class UserData:
def __init__(self, iteration_count = None):
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

pass


def main():
fed = h.helicsCreateValueFederateFromConfig("math_fed.json")
f1 = h.helicsFederateRegisterFilter(fed, h.HELICS_FILTER_TYPE_CUSTOM, "filter1")
userdata = UserData(iteration_count = 10)
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:
def __init__(self, iteration_count = None):
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()

@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

pass


def main():
fed = h.helicsCreateValueFederateFromConfig("math_fed.json")
user_data = UserData(iteration_count = 10)
user_data = UserData(iteration_count=10)
user_data_handle = h.ffi.new_handle(user_data)
h.helicsFederateSetQueryCallback(fed, query_callback, user_data_handle)

Expand Down
4 changes: 3 additions & 1 deletion docs/examples/10-federates-1-topic/pisender.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,9 @@ def main(n):
fed.request_next_step()

fed.publications[f"globaltopic"].publish(value)
print(f"{federate_name}: Sending value pi = {value} at time {fed.current_time} on globaltopic")
print(
f"{federate_name}: Sending value pi = {value} at time {fed.current_time} on globaltopic"
)

# Computing user needs
time.sleep(float(n) * (1 + (0.5 - random.random()) / 10))
Expand Down
14 changes: 10 additions & 4 deletions docs/examples/website_pythonic_interface/pythonic_interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,19 +47,25 @@
# Read data as bytes
mFed.publications["TestFederate/publication"].publish(b"bytes")
assert mFed.request_time(2.0) == 1.0
print(f'mFed.subscriptions["TestFederate/publication"].bytes: {mFed.subscriptions["TestFederate/publication"].bytes}')
print(
f'mFed.subscriptions["TestFederate/publication"].bytes: {mFed.subscriptions["TestFederate/publication"].bytes}'
)
assert mFed.subscriptions["TestFederate/publication"].bytes == b"bytes"

# Read data as a string
mFed.publications["TestFederate/publication"].publish("string")
mFed.request_time(3.0)
print(f'mFed.subscriptions["TestFederate/publication"].string: {mFed.subscriptions["TestFederate/publication"].string}')
print(
f'mFed.subscriptions["TestFederate/publication"].string: {mFed.subscriptions["TestFederate/publication"].string}'
)
assert mFed.subscriptions["TestFederate/publication"].string == "string"

# Read data as a value and let HELICS figure out the right data type
mFed.publications["TestFederate/publication"].publish("value")
mFed.request_time(4.0)
print(f'mFed.subscriptions["TestFederate/publication"].value: {mFed.subscriptions["TestFederate/publication"].value}')
print(
f'mFed.subscriptions["TestFederate/publication"].value: {mFed.subscriptions["TestFederate/publication"].value}'
)
assert mFed.subscriptions["TestFederate/publication"].value == "value"

print("Example complete")
print("Example complete")
2 changes: 1 addition & 1 deletion docs/examples/website_usage/broker.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,4 @@
broker = h.helicsCreateBroker("zmq", "", "-f 2 --loglevel=trace")

while h.helicsBrokerIsConnected(broker):
time.sleep(1)
time.sleep(1)
2 changes: 1 addition & 1 deletion docs/examples/website_usage/federate_1.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,4 +32,4 @@
time.sleep(1)

h.helicsFederateDisconnect(vfed)
h.helicsFederateDestroy(vfed)
h.helicsFederateDestroy(vfed)
2 changes: 1 addition & 1 deletion docs/examples/website_usage/federate_2.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,4 @@
print("RECEIVER: Received value = {} at time {} from SENDER".format(value, currenttime))

h.helicsFederateDisconnect(vfed)
h.helicsFederateDestroy(vfed)
h.helicsFederateDestroy(vfed)
14 changes: 10 additions & 4 deletions docs/pythonic-interface.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,20 +53,26 @@ print("Publishing string and reading as various data types")
# Read data as bytes
mFed.publications["TestFederate/publication"].publish(b"bytes")
assert mFed.request_time(2.0) == 1.0
print(f'mFed.subscriptions["TestFederate/publication"].bytes: {mFed.subscriptions["TestFederate/publication"].bytes}')
print(
f'mFed.subscriptions["TestFederate/publication"].bytes: {mFed.subscriptions["TestFederate/publication"].bytes}'
)
assert mFed.subscriptions["TestFederate/publication"].bytes == b"bytes"

# Read data as a string
mFed.publications["TestFederate/publication"].publish("string")
mFed.request_time(3.0)
print(f'mFed.subscriptions["TestFederate/publication"].string: {mFed.subscriptions["TestFederate/publication"].string}')
print(
f'mFed.subscriptions["TestFederate/publication"].string: {mFed.subscriptions["TestFederate/publication"].string}'
)
assert mFed.subscriptions["TestFederate/publication"].string == "string"

# Read data as a value and let HELICS figure out the right data type
mFed.publications["TestFederate/publication"].publish("value")
mFed.request_time(4.0)
print(f'mFed.subscriptions["TestFederate/publication"].value: {mFed.subscriptions["TestFederate/publication"].value}')
print(
f'mFed.subscriptions["TestFederate/publication"].value: {mFed.subscriptions["TestFederate/publication"].value}'
)
assert mFed.subscriptions["TestFederate/publication"].value == "value"

print("Example complete")
```
```
22 changes: 18 additions & 4 deletions helics/_build.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,11 @@
)
PYHELICS_INSTALL = os.getenv("PYHELICS_INSTALL", HELICS_INSTALL)
if not os.path.isdir(PYHELICS_INSTALL):
warnings.warn("PYHELICS_INSTALL ({}) is not a directory. Using DEFAULT_INSTALL ({})".format(PYHELICS_INSTALL, CURRENT_INSTALL))
warnings.warn(
"PYHELICS_INSTALL ({}) is not a directory. Using DEFAULT_INSTALL ({})".format(
PYHELICS_INSTALL, CURRENT_INSTALL
)
)
PYHELICS_INSTALL = CURRENT_INSTALL

files = [
Expand Down Expand Up @@ -76,7 +80,11 @@ def _load_library():
for file in os.listdir(os.path.join(PYHELICS_INSTALL, "bin")):
if "helics" in file and file.endswith(".dll"):
try:
logger.debug("dlopen helics library in {}".format(os.path.join(PYHELICS_INSTALL, "bin", file)))
logger.debug(
"dlopen helics library in {}".format(
os.path.join(PYHELICS_INSTALL, "bin", file)
)
)
lib = ffi.dlopen(os.path.join(PYHELICS_INSTALL, "bin", file))
break
except OSError as _:
Expand Down Expand Up @@ -112,7 +120,9 @@ def _load_library():
else:
for file in reversed(sorted(os.listdir(lib_folder), key=len)):
if "helicsSharedLibd." in file or "libhelicsd." in file and file.endswith(".dylib"):
logger.debug("dlopen debug helics library in {}".format(os.path.join(lib_folder, file)))
logger.debug(
"dlopen debug helics library in {}".format(os.path.join(lib_folder, file))
)
lib = ffi.dlopen(os.path.join(lib_folder, file))
break
else:
Expand Down Expand Up @@ -141,7 +151,11 @@ def _load_library():
for file in reversed(sorted(os.listdir(lib_folder), key=len)):
if "helicsSharedLibd." in file or "libhelicsd." in file and file.endswith(".so"):
try:
logger.debug("dlopen debug helics library in {}".format(os.path.join(lib_folder, file)))
logger.debug(
"dlopen debug helics library in {}".format(
os.path.join(lib_folder, file)
)
)
lib = ffi.dlopen(os.path.join(lib_folder, file))
break
except:
Expand Down
Loading
Loading