Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add graphviz/dash code to visualize ersap configuration yml file #27

Merged
merged 3 commits into from
Dec 14, 2023
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
143 changes: 143 additions & 0 deletions rtdp/java/config_demo_ersap.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
# This file is copied from https://github.com/baltzell/clas12-workflow/blob/master/yamls/train_calib.yaml

io-services:
reader:
class: org.jlab.jnp.grapes.io.HipoFrameReader
name: HipoFrameReader
writer:
class: org.jlab.jnp.grapes.io.HipoFrameWriter
name: HipoFrameWriter
services:
#################################################### RF
- class: org.jlab.jnp.grapes.services.GenericWagon
name: RF
#################################################### DC
- class: org.jlab.jnp.grapes.services.GenericWagon
name: DCm
- class: org.jlab.jnp.grapes.services.GenericWagon
name: DCp
#################################################### FTOF
- class: org.jlab.jnp.grapes.services.GenericWagon
name: FTOFm
- class: org.jlab.jnp.grapes.services.GenericWagon
name: FTOFp
#################################################### HTCC
- class: org.jlab.jnp.grapes.services.GenericWagon
name: HTCCm
- class: org.jlab.jnp.grapes.services.GenericWagon
name: HTCCp
#################################################### CTOF
- class: org.jlab.jnp.grapes.services.GenericWagon
name: CTOF
#################################################### CND
- class: org.jlab.jnp.grapes.services.GenericWagon
name: CND
#################################################### FT
- class: org.jlab.jnp.grapes.services.GenericWagon
name: FTp
- class: org.jlab.jnp.grapes.services.GenericWagon
name: FTm
- class: org.jlab.jnp.grapes.services.GenericWagon
name: FTn
- class: org.jlab.jnp.grapes.services.GenericWagon
name: FTt
#################################################### BAND
- class: org.jlab.jnp.grapes.services.GenericWagon
name: BAND
#################################################### ECAL
- class: org.jlab.jnp.grapes.services.GenericWagon
name: ECALp
- class: org.jlab.jnp.grapes.services.GenericWagon
name: ECALm
- class: org.jlab.jnp.grapes.services.GenericWagon
name: FMT
####################################################
configuration:
custom-names:
1: rf
2: dc
3: ftof
4: htcc
5: ctof
6: cnd
7: ft
8: band
9: ecal
10: fmt
io-services:
writer:
compression: 2
filter: 1-RUN::config,RUN::rf,RF::.*,REC::Event,REC::Particle,REC::Scintillator-2-RASTER::*,RUN::config,TimeBasedTrkg::TBHits,TimeBasedTrkg::TBSegments,REC::*-3-RUN::rf,RUN::config,REC::Event,REC::Particle,REC::Track,REC::Scintillator,FTOF::*,TimeBasedTrkg::TBTracks-4-RUN::rf,RUN::config,REC::Event,REC::Particle,REC::Track,REC::Scintillator,REC::Cherenkov,HTCC::.*-5-RUN::rf,RUN::config,REC::Event,REC::Particle,REC::Track,REC::Scintillator,CTOF::*,CVTRec::Tracks,CVTRec::Trajectory-6-RUN::config,REC::Particle,REC::Event,CVTRec::Tracks,CVTRec::Trajectory,CND::*-7-RUN::config,REC::Event,REC::Particle,FT::particles,FTCAL::*,FTHODO::*-8-RUN::config,REC::Event,REC::Particle,REC::Calorimeter,REC::Scintillator,REC::Track,BAND::*,RUN::scaler-9-RUN::config,REC::Event,REC::Calorimeter,REC::Scintillator,REC::Particle,ECAL::*-10-RUN::config,REC::Particle,REC::Track,TimeBasedTrkg::TBTracks,TimeBasedTrkg:Trajectory,FMT*
services:
#################################################### RF
RF:
id: 1
forward: 11:X+:X-:Xn
#################################################### DC
DCm:
id: 2
forward: 1-:11:X+:X-:Xn
DCp:
id: 2
forward: 1+:11:X+:X-:Xn
#################################################### FTOF
FTOFm:
id: 3
forward: 1-:11:X+:X-:Xn
FTOFp:
id: 3
forward: 1+:11:X+:X-:Xn
#################################################### HTCC
HTCCm:
id: 4
forward: 1-:11:X+:X-:Xn
HTCCp:
id: 4
forward: 1+:11:X+:X-:Xn
#################################################### CTOF
CTOF:
id: 5
forward: 11:X+:X-:Xn
central: 1-:X+:X-:Xn
#################################################### CND
CND:
id: 6
forward: 11:X+:X-:Xn
central: 1-:X+:X-:Xn
#################################################### FT
FTp:
id: 7
forward: 11:X+:X-:Xn
tagger: 1+:X+:X-:Xn
FTm:
id: 7
forward: 11:X+:X-:Xn
tagger: 1-:X+:X-:Xn
FTn:
id: 7
forward: 11:X+:X-:Xn
tagger: 1n:X+:X-:Xn
FTt:
id: 7
tagger: 2n:X+:X-:Xn
#################################################### BAND
BAND:
id: 8
forward: 11:X+:X-:Xn
tagger: X+:X-:Xn
central: X+:X-:Xn
#################################################### ECAL
ECALm:
id: 9
forward: 1-:11:X+:X-:Xn
ECALp:
id: 9
forward: 1+:11:X+:X-:Xn
#################################################### FMT
FMT:
id: 10
forward: 11:X+:X-:Xn
####################################################

