From a5eb8eb9ac2f3e067f962a023d38263a5aeb1728 Mon Sep 17 00:00:00 2001 From: Neil Wu <602725+nwu63@users.noreply.github.com> Date: Tue, 21 Mar 2023 15:48:03 -0700 Subject: [PATCH] Add auto fading for off-diagonal blocks (#50) * fairly crude initial impl * changed process to namedtuple; added new styles for faded processes; added logic for determining faded processes * updated docstrings and added check for dict keys * apt update * rename unconnected to connected * allow faded process containing off-diagonal blocks * add to kitchen sink * Ignore all files in examples except .py * add "outgoing" and "incoming" options for connection fading * Remove requirement to specify all auto fading options * Slight modification to kitchen sink * `black -l 120 .` * `black -l 120 .` again * Trying to make the kitchen sink make a little more sense * Adding logic to put faded connections on the top layer of the tikz figure --------- Co-authored-by: A-Gray-94 Co-authored-by: Andrew Lamkin --- .github/workflows/test.yaml | 1 + .gitignore | 3 + examples/kitchen_sink.py | 14 ++++- pyxdsm/XDSM.py | 109 +++++++++++++++++++++++++++++------- pyxdsm/diagram_styles.tex | 4 ++ pyxdsm/matrix_eqn.py | 7 --- tests/test_xdsm.py | 2 - 7 files changed, 110 insertions(+), 30 deletions(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 215a109..54dcfb7 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -29,6 +29,7 @@ jobs: python-version: ${{ matrix.python-version }} - name: Install dependencies run: | + sudo apt update sudo apt-get install texlive-pictures texlive-latex-extra -y pip install -U pip wheel pip install testflo numpy${{ matrix.numpy-version }} diff --git a/.gitignore b/.gitignore index f2a3c90..3ea9deb 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,6 @@ +examples/* +!examples/*.py + *.so *.o *.pyc diff --git a/examples/kitchen_sink.py b/examples/kitchen_sink.py index eba5e62..95638ab 100644 --- a/examples/kitchen_sink.py +++ b/examples/kitchen_sink.py @@ -13,7 +13,14 @@ RIGHT, ) -x = XDSM() +x = XDSM( + auto_fade={ + # "inputs": "none", + "outputs": "connected", + "connections": "outgoing", + # "processes": "none", + } +) x.add_system("opt", OPT, r"\text{Optimizer}") x.add_system("DOE", DOE, r"\text{DOE}") @@ -24,7 +31,7 @@ x.add_system("D2", IFUNC, "D_2", faded=True) x.add_system("D3", IFUNC, "D_3") -x.add_system("subopt", SUBOPT, "SubOpt") +x.add_system("subopt", SUBOPT, "SubOpt", faded=True) x.add_system("G1", GROUP, "G_1") x.add_system("G2", IGROUP, "G_2") x.add_system("MM", METAMODEL, "MM") @@ -44,9 +51,12 @@ x.connect("opt", "D2", ["z", "y_1"]) x.connect("opt", "D3", "z, y_1") x.connect("opt", "subopt", "z, y_1") +x.connect("D3", "G1", "y_3") x.connect("subopt", "G1", "z_2") x.connect("subopt", "G2", "z_2") x.connect("subopt", "MM", "z_2") +x.connect("subopt", "F", "f") +x.connect("MM", "subopt", "f") x.connect("opt", "G2", "z") x.connect("opt", "F", "x, z") x.connect("opt", "F", "y_1, y_2") diff --git a/pyxdsm/XDSM.py b/pyxdsm/XDSM.py index 8d7b71f..f60736c 100644 --- a/pyxdsm/XDSM.py +++ b/pyxdsm/XDSM.py @@ -114,10 +114,11 @@ def _label_to_spec(label, spec): Input = namedtuple("Input", "node_name label label_width style stack faded") Output = namedtuple("Output", "node_name label label_width style stack faded side") Connection = namedtuple("Connection", "src target label label_width style stack faded") +Process = namedtuple("Process", "systems arrow faded") -class XDSM(object): - def __init__(self, use_sfmath=True, optional_latex_packages=None): +class XDSM: + def __init__(self, use_sfmath=True, optional_latex_packages=None, auto_fade=None): """Initialize XDSM object Parameters @@ -126,6 +127,14 @@ def __init__(self, use_sfmath=True, optional_latex_packages=None): Whether to use the sfmath latex package, by default True optional_latex_packages : string or list of strings, optional Additional latex packages to use when creating the pdf and tex versions of the diagram, by default None + auto_fade : dictionary, optional + Controls the automatic fading of inputs, outputs, connections and processes based on the fading of diagonal blocks. For each key "inputs", "outputs", "connections", and "processes", the value can be one of: + - "all" : fade all blocks + - "connected" : fade all components connected to faded blocks + - "none" : do not auto-fade anything + For connections there are two additional options: + - "incoming" : Fade all connections that are incoming to faded blocks. + - "outgoing" : Fade all connections that are outgoing from faded blocks. """ self.systems = [] self.connections = [] @@ -133,7 +142,6 @@ def __init__(self, use_sfmath=True, optional_latex_packages=None): self.right_outs = {} self.ins = {} self.processes = [] - self.process_arrows = [] self.use_sfmath = use_sfmath if optional_latex_packages is None: @@ -146,6 +154,26 @@ def __init__(self, use_sfmath=True, optional_latex_packages=None): else: raise ValueError("optional_latex_packages must be a string or a list of strings") + self.auto_fade = {"inputs": "none", "outputs": "none", "connections": "none", "processes": "none"} + fade_options = ["all", "connected", "none"] + if auto_fade is not None: + if any([key not in self.auto_fade for key in auto_fade.keys()]): + raise ValueError( + "The supplied 'auto_fade' dictionary contains keys that are not recognized. " + + "valid keys are 'inputs', 'outputs', 'connections', 'processes'." + ) + + self.auto_fade.update(auto_fade) + for key in self.auto_fade.keys(): + option_is_valid = self.auto_fade[key] in fade_options or ( + key == "connections" and self.auto_fade[key] in ["incoming", "outgoing"] + ) + if not option_is_valid: + raise ValueError( + f"The supplied 'auto_fade' dictionary contains an invalid value: '{key}'. " + + "valid values are 'all', 'connected', 'none', 'incoming', 'outgoing'." + ) + def add_system( self, node_name, @@ -156,7 +184,7 @@ def add_system( label_width=None, spec_name=None, ): - """ + r""" Add a "system" block, which will be placed on the diagonal of the XDSM diagram. Parameters @@ -198,7 +226,7 @@ def add_system( self.systems.append(sys) def add_input(self, name, label, label_width=None, style="DataIO", stack=False, faded=False): - """ + r""" Add an input, which will appear in the top row of the diagram. Parameters @@ -229,10 +257,17 @@ def add_input(self, name, label, label_width=None, style="DataIO", stack=False, faded : bool If true, the component will be faded, in order to highlight some other system. """ + sys_faded = {} + for s in self.systems: + sys_faded[s.node_name] = s.faded + if (self.auto_fade["inputs"] == "all") or ( + self.auto_fade["inputs"] == "connected" and name in sys_faded and sys_faded[name] + ): + faded = True self.ins[name] = Input("output_" + name, label, label_width, style, stack, faded) def add_output(self, name, label, label_width=None, style="DataIO", stack=False, faded=False, side="left"): - """ + r""" Add an output, which will appear in the left or right-most column of the diagram. Parameters @@ -267,6 +302,13 @@ def add_output(self, name, label, label_width=None, style="DataIO", stack=False, Must be one of ``['left', 'right']``. This parameter controls whether the output is placed on the left-most column or the right-most column of the diagram. """ + sys_faded = {} + for s in self.systems: + sys_faded[s.node_name] = s.faded + if (self.auto_fade["outputs"] == "all") or ( + self.auto_fade["outputs"] == "connected" and name in sys_faded and sys_faded[name] + ): + faded = True if side == "left": self.left_outs[name] = Output("left_output_" + name, label, label_width, style, stack, faded, side) elif side == "right": @@ -284,7 +326,7 @@ def connect( stack=False, faded=False, ): - """ + r""" Connects two components with a data line, and adds a label to indicate the data being transferred. @@ -325,9 +367,24 @@ def connect( if (not isinstance(label_width, int)) and (label_width is not None): raise ValueError("label_width argument must be an integer") + sys_faded = {} + for s in self.systems: + sys_faded[s.node_name] = s.faded + + allFaded = self.auto_fade["connections"] == "all" + srcFaded = src in sys_faded and sys_faded[src] + targetFaded = target in sys_faded and sys_faded[target] + if ( + allFaded + or (self.auto_fade["connections"] == "connected" and (srcFaded or targetFaded)) + or (self.auto_fade["connections"] == "incoming" and targetFaded) + or (self.auto_fade["connections"] == "outgoing" and srcFaded) + ): + faded = True + self.connections.append(Connection(src, target, label, label_width, style, stack, faded)) - def add_process(self, systems, arrow=True): + def add_process(self, systems, arrow=True, faded=False): """ Add a process line between a list of systems, to indicate process flow. @@ -341,8 +398,17 @@ def add_process(self, systems, arrow=True): If true, arrows will be added to the process lines to indicate the direction of the process flow. """ - self.processes.append(systems) - self.process_arrows.append(arrow) + sys_faded = {} + for s in self.systems: + sys_faded[s.node_name] = s.faded + if (self.auto_fade["processes"] == "all") or ( + self.auto_fade["processes"] == "connected" + and any( + [sys_faded[s] for s in systems if s in sys_faded.keys()] + ) # sometimes a process may contain off-diagonal blocks + ): + faded = True + self.processes.append(Process(systems, arrow, faded)) def _build_node_grid(self): size = len(self.systems) @@ -498,6 +564,9 @@ def _build_edges(self): node_name = inp.node_name v_edges.append(edge_format_string.format(start=comp_name, end=node_name, style=style)) + h_edges = sorted(h_edges, key=lambda s: "faded" in s) + v_edges = sorted(v_edges, key=lambda s: "faded" in s) + paths_str = "% Horizontal edges\n" + "\n".join(h_edges) + "\n" paths_str += "% Vertical edges\n" + "\n".join(v_edges) + ";" @@ -514,10 +583,10 @@ def _build_process_chain(self): # node_name, style, label, stack = in_data chain_str = "" - for proc, arrow in zip(self.processes, self.process_arrows): + for proc in self.processes: chain_str += "{ [start chain=process]\n \\begin{pgfonlayer}{process} \n" start_tip = False - for i, sys in enumerate(proc): + for i, sys in enumerate(proc.systems): if sys not in sys_names and sys not in output_names: raise ValueError( 'process includes a system named "{}" but no system with that name exists.'.format(sys) @@ -528,21 +597,23 @@ def _build_process_chain(self): chain_str += "\\chainin ({});\n".format(sys) else: if sys in output_names or (i == 1 and start_tip): - if arrow: - chain_str += "\\chainin ({}) [join=by ProcessTipA];\n".format(sys) + if proc.arrow: + style = "ProcessTipA" else: - chain_str += "\\chainin ({}) [join=by ProcessTip];\n".format(sys) + style = "ProcessTip" else: - if arrow: - chain_str += "\\chainin ({}) [join=by ProcessHVA];\n".format(sys) + if proc.arrow: + style = "ProcessHVA" else: - chain_str += "\\chainin ({}) [join=by ProcessHV];\n".format(sys) + style = "ProcessHV" + if proc.faded: + style = "Faded" + style + chain_str += "\\chainin ({}) [join=by {}];\n".format(sys, style) chain_str += "\\end{pgfonlayer}\n}" return chain_str def _compose_optional_package_list(self): - # Check for optional LaTeX packages optional_packages_list = self.optional_packages if self.use_sfmath: diff --git a/pyxdsm/diagram_styles.tex b/pyxdsm/diagram_styles.tex index 816d5ac..3e4f441 100755 --- a/pyxdsm/diagram_styles.tex +++ b/pyxdsm/diagram_styles.tex @@ -63,6 +63,10 @@ \tikzstyle{ProcessHVA} = [->,line width=1pt,to path={-| (\tikztotarget)}] \tikzstyle{ProcessTip} = [-,line width=1pt] \tikzstyle{ProcessTipA} = [->, line width=1pt] +\tikzstyle{FadedProcessHV} = [-,line width=1pt,to path={-| (\tikztotarget)},color=black!30] +\tikzstyle{FadedProcessHVA} = [->,line width=1pt,to path={-| (\tikztotarget)},color=black!30] +\tikzstyle{FadedProcessTip} = [-,line width=1pt,color=black!30] +\tikzstyle{FadedProcessTipA} = [->, line width=1pt,color=black!30] % Matrix options \tikzstyle{MatrixSetup} = [row sep=3mm, column sep=2mm] diff --git a/pyxdsm/matrix_eqn.py b/pyxdsm/matrix_eqn.py index 997e7a1..f132704 100644 --- a/pyxdsm/matrix_eqn.py +++ b/pyxdsm/matrix_eqn.py @@ -279,7 +279,6 @@ def connect(self, src, target, text="", color="tableau0"): self._connections[src, target] = CellData(text=text, color=color, highlight="diag") def _process_vars(self): - if self._setup: return @@ -397,7 +396,6 @@ def add_variable(self, name, size=1, text="", color="blue"): self._total_size += size def connect(self, src, target, text="", color=None, highlight=1): - if isinstance(target, (list, tuple)): for t in target: self._connections[src, t] = CellData(text=text, color=color, highlight=highlight) @@ -430,7 +428,6 @@ def _process_vars(self): self._setup = True def jacobian(self, transpose=False): - self._process_vars() tikz = [] @@ -478,7 +475,6 @@ def jacobian(self, transpose=False): return lhs_tikz def vector(self, base_color="red", highlight=None): - self._process_vars() tikz = [] @@ -487,7 +483,6 @@ def vector(self, base_color="red", highlight=None): highlight = np.ones(self._n_vars) for i, h_light in enumerate(highlight): - color = _color(base_color, h_light) row_size = self._ij_variables[i].size @@ -509,7 +504,6 @@ def vector(self, base_color="red", highlight=None): return vec_tikz def operator(self, opperator="="): - self._process_vars() tikz = [] @@ -528,7 +522,6 @@ def operator(self, opperator="="): return op_tikz def spacer(self): - self._process_vars() tikz = [] diff --git a/tests/test_xdsm.py b/tests/test_xdsm.py index 6515712..9a2def3 100644 --- a/tests/test_xdsm.py +++ b/tests/test_xdsm.py @@ -65,7 +65,6 @@ def test_connect(self): self.fail("Expected ValueError") def test_options(self): - filename = "xdsm_test_options" spec_dir = filename + "_specs" @@ -108,7 +107,6 @@ def test_options(self): self.assertTrue(os.path.isfile(os.path.join(spec_dir, "G_spec.json"))) def test_stacked_system(self): - x = XDSM() x.add_system("test", OPT, r"\text{test}", stack=True)