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

visualize: show variable stages #186

Open
wants to merge 16 commits into
base: master
Choose a base branch
from
2 changes: 2 additions & 0 deletions doc/whats_new.rst
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ v0.6.0 (Unreleased)
This will be set to the main clock when storing the dataset.
- Changed default ``fill_value`` in the zarr stores to maximum dtype value
for integer dtypes and ``np.nan`` for floating-point variables.
- Added custom dependencies as option at model creation e.g.
``xs.Model({"a":A,"b":B},custom_dependencies={"a":"b"})

v0.5.0 (26 January 2021)
------------------------
Expand Down
108 changes: 107 additions & 1 deletion xsimlab/dot.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@
import os
from functools import partial

from .utils import variables_dict, import_required, maybe_to_list
from .utils import variables_dict, import_required, maybe_to_list, has_method

from .process import SimulationStage
from .variable import VarIntent, VarType


Expand Down Expand Up @@ -44,6 +46,10 @@
VAR_NODE_ATTRS = {"shape": "box", "color": "#555555", "fontcolor": "#555555"}
VAR_EDGE_ATTRS = {"arrowhead": "none", "color": "#555555"}

FEEDBACK_EDGE_ATTRS = {"style": "dashed", "width": "200"}
IN_EDGE_ATTRS = {"color": "#2ca02c", "style": "bold"}
INOUT_EDGE_ATTRS = {"color": "#d62728", "style": "bold"}


def _hash_variable(var):
# issue with variables with the same name declared in different processes
Expand Down Expand Up @@ -136,6 +142,89 @@ def add_var_and_targets(self, p_name, var_name):
):
self._add_var(var, p_name)

def add_feedback_arrows(self):
"""
adds dotted arrows from the last inout processes to all processes that
use it in the next timestep before it is changed.
"""
# in->inout1->inout2
# ^ /
# \- - - - - /
feedback_edge_attrs = FEEDBACK_EDGE_ATTRS.copy()

in_vars = {}
inout_vars = {}
for p_name, p_obj in self.model._processes.items():
p_cls = type(p_obj)
if not has_method(p_obj, SimulationStage.RUN_STEP.value) and not has_method(
p_obj, SimulationStage.FINALIZE_STEP.value
):
continue
for var_name, var in variables_dict(p_cls).items():
target_keys = tuple(_get_target_keys(p_obj, var_name))
if var.metadata["intent"] == VarIntent.OUT:
in_vars[target_keys] = {p_name}
# also put a placeholder in inout_vars so we do not add
# anymore in processes
inout_vars[target_keys] = None
if (
var.metadata["intent"] == VarIntent.IN
and not target_keys in inout_vars # only in->inout vars
):
in_vars.setdefault(target_keys, set()).add(p_name)
if var.metadata["intent"] == VarIntent.INOUT:
inout_vars[target_keys] = p_name

for target_keys, io_p in inout_vars.items():
# skip this if there are no inout processes
if io_p is None:
continue
for in_p in in_vars[target_keys]:
self.g.edge(io_p, in_p, **feedback_edge_attrs)

def add_stages_arrows(self, p_name, var_name):
"""
adds red arrows between inout processes and green arrows between in and
inout processes of the same variable.
"""
# green red
# /---------\ /-----------\ red green
# in->other->inout->other->inout------>inout------->in
# ^ \ ^ /
# \ green \--->in----/ green /
# \- - - - - - - - - - - - - - - - /
in_edge_attrs = IN_EDGE_ATTRS.copy()
inout_edge_attrs = INOUT_EDGE_ATTRS.copy()
feedback_edge_attrs = FEEDBACK_EDGE_ATTRS.copy()

this_p_name = p_name
this_var_name = var_name

this_p_obj = self.model._processes[this_p_name]
this_target_keys = _get_target_keys(this_p_obj, this_var_name)

in_vars = [set()]
inout_vars = []
for p_name, p_obj in self.model._processes.items():
p_cls = type(p_obj)
for var_name, var in variables_dict(p_cls).items():
if this_target_keys != _get_target_keys(p_obj, var_name):
continue
if var.metadata["intent"] == VarIntent.IN:
in_vars[-1].add(p_name)
elif var.metadata["intent"] == VarIntent.INOUT:
# add an edge from inout var to inout var
if inout_vars:
self.g.edge(inout_vars[-1], p_name, **inout_edge_attrs)
inout_vars.append(p_name)
in_vars.append(set())

for i in range(len(inout_vars)):
for var_p_name in in_vars[i]:
self.g.edge(var_p_name, inout_vars[i], **in_edge_attrs)
for var_p_name in in_vars[i + 1]:
self.g.edge(inout_vars[i], var_p_name, **in_edge_attrs)

def get_graph(self):
return self.g

Expand All @@ -144,8 +233,10 @@ def to_graphviz(
model,
rankdir="LR",
show_only_variable=None,
show_variable_stages=None,
show_inputs=False,
show_variables=False,
show_feedbacks=True,
graph_attr={},
**kwargs,
):
Expand All @@ -161,12 +252,19 @@ def to_graphviz(
p_name, var_name = show_only_variable
builder.add_var_and_targets(p_name, var_name)

elif show_variable_stages is not None:
p_name, var_name = show_variable_stages
builder.add_stages_arrows(p_name, var_name)

elif show_variables:
builder.add_variables()

elif show_inputs:
builder.add_inputs()

elif show_feedbacks:
builder.add_feedback_arrows()

return builder.get_graph()


Expand Down Expand Up @@ -209,8 +307,10 @@ def dot_graph(
filename=None,
format=None,
show_only_variable=None,
show_variable_stages=None,
show_inputs=False,
show_variables=False,
show_feedbacks=True,
**kwargs,
):
"""
Expand All @@ -236,6 +336,10 @@ def dot_graph(
show_variables : bool, optional
If True, show also the other variables (default: False).
Ignored if `show_only_variable` is not None.
show_feedbacks: bool, optional
if True, draws dotted arrows to indicate what processes use updated
variables in the next timestep. (default: True)
Ignored if `show_variables` is not None
**kwargs
Additional keyword arguments to forward to `to_graphviz`.

Expand All @@ -260,8 +364,10 @@ def dot_graph(
g = to_graphviz(
model,
show_only_variable=show_only_variable,
show_variable_stages=show_variable_stages,
show_inputs=show_inputs,
show_variables=show_variables,
show_feedbacks=show_feedbacks,
**kwargs,
)

Expand Down
Loading