mime-types:
- binary/data-hipo-frame
153 changes: 153 additions & 0 deletions rtdp/python/config_parser.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
# Author: [email protected]

"""
Read the configurations from yaml file and visualize it as DAG.
"""

import yaml
import json
from graphviz import Digraph
from abc import ABC, abstractmethod

class ConfigReader(ABC):
def __init__(self, filepath):
self.config_data = self._get_config(filepath)
if not self.config_data:
print("Error: incorrect yaml file!")
exit(ValueError)
# self._pprint_config()

def _get_config(self, filepath):
"""Parse the configuration file into a dictionary using the pyYAML module.
"""
with open(filepath, 'r') as file:
try:
config_data = yaml.safe_load(file)
return config_data
except yaml.YAMLError as e:
print("Error reading YAML file:", e)
return None

def _pprint_config(self):
"""Print the raw parsed dictionary in json format."""
print(json.dumps(self.config_data, indent=4))

@abstractmethod
def get_flowchart_nodes(self):
"""Translate the input yml configuration file into a flowchart and extract the flowchart nodes.
Details are defined in the children classes."""
pass

def graphviz_flowchart(self):
"""Visualize the configuration file that the name of each service is treated as a node in a DAG."""
# TODO: this function is not called by the main now.
node_list = self.get_flowchart_nodes()

# graphviz examples: https://graphviz.readthedocs.io/en/stable/examples.html
g = Digraph('config',
node_attr={'color': 'lightblue2', 'style': 'filled'},
graph_attr={'rankdir': 'LR'}) # "rankdir" is for the direction
g.attr(size='20,20')

for i in range(len(node_list) - 1):
g.edge(node_list[i].name, node_list[i + 1].name)

# It will generate a pdf file named as config.gz.pdf in the same path. Also a config.gv in dot.
# TODO: better handling of filename, saved path, etc. May take parameters from cli args.
g.view()

def get_cytoscape_elements(self):
"""
Transfer the node list into a cytoscape-format dictionary array.
"""
r = []
node_list = self.get_flowchart_nodes()
n = len(node_list)

# Dash Cytoscape ref: https://dash.plotly.com/cytoscape/elements

# The node elements
for i in range(n):
r.append({
'data': {'id': node_list[i].name, 'label': node_list[i].name},
'position': {'x': 20 + 50 * i, 'y': 50}
})

# The edge elements
for i in range(n - 1):
r.append({'data': {'source': node_list[i].name, 'target': node_list[i + 1].name}})

