From 266b70599795f1cc9eb75b69f46da77f7408690a Mon Sep 17 00:00:00 2001 From: Gleb Belov Date: Thu, 4 Apr 2024 19:31:23 +1100 Subject: [PATCH] RefExplorer: view/save ref tree as JSON #232 --- support/modelexplore/modelexplore.py | 53 +++++++++++++------ support/modelexplore/scripts/python/graph.py | 3 ++ .../modelexplore/scripts/python/matcher.py | 4 +- support/modelexplore/scripts/python/model.py | 51 +++++++++++------- 4 files changed, 74 insertions(+), 37 deletions(-) diff --git a/support/modelexplore/modelexplore.py b/support/modelexplore/modelexplore.py index 18bc3ba80..7162acea5 100644 --- a/support/modelexplore/modelexplore.py +++ b/support/modelexplore/modelexplore.py @@ -1,5 +1,6 @@ import streamlit as st +import json from scripts.python.modelreader import ReadExplorerModel from scripts.python.matcher import MatchSubmodel @@ -19,10 +20,15 @@ fwd = st.sidebar.checkbox( 'Add descendants', disabled=True, - help = 'Include all solver model items derived from matching items in the NL model') + help = 'When searching, include all solver model items derived from matching items in the NL model') bwd = st.sidebar.checkbox( 'Add ancestors', disabled=True, - help = 'Include all NL model items reduced to matching items in the solver model') + help = 'When searching, Include all NL/intermediate model items reduced to matching items in the solver model') + +nl_ref_tree = st.sidebar.radio( + "**NL model presentation mode:**", + ["Text", "Reformulation tree :sparkles:"]) +reftree = "Text"!=nl_ref_tree left_column, right_column = st.columns(2) @@ -34,21 +40,28 @@ def ReadModel(uploader): # Cache the matching function? # @st.cache_data Need cacheable Model. -def MatchSelection(m, srch, fwd, bwd): - return MatchSubmodel(m, srch, fwd, bwd) +def MatchSelection(m, srch, fwd, bwd, reftree): + return MatchSubmodel(m, srch, fwd, bwd, reftree) # Write dictionary of entries @st.cache_data -def WriteDict(d): - whole = "" +def WriteDict(d, reftree=False): + whole = "" if not reftree else {} ## dict/json: add only non-empty sections for k, v in d.items(): - if len(v): - whole = whole + '\n\n## ' + k + ' (' + str(v.count('\n')) + ')\n' - whole = whole + v - with st.expander("""### """ + k + ' (' + \ - str(v.count('\n')) + ')'): + nv = v.count('\n') if not reftree else len(v) + if nv: + k1 = k + ' (' + str(nv) + ')' + if reftree: + whole[k1] = v + else: + whole = whole + '\n\n## ' + k1 + '\n' + whole = whole + v + with st.expander("""### """ + k1): with st.container(height=200): - st.code(v, language='ampl') + if reftree: + st.json(v) + else: + st.code(v, language='ampl') return whole @@ -60,13 +73,21 @@ def WriteDict(d): if uploader is not None: model = ReadModel(uploader) filename_upl = uploader.name - subm1, subm2 = MatchSelection(model, srch, fwd, bwd) + subm1, subm2 = MatchSelection(model, srch, fwd, bwd, reftree) bytes1_data = subm1.GetData() bytes2_data = subm2.GetData() with left_column: st.header("NL model", help = 'NL model lines matching the search pattern') - modelNL = WriteDict(bytes1_data) + st.write("Display mode: **" + nl_ref_tree + "**") + modelNLTitle = "NL Model for '" + filename_upl + \ + "' (search pattern: '" + srch + "')" + if reftree: + modelNL = WriteDict(bytes1_data, reftree) + modelNL["title"] = modelNLTitle + modelNL = json.dumps(modelNL, indent=2) + else: + modelNL = modelNLTitle + WriteDict(bytes1_data, reftree) with right_column: st.header("Solver model", help = 'Solver model lines matching the search pattern') @@ -79,10 +100,8 @@ def WriteDict(d): st.sidebar.download_button("Download NL Model", - "# NL Model for '" + filename_upl + \ - "' (search pattern: '" + srch + "')\n" + \ modelNL, - filename_upl + '_NL.mod', + filename_upl + ('_NL.mod' if not reftree else '_NL.json'), help = 'Download current NL model', disabled = ("" == modelNL)) st.sidebar.download_button("Download Solver Model", diff --git a/support/modelexplore/scripts/python/graph.py b/support/modelexplore/scripts/python/graph.py index bbd26f8d7..331179e3e 100644 --- a/support/modelexplore/scripts/python/graph.py +++ b/support/modelexplore/scripts/python/graph.py @@ -23,5 +23,8 @@ def GetNode(self, idx): def AddLink(self, s, d): self._succ[s].append(d) ## Should have no duplicates + def GetSucc(self, n): + return self._succ[n] + def ToText(self): return str(self._nodes) diff --git a/support/modelexplore/scripts/python/matcher.py b/support/modelexplore/scripts/python/matcher.py index 2c74903a3..10cebc76c 100644 --- a/support/modelexplore/scripts/python/matcher.py +++ b/support/modelexplore/scripts/python/matcher.py @@ -15,7 +15,7 @@ def __init__(self): self.data = None -def MatchSubmodel(m: Model, patt: str, fwd: bool, bwd: bool): +def MatchSubmodel(m: Model, patt: str, fwd: bool, bwd: bool, reftree: bool): """ Match a submodel containg the \a pattern, optionally extended by forward/backward @@ -23,6 +23,6 @@ def MatchSubmodel(m: Model, patt: str, fwd: bool, bwd: bool): """ mv1 = ModelView() mv2 = ModelView() - mv1.SetData(m.MatchOrigModel(patt)) + mv1.SetData(m.MatchOrigModel(patt, reftree)) mv2.SetData(m.MatchFinalModel(patt)) return mv1, mv2 diff --git a/support/modelexplore/scripts/python/model.py b/support/modelexplore/scripts/python/model.py index 9dda7233d..d3cd1ffab 100644 --- a/support/modelexplore/scripts/python/model.py +++ b/support/modelexplore/scripts/python/model.py @@ -143,23 +143,23 @@ def GetLinkNode(self, type, index): # Match keyword to the original model - def MatchOrigModel(self, keyw): + def MatchOrigModel(self, keyw, reftree): result = {} - result["NL Variables"] = self._matchRecords(self._vars, keyw, "is_from_nl") - result["NL Defined Variables"] = self._matchRecords(self._dvars, keyw) - result["NL Objectives"] = self._matchRecords(self._objs_NL, keyw) + result["NL Variables"] = self._matchRecords(self._vars, keyw, reftree, "is_from_nl") + result["NL Defined Variables"] = self._matchRecords(self._dvars, keyw, reftree) + result["NL Objectives"] = self._matchRecords(self._objs_NL, keyw, reftree) # result["NL Constraints"] \ # = self._matchRecords(self._cons_NL.get("All"), keyw) result["NL Nonlinear Constraints"] \ - = self._matchRecords(self._cons_NL.get("Nonlinear"), keyw) + = self._matchRecords(self._cons_NL.get("Nonlinear"), keyw, reftree) result["NL Linear Constraints"] \ - = self._matchRecords(self._cons_NL.get("Linear"), keyw) + = self._matchRecords(self._cons_NL.get("Linear"), keyw, reftree) result["NL Logical Constraints"] \ - = self._matchRecords(self._cons_NL.get("Logical"), keyw) + = self._matchRecords(self._cons_NL.get("Logical"), keyw, reftree) result["NL SOS1 Constraints"] \ - = self._matchRecords(self._cons_NL.get("SOS1"), keyw) + = self._matchRecords(self._cons_NL.get("SOS1"), keyw, reftree) result["NL SOS2 Constraints"] \ - = self._matchRecords(self._cons_NL.get("SOS2"), keyw) + = self._matchRecords(self._cons_NL.get("SOS2"), keyw, reftree) return result # Match keyword to the final model @@ -176,20 +176,35 @@ def MatchFinalModel(self, keyw): # Add records containing keyword # @return array of strings - def _matchRecords(self, cnt, keyw, keyNeed1=None): - result = "" + def _matchRecords(self, cnt, keyw, reftree=False, keyNeed1=None): + result = "" if not reftree else {} if cnt is None: return result for i in cnt: if "final" not in i or 1==i["final"]: - pr = str(i) ## TODO printed form - if "printed" in i: - pr = i["printed"] - assert len(pr) - if ';'!=pr[-1]: - pr = pr + ';' + pr = self.StringifyNode(i) if (""==keyw or keyw in pr) \ and (keyNeed1==None \ or (keyNeed1 in i and 1==i[keyNeed1])): - result = result + " \n" + pr ## Markdown: 2x spaces + EOL + if reftree: + result[pr] = self._getSubtree(i) ## dict + else: + result = result + " \n" + pr ## Markdown: 2x spaces + EOL + return result + + def StringifyNode(self, i): + pr = str(i) + if "printed" in i: + pr = i["printed"] + assert len(pr) + if ';'!=pr[-1]: + pr = pr + ';' + return pr + + ## Refomrulation descendants + def _getSubtree(self, node): + result = {} + for ic in self._graph.GetSucc(node["node_index"]): + c = self._graph.GetNode(ic) + result[self.StringifyNode(c)] = self._getSubtree(c) return result