diff --git a/README.md b/README.md
index ff7ca33..72e7e54 100644
--- a/README.md
+++ b/README.md
@@ -2,8 +2,6 @@
CLI for running Hierarchical Engine for Large-scale Infrastructure Co-Simulations (HELICS).
-![](https://user-images.githubusercontent.com/1813121/118163708-8bd4f380-b3df-11eb-8e18-99e9de81836f.png)
-
- Supports configurations of federates using plugins
- Allows running of federation using a runner configuration
@@ -48,9 +46,16 @@ Usage: helics run [OPTIONS]
Run HELICS federation
Options:
- --path PATH
+ --path PATH Path to config.json that describes how to
+ run a federation [required]
--silent
- --help Show this message and exit.
+ --no-log-files
+ --no-kill-on-error Do not kill all federates on error
+ -l, --broker-loglevel, --loglevel TEXT
+ Log level for HELICS broker
+ --profile Profile flag
+ -w, --web Run the web interface on startup
+ --help Show this message and exit.
```
```bash
@@ -65,6 +70,17 @@ Options:
--help Show this message and exit.
```
+```bash
+$ helics profile-plot --help
+
+Usage: helics profile-plot [OPTIONS]
+
+Options:
+ --path PATH Path to profile.txt that describes profiling results of a
+ federation [required]
+ --help Show this message and exit.
+```
+
### Usage
```bash
@@ -86,6 +102,11 @@ Hello from Federate 2
```
+```bash
+helics run --path examples/pi-exchange/runner.json --profile
+helics profile-plot examples/pi-exchange/profile.txt
+```
+
### Installation
```
diff --git a/examples/pi-exchange/pireceiver.py b/examples/pi-exchange/pireceiver.py
index 3f20f26..e1c27dc 100644
--- a/examples/pi-exchange/pireceiver.py
+++ b/examples/pi-exchange/pireceiver.py
@@ -16,6 +16,7 @@
h.helicsFederateInfoSetTimeProperty(fedinfo, h.helics_property_time_delta, 0.01)
vfed = h.helicsCreateValueFederate(federate_name, fedinfo)
+
print(f"{federate_name}: Value federate created")
sub = h.helicsFederateRegisterSubscription(vfed, f"globaltopic{sys.argv[1]}", "")
@@ -28,9 +29,9 @@
currenttime = -1
-while currenttime <= 100:
+while currenttime <= 10:
- currenttime = h.helicsFederateRequestTime(vfed, 100)
+ currenttime = h.helicsFederateRequestTime(vfed, 10)
value = h.helicsInputGetString(sub)
print(f"{federate_name}: Received value = {value} at time {currenttime}")
diff --git a/examples/pi-exchange/pisender.py b/examples/pi-exchange/pisender.py
index 2ade505..b707a15 100644
--- a/examples/pi-exchange/pisender.py
+++ b/examples/pi-exchange/pisender.py
@@ -4,6 +4,7 @@
from math import pi
import helics as h
+import random
federate_name = f"SenderFederate{sys.argv[1]}"
@@ -17,14 +18,10 @@
h.helicsFederateInfoSetTimeProperty(fedinfo, h.helics_property_time_delta, 0.01)
vfed = h.helicsCreateValueFederate(federate_name, fedinfo)
+
print(f"{federate_name}: Value federate created")
-pub = h.helicsFederateRegisterGlobalTypePublication(
- vfed, f"globaltopic{sys.argv[1]}", "double", ""
-)
-pub = h.helicsFederateRegisterTypePublication(
- vfed, f"localtopic{sys.argv[1]}", "double", ""
-)
+pub = h.helicsFederateRegisterGlobalTypePublication(vfed, f"globaltopic{sys.argv[1]}", "double", "")
print(f"{federate_name}: Publication registered")
@@ -34,7 +31,7 @@
this_time = 0.0
value = pi
-for t in range(5, 10):
+for t in range(0, 10):
val = value
currenttime = h.helicsFederateRequestTime(vfed, t)
@@ -42,7 +39,8 @@
h.helicsPublicationPublishDouble(pub, val)
print(f"{federate_name}: Sending value pi = {val} at time {currenttime}")
- time.sleep(1)
+ # Computing user needs
+ time.sleep(float(sys.argv[1]) * (1 + (0.5 - random.random()) / 10))
h.helicsFederateFinalize(vfed)
print(f"{federate_name}: Federate finalized")
diff --git a/examples/pi-exchange/runner.json b/examples/pi-exchange/runner.json
index f62cffe..0de992a 100644
--- a/examples/pi-exchange/runner.json
+++ b/examples/pi-exchange/runner.json
@@ -19,6 +19,48 @@
"host": "localhost",
"name": "pisender3"
},
+ {
+ "directory": ".",
+ "exec": "python -u pisender.py 4",
+ "host": "localhost",
+ "name": "pisender4"
+ },
+ {
+ "directory": ".",
+ "exec": "python -u pisender.py 5",
+ "host": "localhost",
+ "name": "pisender5"
+ },
+ {
+ "directory": ".",
+ "exec": "python -u pisender.py 6",
+ "host": "localhost",
+ "name": "pisender6"
+ },
+ {
+ "directory": ".",
+ "exec": "python -u pisender.py 7",
+ "host": "localhost",
+ "name": "pisender7"
+ },
+ {
+ "directory": ".",
+ "exec": "python -u pisender.py 8",
+ "host": "localhost",
+ "name": "pisender8"
+ },
+ {
+ "directory": ".",
+ "exec": "python -u pisender.py 9",
+ "host": "localhost",
+ "name": "pisender9"
+ },
+ {
+ "directory": ".",
+ "exec": "python -u pisender.py 10",
+ "host": "localhost",
+ "name": "pisender10"
+ },
{
"directory": ".",
"exec": "python -u pireceiver.py 1",
@@ -36,6 +78,48 @@
"exec": "python -u pireceiver.py 3",
"host": "localhost",
"name": "pireceiver3"
+ },
+ {
+ "directory": ".",
+ "exec": "python -u pireceiver.py 4",
+ "host": "localhost",
+ "name": "pireceiver4"
+ },
+ {
+ "directory": ".",
+ "exec": "python -u pireceiver.py 5",
+ "host": "localhost",
+ "name": "pireceiver5"
+ },
+ {
+ "directory": ".",
+ "exec": "python -u pireceiver.py 6",
+ "host": "localhost",
+ "name": "pireceiver6"
+ },
+ {
+ "directory": ".",
+ "exec": "python -u pireceiver.py 7",
+ "host": "localhost",
+ "name": "pireceiver7"
+ },
+ {
+ "directory": ".",
+ "exec": "python -u pireceiver.py 8",
+ "host": "localhost",
+ "name": "pireceiver8"
+ },
+ {
+ "directory": ".",
+ "exec": "python -u pireceiver.py 9",
+ "host": "localhost",
+ "name": "pireceiver9"
+ },
+ {
+ "directory": ".",
+ "exec": "python -u pireceiver.py 10",
+ "host": "localhost",
+ "name": "pireceiver10"
}
],
"name": "pi-exchange"
diff --git a/helics_cli/cli.py b/helics_cli/cli.py
index 1b8f11c..a7a7da5 100644
--- a/helics_cli/cli.py
+++ b/helics_cli/cli.py
@@ -22,6 +22,7 @@
from .status_checker import CheckStatusThread
from .utils import extra
from .utils.extra import echo
+from . import profile as p
logger = logging.getLogger(__name__)
@@ -46,7 +47,10 @@ def cli(ctx, verbose):
@cli.command()
@click.option(
- "--name", type=click.STRING, default="HELICSFederation", help="Name of the folder that contains the config.json",
+ "--name",
+ type=click.STRING,
+ default="HELICSFederation",
+ help="Name of the folder that contains the config.json",
)
@click.option("--path", type=click.Path(), default="./", help="Path to the folder")
@click.option("--purge/--no-purge", default=False, help="Delete folder if it exists")
@@ -67,7 +71,8 @@ def setup(name, path, purge):
os.makedirs(path)
else:
echo(
- "The following path already exists: {path}".format(path=path), status="error",
+ "The following path already exists: {path}".format(path=path),
+ status="error",
)
echo("Please remove the directory and try again.", status="error")
return None
@@ -88,16 +93,41 @@ def setup(name, path, purge):
@cli.command()
@click.option(
- "--path", required=True, type=click.Path(file_okay=True, exists=True), help="Path to config.json that describes how to run a federation",
+ "--path",
+ required=True,
+ type=click.Path(file_okay=True, exists=True),
+ help="Path to profile.txt that describes profiling results of a federation",
+)
+def profile_plot(path):
+ p.plot(p.profile(path), kind="realtime")
+
+
+@cli.command()
+@click.option(
+ "--path",
+ required=True,
+ type=click.Path(file_okay=True, exists=True),
+ help="Path to config.json that describes how to run a federation",
)
@click.option("--silent", is_flag=True)
@click.option("--no-log-files", is_flag=True, default=False)
@click.option("--no-kill-on-error", is_flag=True, default=False, help="Do not kill all federates on error")
@click.option(
- "--broker-loglevel", "--loglevel", "-l", type=str, default="error", help="Log level for HELICS broker",
+ "--broker-loglevel",
+ "--loglevel",
+ "-l",
+ type=str,
+ default="error",
+ help="Log level for HELICS broker",
+)
+@click.option(
+ "--profile",
+ is_flag=True,
+ default=False,
+ help="Profile flag",
)
@click.option("--web", "-w", is_flag=True, default=False, help="Run the web interface on startup")
-def run(path, silent, no_log_files, broker_loglevel, web, no_kill_on_error):
+def run(path, silent, no_log_files, broker_loglevel, web, no_kill_on_error, profile):
"""
Run HELICS federation
"""
@@ -108,7 +138,8 @@ def run(path, silent, no_log_files, broker_loglevel, web, no_kill_on_error):
if not os.path.exists(path_to_config):
echo(
- "Unable to find file `config.json` in path: {path_to_config}".format(path_to_config=path_to_config), status="error",
+ "Unable to find file `config.json` in path: {path_to_config}".format(path_to_config=path_to_config),
+ status="error",
)
return None
@@ -124,18 +155,40 @@ def run(path, silent, no_log_files, broker_loglevel, web, no_kill_on_error):
process_handler.message_handler.set_enable(True)
if web:
- process_handler.run_web(target=startup, args=(False, path_to_config, process_handler.message_handler,), daemon=True)
+ process_handler.run_web(
+ target=startup,
+ args=(
+ False,
+ path_to_config,
+ process_handler.message_handler,
+ ),
+ daemon=True,
+ )
if "broker" in config.keys() and config["broker"] is not False:
if config["broker"] is not True and "observer" in config["broker"].keys():
process_handler.run_broker(
- target=observer.run, args=(len(config["federates"]), path_to_config, broker_loglevel, process_handler.message_handler,), daemon=True
+ target=observer.run,
+ args=(
+ len(config["federates"]),
+ path_to_config,
+ broker_loglevel,
+ process_handler.message_handler,
+ ),
+ daemon=True,
)
process_handler.broker_process.name = "broker"
else:
+ cmd = "helics_broker -f {num_fed} --loglevel={log_level}"
+ if profile:
+ profiler_txt = os.path.join(os.path.abspath(os.path.expanduser(path)), "profile.txt")
+ if os.path.exists(profiler_txt):
+ os.remove(profiler_txt)
+ cmd += " --profiler=profile.txt"
broker_o = open(os.path.join(path, "broker.log"), "w")
+ cmd = cmd.format(num_fed=len(config["federates"]), log_level=broker_loglevel)
broker_subprocess = subprocess.Popen(
- shlex.split("helics_broker -f {num_fed} --loglevel={log_level}".format(num_fed=len(config["federates"]), log_level=broker_loglevel)),
+ shlex.split(cmd),
cwd=os.path.abspath(os.path.expanduser(path)),
stdout=broker_o,
stderr=broker_o,
@@ -155,7 +208,8 @@ def run(path, silent, no_log_files, broker_loglevel, web, no_kill_on_error):
for f in config["federates"]:
if not silent:
echo(
- "Running federate {name} as a background process".format(name=f["name"]), status="info",
+ "Running federate {name} as a background process".format(name=f["name"]),
+ status="info",
)
if log is True:
@@ -169,7 +223,13 @@ def run(path, silent, no_log_files, broker_loglevel, web, no_kill_on_error):
if "env" in f:
for k, v in f["env"].items():
env[k] = v
- p = subprocess.Popen(shlex.split(f["exec"]), cwd=os.path.abspath(os.path.expanduser(directory)), stdout=o, stderr=o, env=env,)
+ p = subprocess.Popen(
+ shlex.split(f["exec"]),
+ cwd=os.path.abspath(os.path.expanduser(directory)),
+ stdout=o,
+ stderr=o,
+ env=env,
+ )
p.name = f["name"]
except FileNotFoundError as e:
@@ -183,7 +243,8 @@ def run(path, silent, no_log_files, broker_loglevel, web, no_kill_on_error):
try:
t.start()
echo(
- "Waiting for {} processes to finish ...".format(len(process_handler.process_list)), status="info",
+ "Waiting for {} processes to finish ...".format(len(process_handler.process_list)),
+ status="info",
)
for p in process_handler.process_list:
p.wait()
@@ -210,16 +271,22 @@ def run(path, silent, no_log_files, broker_loglevel, web, no_kill_on_error):
for p in process_handler.process_list:
if p.returncode != 0 and p.returncode is not None:
echo(
- "Process {} exited with return code {}".format(p.name, p.returncode), status="error",
+ "Process {} exited with return code {}".format(p.name, p.returncode),
+ status="error",
)
echo(
- "Done.", status="info",
+ "Done.",
+ status="info",
)
@cli.command()
@click.option(
- "--path", required=True, type=click.Path(exists=True), default="./", help="Path to config.json file that describes how to run a federation",
+ "--path",
+ required=True,
+ type=click.Path(exists=True),
+ default="./",
+ help="Path to config.json file that describes how to run a federation",
)
def validate(path):
"""
@@ -248,7 +315,10 @@ def validate(path):
@cli.command()
@click.option(
- "--n-federates", required=True, type=click.INT, help="Number of federates to observe",
+ "--n-federates",
+ required=True,
+ type=click.INT,
+ help="Number of federates to observe",
)
@click.option("--path", type=click.Path(exists=True), default="./", help="Internal path to config file used for filtering output")
@click.option("--broker_loglevel", "--loglevel", "-l", type=click.INT, default=2, help="Log level for HELICS broker")
@@ -258,7 +328,10 @@ def observe(n_federates: int, path: str, log_level) -> int:
@cli.command()
@click.option(
- "--browser", is_flag=True, default=False, help="Open browser on startup",
+ "--browser",
+ is_flag=True,
+ default=False,
+ help="Open browser on startup",
)
@click.option("--path", type=click.Path(exists=True), default="./", help="Path for database file")
def server(browser: bool, path: str):
diff --git a/helics_cli/profile.py b/helics_cli/profile.py
new file mode 100644
index 0000000..5c3aac9
--- /dev/null
+++ b/helics_cli/profile.py
@@ -0,0 +1,123 @@
+# -*- coding: utf-8 -*-
+
+import os
+import re
+
+import matplotlib
+import matplotlib.pyplot as plt
+import matplotlib.patches as patches
+import numpy as np
+
+CURRENT_DIR = os.path.realpath(os.path.basename(__file__))
+
+# SenderFederate2[131074](initializing)HELICS CODE ENTRY<4570827706580384>[t=-1000000]
+PATTERN = re.compile(
+ r"""
+ (?P\w+) # SenderFederate2
+ \[(\d+)\] # [131074]
+ \((?P\w+)\) # (initializing)
+ (?P(?:\w|\s)+) # HELICS CODE ENTRY
+ \<(?P\d+(?:\|\d+)?)\> # <4570827706580384|534534523453>
+ \[t=(?P-?\d*\.{0,1}\d+)\] # [t=-1000000]
+ """,
+ re.X,
+)
+
+
+def profile(filename):
+ with open(filename) as f:
+ data = f.read()
+ data = data.replace(r"", "").replace(r"", "")
+ names = []
+ states = []
+ messages = []
+ simtimes = []
+ realtimes = []
+ time_marker = {}
+ for line in data.splitlines():
+ m = PATTERN.match(line)
+ name = m.group("name")
+ state = m.group("state")
+ message = m.group("message")
+ simtime = float(m.group("simtime"))
+ try:
+ realtime = float(m.group("realtime"))
+ except:
+ realtime, markertime = m.group("realtime").split("|")
+ time_marker[name] = float(markertime)
+ realtime = float(realtime)
+ names.append(name)
+ states.append(state)
+ messages.append(message)
+ simtimes.append(simtime)
+ realtimes.append(realtime)
+
+ profile = {}
+ for name in set(names):
+ profile[name] = []
+
+ for (name, state, message, simtime, realtime) in zip(names, states, messages, simtimes, realtimes):
+ if state == "created":
+ continue
+ if "ENTRY" in message:
+ profile[name].append({"s_enter": simtime, "r_enter": realtime})
+ if "EXIT" in message:
+ profile[name][-1]["s_end"] = simtime
+ profile[name][-1]["r_end"] = realtime
+
+ return profile
+
+
+def plot(profile, kind="simulation", **kwargs):
+ profiles = []
+ names = {k: i for i, k in enumerate(sorted(profile.keys()))}
+
+ if kind == "simulation":
+ end = "s_end"
+ enter = "s_enter"
+ scaling = 1
+ elif kind == "realtime":
+ end = "r_end"
+ enter = "r_enter"
+ scaling = 1e9
+ else:
+ raise Exception("unknown kind")
+
+ for k in profile.keys():
+ for i in profile[k]:
+ if end in i.keys() and enter in i.keys():
+ i["name"] = k
+ i[enter] = i[enter] / scaling
+ i[end] = i[end] / scaling
+ profiles.append(i)
+
+ fig, axs = plt.subplots(1, 1, figsize=(16, 9))
+ ax = axs
+
+ cmap = matplotlib.cm.get_cmap('seismic')
+
+ values = list(d[end] - d[enter] for d in profiles)
+ norm = matplotlib.colors.Normalize(vmin=min(values), vmax=max(values))
+
+ m_enter = min(d[enter] for d in profiles)
+ for i, d in enumerate(profiles):
+ d["color"] = cmap(norm(values[i]))
+
+ for d in profiles:
+ d[enter] = d[enter] - m_enter
+ d[end] = d[end] - m_enter
+ ax.barh(names[d["name"]], d[end] - d[enter], left=d[enter], edgecolor = d["color"], color = d["color"], **kwargs)
+
+ if kind == "Simulation":
+ ax.set_xlabel("Simulation Time (s)")
+ else:
+ ax.set_xlabel("Real Time (s)")
+ ax.set_yticks([i for i in range(0, len(names))])
+ ax.set_yticklabels(sorted(names.keys()))
+ ax.set_facecolor('#f0f0f0')
+ fig.colorbar(matplotlib.cm.ScalarMappable(norm=norm, cmap=cmap), ax=ax, location = "top")
+ plt.show()
+
+
+if __name__ == "__main__":
+ plot(profile(os.path.abspath(os.path.join(CURRENT_DIR, "../examples/pi-exchange/profile.txt"))), kind = "realtime")
diff --git a/setup.py b/setup.py
index adad83a..faa209a 100644
--- a/setup.py
+++ b/setup.py
@@ -25,7 +25,7 @@
author="Dheepak Krishnamurthy",
license="BSD-compatible",
packages=["web", "database"] + find_packages(),
- install_requires=["future", "six", "click", "jinja2", "helics>=2.7.0", "flask"],
+ install_requires=["future", "six", "click", "jinja2", "helics>=2.7.0", "flask", "matplotlib", "numpy"],
keywords=["helics", "cosimulation"],
include_package_data=True,
entry_points={"console_scripts": ["helics = helics_cli.cli:cli"]},