Visualizing Message Connections

The previous tutorial, Creating Stand-Alone Messages, shows how to create stand-alone messages and subscribe module input messages to them. Once a simulation grows beyond a few modules it can be difficult to verify that every input is subscribed to the expected output or stand-alone message. The SimulationBaseClass helper ShowMessageConnectionFigure can create a compact Matplotlib figure or a Graphviz-rendered file that shows the configured module message flow.

The message flow diagram is an inspection tool. It does not change task execution, message connections, message logging, or the simulation results. It can be called after the simulation objects have been created and added to tasks, either before or after the simulation is executed.

The sample script below creates a module, connects a stand-alone input message, and records the module output message. The stand-alone input message is supplied through extraMessages so the diagram can draw where the module input is connected.

 1
 2import os
 3import shutil
 4
 5import matplotlib.pyplot as plt
 6from Basilisk.architecture import messaging
 7from Basilisk.moduleTemplates import cppModuleTemplate
 8from Basilisk.utilities import SimulationBaseClass
 9from Basilisk.utilities import macros
10from Basilisk.utilities import unitTestSupport
11
12
13def make_graphviz_figure(scSim, inputMsg, show_plots=False):
14    """Create the optional Graphviz tutorial output if Graphviz is available."""
15    if shutil.which("dot") is None:
16        return None
17
18    return unitTestSupport.saveScenarioGraphvizFigure(
19        "bsk-5a-graphviz",
20        scSim,
21        os.path.dirname(os.path.abspath(__file__)),
22        show_plots=show_plots,
23        graphvizLayout="vertical",
24        extraMessages={"inputMsg": inputMsg},
25        includeUnlinked=False,
26        includeRecorders=True,
27    )
28
29
30def run(show_plots=False):
31    """Illustrate Basilisk message connection visualization."""
32
33    # Create a simulation container.
34    scSim = SimulationBaseClass.SimBaseClass()
35
36    # Create the simulation process and task.
37    dynProcess = scSim.CreateNewProcess("dynamicsProcess")
38    simulationTimeStep = macros.sec2nano(1.0)  # [ns]
39    dynProcess.addTask(scSim.CreateNewTask("dynamicsTask", simulationTimeStep))
40
41    # Create a module with one input message and one output message.
42    mod1 = cppModuleTemplate.CppModuleTemplate()
43    mod1.ModelTag = "cppModule1"
44    scSim.AddModelToTask("dynamicsTask", mod1)
45
46    # Create a stand-alone input message and connect it to the module input.
47    msgData = messaging.CModuleTemplateMsgPayload(dataVector=[1.0, 2.0, 3.0])
48    inputMsg = messaging.CModuleTemplateMsg().write(msgData)
49    mod1.dataInMsg.subscribeTo(inputMsg)
50
51    # Add a recorder so the diagram also shows a regular module output link.
52    msgRec = mod1.dataOutMsg.recorder()
53    scSim.AddModelToTask("dynamicsTask", msgRec)
54
55    # The message connection figure is optional and inspects the setup without
56    # changing the simulation execution or logging behavior.
57    figureList = {}
58    figureList["bsk-5a"] = scSim.ShowMessageConnectionFigure(
59        show_plots=show_plots,
60        extraMessages={"inputMsg": inputMsg},
61        includeUnlinked=False,
62        includeRecorders=True,
63    )
64
65    # Graphviz writes a file rather than returning a Matplotlib figure.  The
66    # tutorial uses this optional file to show the Graphviz renderer result.
67    make_graphviz_figure(scSim, inputMsg, show_plots)
68    plt.close("all")
69
70    return figureList
71
72
73if __name__ == "__main__":
74    run(True)

Running the script returns the following Matplotlib figure.

../../_images/bsk-5a.svg

If Graphviz is installed, the same script also writes the following Graphviz-rendered SVG file. The sample uses unitTestSupport.saveScenarioGraphvizFigure so documentation images are saved to the standard scenario image folder. When show_plots is True, the tutorial displays both the Matplotlib figure and the saved Graphviz output.

../../_images/bsk-5a-graphviz.svg

Creating the Figure

The figure is created with:

fig = scSim.ShowMessageConnectionFigure(
    show_plots=False,
    extraMessages=None,
    includeUnlinked=True,
    includeRecorders=True,
    renderer="matplotlib",
    fileName=None,
    graphvizFormat="svg",
    graphvizLayout="vertical",
)

With the default renderer="matplotlib" option, the return value is the Matplotlib figure object. This lets a scenario return the figure in its figureList dictionary for automatic documentation, or save the figure using the normal Matplotlib interface.

The options are:

show_plots

If this is True, the figure is displayed with plt.show(). Scenario and documentation tests normally set this to False and return the figure instead.

extraMessages

