From d6f45da01bcb38f0d62bfb0e92c70ac91756e198 Mon Sep 17 00:00:00 2001 From: Griffin Roupe Date: Thu, 2 Dec 2021 13:12:52 -0500 Subject: [PATCH 01/17] Fix minor bug in automatic English curie name resolution --- src/tranql/web/src/App.js | 31 +++++++++++++++++++++++++------ 1 file changed, 25 insertions(+), 6 deletions(-) diff --git a/src/tranql/web/src/App.js b/src/tranql/web/src/App.js index 9376ac4..ba7eff3 100644 --- a/src/tranql/web/src/App.js +++ b/src/tranql/web/src/App.js @@ -1258,8 +1258,18 @@ class App extends Component { * Very similar to `_resolveIdentifiersFromConcept`, except that is designed for use with a curie instead of a name & biolink type. * This is made to be used so that whenever a user types a curie directly into the query, it can automatically resolve the English name for them. * + * @param {string} curie - A curie, e.g. "MONDO:0005240" + * @param {boolean=false} ignoreCache - When false, this method returns previously cached results for `curie`. + * */ - async _resolveIdentifiersFromCurie(curie) { + async _resolveIdentifiersFromCurie(curie, ignoreCache=false) { + /** If caching is enabled and the curie results are cached, return the cached results */ + if (!ignoreCache && this._autocompleteResolvedIdentifiers[curie]) { + const results = {}; + results[curie] = this._autocompleteResolvedIdentifiers[curie]; + return results; + } + const nodeNormResult = await (await fetch( this.nodeNormalizationURL + '/get_normalized_nodes?' + qs.stringify({ curie }) )).json(); @@ -1719,11 +1729,20 @@ class App extends Component { if (editor.state.completionActive) { this._codeAutoComplete(); } - const currentToken = editor.getTokenAt(editor.getCursor()); - if (currentToken.type === "string" && currentToken.string.includes(":")) { - // It really doesn't need to be a curie here, since it'll just return no results, - // but might as well check if there's a colon to avoid making unnecessary API requests. - this._resolveIdentifiersFromCurie(getCurieFromCMToken(currentToken.string)).catch(() => {}) + /* Traverse through each token in the editor and perform identifier resolution on it if it's a string representing a curie */ + const lines = editor.lineCount(); + for (let lineNumber=0; lineNumber {}, false); + } + } } if (this.embedded) this._debouncedExecuteQuery(); }} From c658a2317bba3e6cca0912217515fbb3082e8105 Mon Sep 17 00:00:00 2001 From: Griffin Roupe Date: Thu, 2 Dec 2021 13:45:18 -0500 Subject: [PATCH 02/17] Relocate codemirror update logic into the _updateCode method. Fix small bug where English curie name resolution didn't work on the initial query loaded with ?query param --- src/tranql/web/src/App.js | 51 ++++++++++++++++++++++++--------------- 1 file changed, 31 insertions(+), 20 deletions(-) diff --git a/src/tranql/web/src/App.js b/src/tranql/web/src/App.js index ba7eff3..ee93707 100644 --- a/src/tranql/web/src/App.js +++ b/src/tranql/web/src/App.js @@ -541,9 +541,39 @@ class App extends Component { * @private */ _updateCode (newCode) { + console.log("UPDATE CODE"); this.setState({ code: newCode }); + + /** + * Perform editor-related tasks upon query updating. + * Perform this on the callback (updated) state, so that the codemirror state is updated. + * + */ + this.setState({}, () => { + const editor = this._codemirror; + if (!editor) return; + if (editor.state.completionActive) { + this._codeAutoComplete(); + } + /* Traverse through each token in the editor and perform identifier resolution on it if it's a string representing a curie */ + const lines = editor.lineCount(); + for (let lineNumber=0; lineNumber {}, false); + } + } + } + if (this.embedded) this._debouncedExecuteQuery(); + }); } /** @@ -1012,7 +1042,7 @@ class App extends Component { node["id"] = id; if (node["category"] !== undefined && node["category"] !== null) { const catArr = node["category"]; - console.log(`CATEGORY=[${catArr}]`); + // console.log(`CATEGORY=[${catArr}]`); let typeArr = catArr.map(this._categoryToType); node["type"] = typeArr; } @@ -1726,25 +1756,6 @@ class App extends Component { value={this.state.code} onBeforeChange={(editor, data, code) => this._updateCode(code)} onChange={(editor) => { - if (editor.state.completionActive) { - this._codeAutoComplete(); - } - /* Traverse through each token in the editor and perform identifier resolution on it if it's a string representing a curie */ - const lines = editor.lineCount(); - for (let lineNumber=0; lineNumber {}, false); - } - } - } - if (this.embedded) this._debouncedExecuteQuery(); }} options={this.state.codeMirrorOptions} autoFocus={true} /> From ba344e13d201640e6a9d9790c1d35b896e6e8697 Mon Sep 17 00:00:00 2001 From: Griffin Roupe Date: Fri, 3 Dec 2021 19:10:18 -0500 Subject: [PATCH 03/17] Quick fix to minor bug in debounce query execution caused by moving update logic to --- src/tranql/web/src/App.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/tranql/web/src/App.js b/src/tranql/web/src/App.js index ee93707..78fe8bf 100644 --- a/src/tranql/web/src/App.js +++ b/src/tranql/web/src/App.js @@ -541,7 +541,6 @@ class App extends Component { * @private */ _updateCode (newCode) { - console.log("UPDATE CODE"); this.setState({ code: newCode }); @@ -572,8 +571,12 @@ class App extends Component { } } } - if (this.embedded) this._debouncedExecuteQuery(); }); + /** + * Note that the debounced query execution in embedded mode still occurs in the codemirror's `onChange` callback. + * This is because initially loading the ?query param into the graph should execute *immediately*, not on a debounce, + * (and there are a couple other issues other than the immediate execution that arise from having it here related the ?query loading). + */ } /** @@ -1756,6 +1759,7 @@ class App extends Component { value={this.state.code} onBeforeChange={(editor, data, code) => this._updateCode(code)} onChange={(editor) => { + if (this.embedded) this._debouncedExecuteQuery(); }} options={this.state.codeMirrorOptions} autoFocus={true} /> From 75c16088f08233affceaeac7ea6d348ce656d4fc Mon Sep 17 00:00:00 2001 From: Griffin Roupe Date: Mon, 6 Dec 2021 17:12:07 -0500 Subject: [PATCH 04/17] Updates embedded TranQL window to only use 100vh (no overflow) instead of header + 100vh graph. --- src/tranql/web/package.json | 2 ++ src/tranql/web/src/App.js | 8 ++++++-- src/tranql/web/src/TranQLEmbedded.js | 18 ++++++++++++++---- 3 files changed, 22 insertions(+), 6 deletions(-) diff --git a/src/tranql/web/package.json b/src/tranql/web/package.json index 3a50d1a..6df289f 100644 --- a/src/tranql/web/package.json +++ b/src/tranql/web/package.json @@ -6,6 +6,7 @@ "@emotion/core": "^10.1.1", "@material-ui/core": "^3.9.2", "@material-ui/lab": "^3.0.0-alpha.30", + "@popperjs/core": "^2.8.0", "abortcontroller-polyfill": "^1.7.3", "bootstrap": "^4.3.1", "classnames": "^2.3.1", @@ -41,6 +42,7 @@ "react-json-tree": "^0.11.2", "react-notifications": "^1.4.3", "react-scripts": "^2.1.8", + "react-sizeme": "^3.0.2", "react-spinners": "^0.5.3", "react-split-pane": "^0.1.92", "react-table": "^6.9.2", diff --git a/src/tranql/web/src/App.js b/src/tranql/web/src/App.js index 78fe8bf..e138441 100644 --- a/src/tranql/web/src/App.js +++ b/src/tranql/web/src/App.js @@ -2438,7 +2438,11 @@ class App extends Component { // & hydrate state accordingly this._handleQueryString (); // Hydrate persistent state from local storage - if (!this.embedded) this._hydrateState (); + if (!this.embedded) { + this._hydrateState (); + // Make sure that the code loaded from localStorage goes through `_updateCode` + this.setState({}, () => this._updateCode(this.state.code)); + } // Populate the cache viewer this._updateCacheViewer (); @@ -2581,7 +2585,7 @@ class App extends Component { {this._renderAnswerViewer()} {this._renderBanner()} -
+
{ this.state.showCodeMirror ? ( diff --git a/src/tranql/web/src/TranQLEmbedded.js b/src/tranql/web/src/TranQLEmbedded.js index 0d2b8f1..624c269 100644 --- a/src/tranql/web/src/TranQLEmbedded.js +++ b/src/tranql/web/src/TranQLEmbedded.js @@ -1,5 +1,6 @@ import React, { useState } from 'react'; import { Button } from 'reactstrap'; +import { SizeMe } from 'react-sizeme'; import { GridLoader } from 'react-spinners'; import { FaFrown, FaPlayCircle } from 'react-icons/fa'; import { css } from '@emotion/core'; @@ -34,10 +35,19 @@ export default function TranQLEmbedded({ embedMode, graphLoading, graph, renderF {renderAnswerViewer({asModal: false})}
); - else return renderForceGraph ( - graph, { - ref: graphRefCallback - }); + else return ( + + { + ({size}) => + renderForceGraph ( + graph, { + ref: graphRefCallback, + height: size.height + } + ) + } + + ); } return ( From 3ffd64db9d233c1528f75c23dfcdfe254f4dcf42 Mon Sep 17 00:00:00 2001 From: Griffin Roupe Date: Mon, 6 Dec 2021 17:17:02 -0500 Subject: [PATCH 05/17] Re-enables navigation tools on embedded mode now that embedded window does not have overflow --- src/tranql/web/src/App.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/tranql/web/src/App.js b/src/tranql/web/src/App.js index e138441..7f611e0 100644 --- a/src/tranql/web/src/App.js +++ b/src/tranql/web/src/App.js @@ -1583,7 +1583,8 @@ class App extends Component { if (this.fg) { // Performing actions such as dragging a node will reset fg.controls().enabled back to true automatically, // thus it's necessary to hook into the TrackballControls and make sure that they're immediately re-disabled. - if (this.embedded) { + // *Currently disabled always.* + if (this.embedded && false) { const controls = this.fg.controls(); controls.enabled = false; const _update = controls.update.bind(controls); From f237c8c28067349b0609f602166f83ab1fd529ad Mon Sep 17 00:00:00 2001 From: Griffin Roupe Date: Mon, 6 Dec 2021 17:33:36 -0500 Subject: [PATCH 06/17] Add query param ?answer_viewer for use when embedded param is true, automatically showing the answer viewer instead of the force graph in embedded mode --- src/tranql/web/src/App.js | 18 ++++++++++++++++-- src/tranql/web/src/TranQLEmbedded.js | 4 ++-- 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/src/tranql/web/src/App.js b/src/tranql/web/src/App.js index 7f611e0..01542d7 100644 --- a/src/tranql/web/src/App.js +++ b/src/tranql/web/src/App.js @@ -398,9 +398,10 @@ class App extends Component { activeModal : null, - exampleQueries : require("./static/app_data/example_queries.js") + exampleQueries : require("./static/app_data/example_queries.js"), //showAnswerViewer : true + showAnswerViewerOnLoad: false }; // This is a cache that stores results from `this._resolveIdentifiersFromConcept` for use in codemirror tooltips. @@ -2372,8 +2373,18 @@ class App extends Component { // `ignoreQueryPrefix` automatically truncates the leading question mark within a query string in order to prevent it from being interpreted as part of it const params = qs.parse(window.location.search, { ignoreQueryPrefix : true }); + /** + * Parse params into vars: + * - q|query + * - embedded: full|graph|simple|true|false + * - answer_viewer: true|false + */ const tranqlQuery = params.q || params.query; - const embedded = params.embed; + const { + embed: embedded, + answer_viewer: showAnswerViewer + } = params; + if (tranqlQuery !== undefined) { this._queryExecOnLoad = tranqlQuery; } @@ -2413,6 +2424,8 @@ class App extends Component { length: 0 }); } + // Note: this option is only intended for use within an embedded page, since it doesn't make much sense in a normal context. + this.setState({ showAnswerViewerOnLoad: showAnswerViewer === "true" || showAnswerViewer === "" }); } /** * Perform any necessary cleanup before being unmounted @@ -2487,6 +2500,7 @@ class App extends Component { render() { // Render just the graph if the app is being embedded if (this.embedded) return { useAnswerViewer(!answerViewer); From cfe3b141fea4b7aa25e6ebd0e11d5b47f7a4329c Mon Sep 17 00:00:00 2001 From: Griffin Roupe Date: Mon, 6 Dec 2021 18:38:47 -0500 Subject: [PATCH 07/17] Removes ?answer_viewer for ?use_last_view. Adds embeddedLocalStorage for explicitly using localStorage while embedded. Adds ?use_last_view, which will show answer viewer/force graph as the default view when embedded, depending on which was last used --- src/tranql/web/src/App.js | 33 +++++++++++++++++++--------- src/tranql/web/src/TranQLEmbedded.js | 11 +++++++--- 2 files changed, 31 insertions(+), 13 deletions(-) diff --git a/src/tranql/web/src/App.js b/src/tranql/web/src/App.js index 01542d7..5665341 100644 --- a/src/tranql/web/src/App.js +++ b/src/tranql/web/src/App.js @@ -401,7 +401,8 @@ class App extends Component { exampleQueries : require("./static/app_data/example_queries.js"), //showAnswerViewer : true - showAnswerViewerOnLoad: false + // showAnswerViewerOnLoad: false + useLastUsedView: false }; // This is a cache that stores results from `this._resolveIdentifiersFromConcept` for use in codemirror tooltips. @@ -2376,13 +2377,15 @@ class App extends Component { /** * Parse params into vars: * - q|query - * - embedded: full|graph|simple|true|false - * - answer_viewer: true|false + * - embedded: full | (graph|simple|true|"") | (false|) + * - answer_viewer: (true|"") | (false|) <- DISABLED + * - use_last_view: (true|"") | (false|) */ const tranqlQuery = params.q || params.query; const { embed: embedded, - answer_viewer: showAnswerViewer + use_last_view: useLastUsedView + // answer_viewer: showAnswerViewer } = params; if (tranqlQuery !== undefined) { @@ -2412,20 +2415,30 @@ class App extends Component { this.setState({ useCache : false }); - // Disable localStorage on embedded websites. Even though settings/cache is disabled, - // some other parts of the app also write/read from localStorage, for example: - // when a query is executed, it stores the query in local stoarge under the key `code`. - window.localStorage.__proto__ = Object.create({ + // Maintain localStorage under window.embeddedLocalStorage such that + // the App can only use localStorage while embedded when explcitly intended. + window.embeddedLocalStorage = window.localStorage; + const _localStorage = window.localStorage; + // localStorage is a property of window with a getter but no setter, so it has to be fully deleted before setting it to a new value. + delete window.localStorage; + // Create a skeleton version of localStorage, in which persistent methods are overwritten while preserving the full API. + const skeletonStorage = Object.assign({ setItem: () => {}, removeItem: () => {}, key: () => {}, getItem: () => {}, removeItem: () => {}, length: 0 + }, _localStorage); + // Create the new localStorage property with the skeleton storage. + Object.defineProperty(window, "localStorage", { + get: () => skeletonStorage }); } // Note: this option is only intended for use within an embedded page, since it doesn't make much sense in a normal context. - this.setState({ showAnswerViewerOnLoad: showAnswerViewer === "true" || showAnswerViewer === "" }); + // Note2: *DISABLED* + // this.setState({ showAnswerViewerOnLoad: showAnswerViewer === "true" || showAnswerViewer === "" }); + this.setState({ useLastUsedView: useLastUsedView === "true" || useLastUsedView === "" }); } /** * Perform any necessary cleanup before being unmounted @@ -2500,7 +2513,7 @@ class App extends Component { render() { // Render just the graph if the app is being embedded if (this.embedded) return { - useAnswerViewer(!answerViewer); + const newValue = !answerViewer; + useAnswerViewer(newValue); + // localStorage is disabled on embedded mode. Need to use embeddedLocalStorage to write/read. + window.embeddedLocalStorage.setItem('defaultUseAnswerViewer', JSON.stringify(newValue)); } const renderBody = () => { if (answerViewer) return ( From d476bef4f31869e89cab6572a36f619ae647c8dd Mon Sep 17 00:00:00 2001 From: Griffin Roupe Date: Tue, 7 Dec 2021 14:05:57 -0500 Subject: [PATCH 08/17] Fixes significant bug with _translateGraph that caused numerous issues with the UI. Fixes bug that was previously noted in a comment. Caused the Show Graph button to display the schema if no actual graph had been loaded yet. Also created a bug in which a race condition could cause the schemaMessage to be misconfigured and subsequently break the answer viewer component. --- src/tranql/web/src/App.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/tranql/web/src/App.js b/src/tranql/web/src/App.js index 5665341..527adda 100644 --- a/src/tranql/web/src/App.js +++ b/src/tranql/web/src/App.js @@ -998,6 +998,8 @@ class App extends Component { cond.schemaMessage = message; } else { + if (!message.message) console.trace(); + console.log(Object.keys(message)); cond.message = message; cond.record = this._cacheFormat(message); } @@ -1085,9 +1087,7 @@ class App extends Component { }; message.knowledge_graph.edges = newLinkArray; }; - // NOTE: Pretty sure this is the culprit of a bug where the graph is set to the schema on page load, - // causing the graph view to display the schema until a query is made overriding it. - this._configureMessage (message,false,false); + this._configureMessage (message,false,schema); this.setState({},() => { if (typeof noRenderChain === "undefined") noRenderChain = false; if (typeof schema === "undefined") schema = this.state.schemaViewerActive && this.state.schemaViewerEnabled; From d87c22c8f077675ec1bca5a671fc63d1d5a7d242 Mon Sep 17 00:00:00 2001 From: Griffin Roupe Date: Tue, 7 Dec 2021 14:08:07 -0500 Subject: [PATCH 09/17] Delete spurious logs added in debugging for previous commit --- src/tranql/web/src/App.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/tranql/web/src/App.js b/src/tranql/web/src/App.js index 527adda..99ae213 100644 --- a/src/tranql/web/src/App.js +++ b/src/tranql/web/src/App.js @@ -998,8 +998,6 @@ class App extends Component { cond.schemaMessage = message; } else { - if (!message.message) console.trace(); - console.log(Object.keys(message)); cond.message = message; cond.record = this._cacheFormat(message); } From a0a08ade14fde6eb9416f67efb06e3dd77e07a19 Mon Sep 17 00:00:00 2001 From: Griffin Roupe Date: Wed, 8 Dec 2021 15:50:30 -0500 Subject: [PATCH 10/17] Adds sorting for concept->concept suggestions based on edge count between concepts --- src/tranql/web/src/autocomplete.js | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/src/tranql/web/src/autocomplete.js b/src/tranql/web/src/autocomplete.js index 163ec63..6303f43 100644 --- a/src/tranql/web/src/autocomplete.js +++ b/src/tranql/web/src/autocomplete.js @@ -450,13 +450,22 @@ export default function autoComplete () { } }) } - validConcepts = validConcepts.unique().map((concept) => { - return { - displayText: concept, - text: concept, - replaceText: currentConcept - }; - }); + const getEdgeCount = (sourceName, targetName) => ( + graph.edges.filter((edge) => edge.source_id === sourceName && edge.target_id === targetName).length + ); + const getConnectivityScore = (concept) => { + // Since select statement completion isn't really supported on backwards arrows yet, just sort as if it's a forwards arrow. + return getEdgeCount(previousConcept, concept); + } + validConcepts = validConcepts.unique() + .sort((conceptA, conceptB) => getConnectivityScore(conceptB) - getConnectivityScore(conceptA)) + .map((concept) => { + return { + displayText: concept, + text: concept, + replaceText: currentConcept + }; + }); } setHint(validConcepts); From bcaf2932eb2f72ec65a0ace935115786fb0bf16f Mon Sep 17 00:00:00 2001 From: Griffin Roupe Date: Wed, 8 Dec 2021 17:18:05 -0500 Subject: [PATCH 11/17] Add sorting of predicate suggestions in autocomplete based on edge scoring. Removes the redundant concept hints on predicate suggestions. Note that edge scoring is not yet implemented in tranql_schema.py, so this sorting will not be apparent. --- src/tranql/web/src/autocomplete.js | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/src/tranql/web/src/autocomplete.js b/src/tranql/web/src/autocomplete.js index 6303f43..4efe49e 100644 --- a/src/tranql/web/src/autocomplete.js +++ b/src/tranql/web/src/autocomplete.js @@ -308,6 +308,17 @@ export default function autoComplete () { const secondLastToken = lastStatement[lastStatement.length-2]; const thirdLastToken = lastStatement[lastStatement.length-3]; + // It's difficult to tell when parsing "SELECT disease->| FROM ...", whether "FROM" is the second concept, or if it starts the FROM statement. + // In the ordinary parser, "FROM" will be parsed as a concept name here, but obviously this is almost 99% of the time not the intention when + // using autocomplete. + // Thus, in autocompletion specifcially, let's blacklist FROM as a concept name (the incomplete parser itself will still parse it as a concept name), + // for the sake of convenience. Of course, if by some chance "from" becomes a valid concept in the biolink model, this will a niche bug, but it's probably + // better for the sake of avoiding the headache that would be working around this fringe case. + let hasNoNextConcept = (lastStatementComplete.length - lastStatement.length) == 0; + let nextConcept = hasNoNextConcept ? undefined : concept(lastStatementComplete[lastStatement.length]) ; + // See https://github.com/frostyfan109/tranql/issues/117 for an older detailed breakdown on the issue. + if (typeof nextConcept === "string" && nextConcept.toLowerCase() === "from") nextConcept = undefined; + console.log(statementType, lastStatement, lastToken); // Try/catch the entirety of the logic @@ -333,19 +344,12 @@ export default function autoComplete () { lastToken[1] !== undefined ? lastToken[1] : "" ]); let previousConcept = concept(secondLastToken); - // May be undefined if there is no next concept - let has_no_next_concept = (lastStatementComplete.length - lastStatement.length) == 0; - let nextConcept = has_no_next_concept? undefined : concept(lastStatementComplete[lastStatement.length]) ; - // See https://github.com/frostyfan109/tranql/issues/117 for why this approach doesn't work - - const backwards = isBackwardsPredicate (currentPredicate); console.log ([previousConcept, currentPredicate, nextConcept]); // Should replace this method with reduce - const allEdges = graph.edges.filter((edge) => { if (backwards) { return edge.target_id === previousConcept && @@ -367,12 +371,12 @@ export default function autoComplete () { } }); const uniqueEdges = Object.values(uniqueEdgeMap); - validConcepts = uniqueEdges.map((edge) => { + validConcepts = uniqueEdges.sort((e1, e2) => e2.score - e1.score).map((edge) => { const replaceText = currentPredicate[1]; // const actualText = type + currentPredicate[2]; const conceptHint = " (" + (backwards ? edge.source_id : edge.target_id) + ")"; const actualText = edge.type; - const displayText = edge.type + conceptHint; + const displayText = edge.type; return { displayText: displayText, text: actualText, From 543e6803b4403e1fded81b6c36f1b771b1ffaf4b Mon Sep 17 00:00:00 2001 From: Griffin Roupe Date: Wed, 8 Dec 2021 18:48:35 -0500 Subject: [PATCH 12/17] Adds prototype scoring mechanism to edges provided by redis reasoner --- src/tranql/tranql_schema.py | 63 ++++++++++++++++++++++++++++++------- src/tranql/util.py | 10 ++++++ 2 files changed, 62 insertions(+), 11 deletions(-) diff --git a/src/tranql/tranql_schema.py b/src/tranql/tranql_schema.py index dfae8ea..672b624 100644 --- a/src/tranql/tranql_schema.py +++ b/src/tranql/tranql_schema.py @@ -9,7 +9,11 @@ import time import threading from PLATER.services.util.graph_adapter import GraphInterface -from tranql.util import snake_case +from tranql.util import snake_case, title_case +import logging + +logger = logging.getLogger(__name__) + class NetworkxGraph: def __init__(self): @@ -25,14 +29,19 @@ def get_node (self, identifier, properties=None): nodes = self.net.nodes(data=True) filtered = [i for i in nodes if i[0] == identifier] return filtered[0] if len(filtered) > 0 else None - def get_edge (self, start, end, properties=None): - result = None - for e in self.net.edges: - #print (f"----- {start} {end} | {e[0]} {e[2]}") - if e[0] == start and e[1] == end: - result = e - break - return result + """ Returns an edge from the graph or None. Note that the edge attr dict returned is mutable. + :param predicate: If None, will return the first edge found between start->end if one exists. + :return: (start, predicate, end, attr_data) | None + """ + def get_edge (self, start, end, predicate=None, properties=None): + try: + edges = self.net[start][end] + if predicate is None: + predicate = list(edges.keys())[0] + edge_data = edges[predicate] + return (start, predicate, end, edge_data) + except: + return None def get_nodes (self,**kwargs): return self.net.nodes(**kwargs) def get_edges (self,**kwargs): @@ -161,7 +170,7 @@ def _create_graph_interface(service_name, redis_conf, tranql_config): **redis_connection_details ) - def _get_adatpter(self, name): + def _get_adapter(self, name): if name not in RedisAdapter.registry_adapters: raise ValueError(f"Redis backend with name {name} not registered.") return RedisAdapter.registry_adapters.get(name) @@ -174,7 +183,7 @@ def set_adapter(self, name, redis_config, tranql_config): ) def get_schema(self, name): - gi: GraphInterface = self._get_adatpter(name) + gi: GraphInterface = self._get_adapter(name) schema = gi.get_schema(force_update=True) return schema @@ -337,6 +346,38 @@ def add_layer (self, layer, name=None): for link in links: #print (f" {source_name}->{target_type} [{link}]") self.schema_graph.add_edge (source_name, link, target_type, {"reasoner":[name]}) + self.decorate_schema(layer, name) + + """ + Manually perform scoring on the Redis reasoner, since it supports it. If more reasoners support it, + this should obviously be changed to be done dynamically on each reasoner and should probably be generalized + to attribute decoration rather than just scoring. + """ + def decorate_schema(self, layer, name=None): + if name != "redis": + return + + def toBiolink(concept): + return "biolink:" + title_case(concept) + + redis_adapter = RedisAdapter() + adapter = redis_adapter._get_adapter(name) + for source_name, targets_list in layer.items (): + source_node = self.get_node (node_id=source_name) + for target_type, links in targets_list.items (): + target_node = self.get_node (node_id=target_type) + # Get the number of total transitions from source_node->target_node + logger.critical(f"MATCH (c:`{toBiolink(source_name)}`)-[e]->(b:`{toBiolink(target_type)}`) return COUNT(e) as cnt") + total_count = adapter.driver.run_sync(f"MATCH (c:`{toBiolink(source_name)}`)-[e]->(b:`{toBiolink(target_type)}`) return COUNT(e) as cnt")["results"][0]["data"][0]["row"][0] + if isinstance(links, str): + links = [links] + for link in links: + (_, _, _, edge_data) = self.schema_graph.get_edge (source_name, target_type, link) + # Then score each individual link based on the proportion of said edge between source_node->target_node + # relative to the total edges between the concepts. + individual_count = adapter.driver.run_sync(f"MATCH (c:`{toBiolink(source_name)}`)-[e:`biolink:{link}`]->(b:`{toBiolink(target_type)}`) return COUNT(e) as cnt")["results"][0]["data"][0]["row"][0] + edge_data["score"] = individual_count / total_count + def get_edge (self, plan, source_name, source_type, target_name, target_type, predicate, edge_direction): diff --git a/src/tranql/util.py b/src/tranql/util.py index 70cacf6..02cf75d 100644 --- a/src/tranql/util.py +++ b/src/tranql/util.py @@ -408,6 +408,16 @@ def deep_merge(source, destination, no_list_repeat=True): return destination +def camel_case(arg: str): + """Convert snake_case string to camelCase""" + split = arg.split("_") + return split[0] + ''.join([i.title() for i in split[1:]]) + +def title_case(arg: str): + """Convert snake_case string to TitleCase""" + split = arg.split("_") + return ''.join([i.title() for i in arg.split("_")]) + def snake_case(arg: str): """Convert string to snake_case. Non-alphanumeric characters are replaced with _. From ee8f4f8fef7857bd3bc664ca000233f4b0649327 Mon Sep 17 00:00:00 2001 From: Griffin Roupe Date: Thu, 9 Dec 2021 15:33:50 -0500 Subject: [PATCH 13/17] Updates the way that scoring data is obtained in decorate_schema --- src/tranql/tranql_schema.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/src/tranql/tranql_schema.py b/src/tranql/tranql_schema.py index 672b624..5887de2 100644 --- a/src/tranql/tranql_schema.py +++ b/src/tranql/tranql_schema.py @@ -362,20 +362,24 @@ def toBiolink(concept): redis_adapter = RedisAdapter() adapter = redis_adapter._get_adapter(name) + schema_summary = adapter.summary for source_name, targets_list in layer.items (): source_node = self.get_node (node_id=source_name) + biolink_source_name = toBiolink(source_name) + if not source_name in schema_summary: continue for target_type, links in targets_list.items (): target_node = self.get_node (node_id=target_type) - # Get the number of total transitions from source_node->target_node - logger.critical(f"MATCH (c:`{toBiolink(source_name)}`)-[e]->(b:`{toBiolink(target_type)}`) return COUNT(e) as cnt") - total_count = adapter.driver.run_sync(f"MATCH (c:`{toBiolink(source_name)}`)-[e]->(b:`{toBiolink(target_type)}`) return COUNT(e) as cnt")["results"][0]["data"][0]["row"][0] + biolink_target_type = toBiolink(target_type) + if not biolink_target_type in schema_summary[biolink_source_name]: continue + edge_summary = schema_summary[biolink_source_name][biolink_target_type] + total_count = sum(edge_summary.values()) if isinstance(links, str): links = [links] for link in links: + biolink_link = "biolink:" + link + if not biolink_link in edge_summary: continue (_, _, _, edge_data) = self.schema_graph.get_edge (source_name, target_type, link) - # Then score each individual link based on the proportion of said edge between source_node->target_node - # relative to the total edges between the concepts. - individual_count = adapter.driver.run_sync(f"MATCH (c:`{toBiolink(source_name)}`)-[e:`biolink:{link}`]->(b:`{toBiolink(target_type)}`) return COUNT(e) as cnt")["results"][0]["data"][0]["row"][0] + individual_count = edge_summary[biolink_link] edge_data["score"] = individual_count / total_count From 6fe1f374748b5c874a809e16e872d10049ffce22 Mon Sep 17 00:00:00 2001 From: Griffin Roupe Date: Thu, 9 Dec 2021 18:19:08 -0500 Subject: [PATCH 14/17] Update plater graph interface mock to have an empty field. --- tests/test_tranql.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/test_tranql.py b/tests/test_tranql.py index 1690240..5ab3a7a 100644 --- a/tests/test_tranql.py +++ b/tests/test_tranql.py @@ -2090,6 +2090,7 @@ def __init__(self, limit , skip, options_set): self.limit = limit self.skip = skip self.options_set = options_set + self.summary = {} async def answer_trapi_question(self, message, options={}, timeout=0): assert message @@ -2161,6 +2162,7 @@ class graph_Inteface_mock: def __init__(self): self.get_schema_called =False self.answer_trapi_question_called =False + self.summary = {} def get_schema(self, force_update=False): self.get_schema_called = True return {} @@ -2231,6 +2233,7 @@ def __init__(self, limit, skip, options_set): self.options_set = options_set self.answer_trapi_question self.is_called = False + self.summary = {} def get_schema(self, *args, **kwargs): return { From 02701c92fb09899dafc4491eff46ca43a1394db1 Mon Sep 17 00:00:00 2001 From: Griffin Roupe Date: Fri, 10 Dec 2021 09:30:39 -0500 Subject: [PATCH 15/17] Fix typo in tranql_schema --- src/tranql/tranql_schema.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tranql/tranql_schema.py b/src/tranql/tranql_schema.py index 5887de2..e70e929 100644 --- a/src/tranql/tranql_schema.py +++ b/src/tranql/tranql_schema.py @@ -366,7 +366,7 @@ def toBiolink(concept): for source_name, targets_list in layer.items (): source_node = self.get_node (node_id=source_name) biolink_source_name = toBiolink(source_name) - if not source_name in schema_summary: continue + if not biolink_source_name in schema_summary: continue for target_type, links in targets_list.items (): target_node = self.get_node (node_id=target_type) biolink_target_type = toBiolink(target_type) From 5082a033f6f26b6409d5cfe0106f6ef1cf6865ac Mon Sep 17 00:00:00 2001 From: Griffin Roupe Date: Fri, 10 Dec 2021 16:00:48 -0500 Subject: [PATCH 16/17] Adds rudimentary localStorage cache for curie-to-english-name resolution --- src/tranql/web/src/App.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/tranql/web/src/App.js b/src/tranql/web/src/App.js index 99ae213..a0937f9 100644 --- a/src/tranql/web/src/App.js +++ b/src/tranql/web/src/App.js @@ -1324,6 +1324,7 @@ class App extends Component { _updateResolvedIdentifiers(results) { // Add results to the cache. this._autocompleteResolvedIdentifiers = { ...results, ...this._autocompleteResolvedIdentifiers }; + localStorage.setItem('autocompleteIdentifiers', JSON.stringify(this._autocompleteResolvedIdentifiers)); // Update codemirror tooltips with new cached results. this._codemirror.state.resolvedIdentifiers = this._autocompleteResolvedIdentifiers; } @@ -2465,6 +2466,9 @@ class App extends Component { // Hydrate persistent state from local storage if (!this.embedded) { this._hydrateState (); + // This is a class field, not a state variable, so it needs to be manually loaded. It also has to update codemirror state, + // which is another reason it needs to be manualy handled. + this._updateResolvedIdentifiers(JSON.parse(localStorage.getItem('autocompleteIdentifiers')) || {}); // Make sure that the code loaded from localStorage goes through `_updateCode` this.setState({}, () => this._updateCode(this.state.code)); } From a04130cd53d978b63c78562cf784fd44a509689d Mon Sep 17 00:00:00 2001 From: YaphetKG <45075777+YaphetKG@users.noreply.github.com> Date: Tue, 4 Jan 2022 15:54:26 -0500 Subject: [PATCH 17/17] Update requirements.txt Updating plater version for summary fix --- requirements.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index ecb04bb..302bd87 100644 --- a/requirements.txt +++ b/requirements.txt @@ -19,8 +19,8 @@ redisgraph==2.2.4 requests==2.25.1 requests-cache==0.4.13 requests-mock==1.5.2 -PLATER-GRAPH==1.9.8 +git+https://github.com/helxplatform/Plater.git@v1.9.9 bmt==0.7.2 git+https://github.com/ranking-agent/reasoner.git git+https://github.com/TranslatorSRI/reasoner-pydantic@v1.0.0#egg=reasoner-pydantic -git+https://github.com/TranslatorSRI/reasoner-converter@1.2.4#egg=reasoner-converter \ No newline at end of file +git+https://github.com/TranslatorSRI/reasoner-converter@1.2.4#egg=reasoner-converter