return r

def print_cytoscape_elements(self):
print(json.dumps(self.get_cytoscape_elements(), indent=4))


class ERSAPFlowchartNode:
def __init__(self, item):
"""Definition of an ERSAP flowchart node. Currently it's only for visulization, but it may support
services launching in the future (needs to figure out parameter extracting first).

Args:
item: A dictionary entry parsed by pyYAML.
"""
self._validate_required(item)
self.name = item["name"]
self.lan = item["language"] if "language" in item else "java" # language
self.cls = item["class"]
self.parameters = "" # TODO: extract parameters for an ERSAP service

def _validate_required(self, item):
# "name" and "class" are the two required fields now
for f in ["name", "class"]:
if f not in item:
print(f"Error: '{f}' is required in the service module {item}!")
exit(KeyError)


class ERSAPReader(ConfigReader):
def __init__(self, filepath):
super().__init__(filepath)

def _validate_ioservices(self):
"""Validate the config yaml file has "io-services" and its sub "reader" and "writer."""
if "io-services" not in self.config_data:
print(f"Error: 'io-services' is required in ERSAP configuration!")
return False
if "reader" not in self.config_data["io-services"]:
print(f"Error: 'reader' is required in 'io-services'!")
return False
if "writer" not in self.config_data["io-services"]:
print(f"Error: 'writer' is required in 'io-services'!")
return False
return True

def get_flowchart_nodes(self):
"""Extract the io-services and services in the ERSAP configuration.

Returns:
node_list: A list where each element is an ERSAPFlowchartNode.
"""
if not self._validate_ioservices():
exit(KeyError)

node_list = []

# EARSAP io-services reader, the 1st node in the flowchart.
node_list.append(ERSAPFlowchartNode(self.config_data["io-services"]["reader"]))

for service in self.config_data["services"]:
node_list.append(ERSAPFlowchartNode(service))

# EARSAP io-services writer, the last node in the flowchart.
node_list.append(ERSAPFlowchartNode(self.config_data["io-services"]["writer"]))

return node_list

def print_nodes(self):
node_list = self.get_flowchart_nodes()

print("\n\nThe ERSAP services:\n")
for i in node_list:
print(i.name)
print(f' class: {i.cls}\n language: {i.lan}\n parameters: {i.parameters}\n')
34 changes: 25 additions & 9 deletions rtdp/python/rtdp.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,12 @@
# Logging cookboook: https://docs.python.org/3/howto/logging-cookbook.html
# import logging

# Dash modules
from dash import html, Dash, dcc
import dash_cytoscape as cyto

from config_parser import ERSAPReader

RTDP_CLI_APP_DESCRIP_STR = \
"rtdp: JLab's streaming readout RealTime Development and testing Platform."
RTDP_CLI_APP_URL_STR = "https://github.com/JeffersonLab/SRO-RTDP"
Expand Down Expand Up @@ -39,19 +45,29 @@ def run_rtdp(parser):
"""
args = parser.parse_args()
if args.config_file:
# TODO: this does not gaurentee the config file even exit or is a valid yaml file.
print(f"Starting the platform using the specified YAML configuration file:\
{args.config_file}")
else:
parser.print_help()
# TODO: using ERSAP reader here. Should be generalized.
# TODO: not a service launching yet.
configurations = ERSAPReader(args.config_file)
# configurations.print_cytoscape_elements()

app = Dash(__name__)

def main():
"""
The main function to start the application.
"""
run_rtdp(get_parser())
app.layout = html.Div([
html.H1(children='Visulization of the ERSAP configuration file', style={'textAlign':'Left'}),
cyto.Cytoscape(
id='cyto-display',
layout={'name': 'grid'},
style={'width': '1200px', 'height': '400px'},
elements=configurations.get_cytoscape_elements()
)
])

app.run_server(debug=True)
else:
parser.print_help()


if __name__ == '__main__':
main()
run_rtdp(get_parser())