Optional stand-alone source messages to include in the graph. This can be a dictionary such as {"vehicleConfigMsg": vcMsg}, or a list of dictionaries such as [{"vehicleConfigMsg": vcMsg}, {"rwConfigMsg": fswRwParamMsg}]. The dictionary keys become the labels used in the figure. Use this option for stand-alone messages that are not owned by a module, such as vehicle configuration messages, reaction wheel configuration messages, commanded torque messages, or an epoch message. Connections from these supplied messages are drawn with dashed lines.

includeUnlinked

If this is True, input message fields that are not subscribed to a source are shown as gray ports. This is useful when checking whether a setup forgot to connect an input message. If it is False, these unused input message fields are hidden to reduce clutter. Linked inputs whose source message cannot be found remain in the graph and are listed in the programmatic graph output as unresolvedInputs.

includeRecorders

If this is True, message recorder modules created with .recorder() are included in the diagram. This is useful when verifying that the desired message is being recorded. If it is False, recorder modules are hidden so the figure focuses on the regular Basilisk modules. A recorder attached to an input message is drawn from that module input endpoint to the recorder. A recorder attached to an output message is drawn from that module output endpoint to the recorder.

renderer

Selects the drawing backend. The default value "matplotlib" returns a Matplotlib figure. The value "graphviz" writes a Graphviz dot file and renders it through the Graphviz dot executable. Graphviz does not create a Matplotlib figure object; it creates a saved output artifact such as an SVG, PNG, or PDF file and returns the file path.

fileName

Optional output path used by the Graphviz renderer. If the path has an extension such as .svg or .pdf, that extension selects the Graphviz output format. If no extension is provided, graphvizFormat is appended. If no fileName is supplied, a temporary file is created.

graphvizFormat

Output format used by the Graphviz renderer when fileName does not provide an extension. Common values are "svg", "png", "pdf", and "dot". The "dot" value writes only the Graphviz source file and does not run the Graphviz layout command.

graphvizLayout

Module layout direction used by the Graphviz renderer. The default value "vertical" stacks the modules from top to bottom to keep large scenarios narrower. The value "horizontal" draws the execution order from left to right, matching the original Graphviz layout style. The aliases "TB" and "LR" can also be used.

Reading the Figure

The module boxes are shown in the same order that the simulation executes them. Each box includes the module tag and its process and task names. Input message fields are drawn on the left side of a module box, while output message fields are drawn on the right side.

The default message port color is gray. When a message port participates in a discovered connection, the port is shown in orange on both ends of the connection. Solid lines represent links between endpoints discovered within the configured simulation. Dashed lines represent links whose source was provided through extraMessages.

Creating and Showing a Graphviz Diagram

For larger simulations, Graphviz can route the connection lines around module boxes more cleanly than the Matplotlib renderer. The default Graphviz layout is vertical, which is usually more compact for documentation pages than a wide left-to-right module chain. Because Graphviz is file-oriented, the result must be written to a file. If show_plots is True, Basilisk opens that rendered file with the operating system’s default viewer after it is written.

For example, this writes an SVG message-flow diagram, opens it with the default viewer, and returns the SVG path:

outputPath = scSim.ShowMessageConnectionFigure(
    show_plots=True,
    renderer="graphviz",
    fileName="messageConnections.svg",
    graphvizLayout="vertical",
    extraMessages={"inputMsg": inputMsg},
    includeUnlinked=False,
    includeRecorders=True,
)

The Graphviz renderer also writes the intermediate messageConnections.dot file next to the SVG file. This DOT file can be useful when debugging the layout or when a user wants to run Graphviz manually with custom command line options.

The Graphviz renderer requires the dot executable to be installed and available on the system path. The Python graphviz package is not required because Basilisk writes the DOT source directly.

Extracting the Graph

If the connections are needed for a unit test or custom analysis, use GetMessageConnectionGraph instead of drawing a figure:

graph = scSim.GetMessageConnectionGraph(
    extraMessages={"inputMsg": inputMsg},
    includeUnlinked=False,
    includeRecorders=True,
)

This method uses the same extraMessages, includeUnlinked, and includeRecorders options. It returns a dictionary with the following entries:

modules

The ordered module records, including process, task, priority, period, inputs, and outputs.

standaloneMessages

The stand-alone messages provided through extraMessages.

inputs and outputs

The endpoint records found on modules and supplied stand-alone messages.

edges

The discovered source-to-target message connections.

unlinkedInputs

Input endpoints that are not connected, included only when includeUnlinked is True.

unresolvedInputs

Linked input endpoints whose source message was not found among module outputs or extraMessages.

The corresponding Graphviz DOT source can be retrieved without rendering a file using:

dotText = scSim.GetMessageConnectionDot(
    extraMessages={"inputMsg": inputMsg},
    includeUnlinked=False,
    includeRecorders=True,
    graphvizLayout="vertical",
)