diff --git a/epdb/logic.py b/epdb/logic.py index d7d8d8cd..7d0fce9d 100644 --- a/epdb/logic.py +++ b/epdb/logic.py @@ -895,9 +895,10 @@ class SearchManager(object): class SNode(object): - def __init__(self, smiles: str, depth: int): + def __init__(self, smiles: str, depth: int, app_domain_assessment: dict = None): self.smiles = smiles self.depth = depth + self.app_domain_assessment = app_domain_assessment def __hash__(self): return hash(self.smiles) @@ -1040,7 +1041,7 @@ class SPathway(object): def depth(self): return max([v.depth for v in self.smiles_to_node.values()]) - def _get_nodes_for_depth(self, depth: int): + def _get_nodes_for_depth(self, depth: int) -> List[SNode]: if depth == 0: return self.root_nodes @@ -1051,7 +1052,7 @@ class SPathway(object): return sorted(res, key=lambda x: x.smiles) - def _get_edges_for_depth(self, depth: int): + def _get_edges_for_depth(self, depth: int) -> List[SEdge]: res = [] for e in self.edges: for n in e.educts: @@ -1076,15 +1077,44 @@ class SPathway(object): new_tp = False if substrates: for sub in substrates: + + if sub.app_domain_assessment is None: + if self.prediction_setting.model: + if self.prediction_setting.model.app_domain: + app_domain_assessment = self.prediction_setting.model.app_domain.assess(sub.smiles)[0] + + if self.persist is not None: + n = self.snode_persist_lookup[sub] + + assert n.id is not None, "Node has no id! Should have been saved already... aborting!" + node_data = n.simple_json() + node_data['image'] = f"{n.url}?image=svg" + app_domain_assessment['assessment']['node'] = node_data + + n.kv['app_domain_assessment'] = app_domain_assessment + n.save() + + sub.app_domain_assessment = app_domain_assessment + + candidates = self.prediction_setting.expand(self, sub) + # candidates is a List of PredictionResult. The length of the List is equal to the number of rules for cand_set in candidates: if cand_set: new_tp = True + # cand_set is a PredictionResult object that can consist of multiple candidate reactions for cand in cand_set: cand_nodes = [] + # candidate reactions can have multiple fragments for c in cand: if c not in self.smiles_to_node: - self.smiles_to_node[c] = SNode(c, sub.depth + 1) + # For new nodes do an AppDomain Assessment if an AppDomain is attached + app_domain_assessment = None + if self.prediction_setting.model: + if self.prediction_setting.model.app_domain: + app_domain_assessment = self.prediction_setting.model.app_domain.assess(c)[0] + + self.smiles_to_node[c] = SNode(c, sub.depth + 1, app_domain_assessment) node = self.smiles_to_node[c] cand_nodes.append(node) @@ -1097,18 +1127,30 @@ class SPathway(object): if len(substrates) == 0 or from_node is not None: self.done = True - # Check if we need to write back data to database + # Check if we need to write back data to the database if new_tp and self.persist: self._sync_to_pathway() - # call save to update internal modified field + # call save to update the internal modified field self.persist.save() - def _sync_to_pathway(self): + def _sync_to_pathway(self) -> None: logger.info("Updating Pathway with SPathway") for snode in self.smiles_to_node.values(): if snode not in self.snode_persist_lookup: n = Node.create(self.persist, snode.smiles, snode.depth) + + if snode.app_domain_assessment is not None: + app_domain_assessment = snode.app_domain_assessment + + assert n.id is not None, "Node has no id! Should have been saved already... aborting!" + node_data = n.simple_json() + node_data['image'] = f"{n.url}?image=svg" + app_domain_assessment['assessment']['node'] = node_data + + n.kv['app_domain_assessment'] = app_domain_assessment + n.save() + self.snode_persist_lookup[snode] = n for sedge in self.edges: @@ -1130,7 +1172,6 @@ class SPathway(object): self.sedge_persist_lookup[sedge] = e logger.info("Update done!") - pass def to_json(self): nodes = [] diff --git a/epdb/models.py b/epdb/models.py index 61ef704c..55ca1dd9 100644 --- a/epdb/models.py +++ b/epdb/models.py @@ -912,7 +912,8 @@ class Pathway(EnviPathModel, AliasMixin, ScenarioMixin): 'reaction_probability': link['reaction_probability'], 'scenarios': link['scenarios'], 'source': node_url_to_idx[link['start_node_urls'][0]], - 'target': pseudo_idx + 'target': pseudo_idx, + 'app_domain': link.get('app_domain', None) } adjusted_links.append(new_link) @@ -927,7 +928,8 @@ class Pathway(EnviPathModel, AliasMixin, ScenarioMixin): 'reaction_probability': link['reaction_probability'], 'scenarios': link['scenarios'], 'source': pseudo_idx, - 'target': node_url_to_idx[target] + 'target': node_url_to_idx[target], + 'app_domain': link.get('app_domain', None) } adjusted_links.append(new_link) @@ -1044,6 +1046,8 @@ class Node(EnviPathModel, AliasMixin, ScenarioMixin): return '{}/node/{}'.format(self.pathway.url, self.uuid) def d3_json(self): + app_domain_data = self.get_app_domain_assessment_data() + return { "depth": self.depth, "url": self.url, @@ -1053,6 +1057,10 @@ class Node(EnviPathModel, AliasMixin, ScenarioMixin): "name": self.default_node_label.name, "smiles": self.default_node_label.smiles, "scenarios": [{'name': s.name, 'url': s.url} for s in self.scenarios.all()], + "app_domain": { + 'inside_app_domain': app_domain_data['assessment']['inside_app_domain'] if app_domain_data else None, + 'uncovered_functional_groups': False, + } } @staticmethod @@ -1078,6 +1086,32 @@ class Node(EnviPathModel, AliasMixin, ScenarioMixin): def as_svg(self): return IndigoUtils.mol_to_svg(self.default_node_label.smiles) + def get_app_domain_assessment_data(self): + data = self.kv.get('app_domain_assessment', None) + + if data: + rule_ids = dict() + for e in Edge.objects.filter(start_nodes__in=[self]): + for r in e.edge_label.rules.all(): + rule_ids[str(r.uuid)] = e + + + for t in data['assessment']['transformations']: + if t['rule']['uuid'] in rule_ids: + t['is_predicted'] = True + t['edge'] = rule_ids[t['rule']['uuid']].simple_json() + + return data + + + def simple_json(self, include_description=False): + res = super().simple_json() + name = res.get('name', None) + if name == 'no name': + res['name'] = self.default_node_label.name + + return res + class Edge(EnviPathModel, AliasMixin, ScenarioMixin): pathway = models.ForeignKey('epdb.Pathway', verbose_name='belongs to', on_delete=models.CASCADE, db_index=True) @@ -1090,19 +1124,44 @@ class Edge(EnviPathModel, AliasMixin, ScenarioMixin): return '{}/edge/{}'.format(self.pathway.url, self.uuid) def d3_json(self): - return { + edge_json = { 'name': self.name, 'id': self.url, 'url': self.url, 'image': self.url + '?image=svg', 'reaction': {'name': self.edge_label.name, 'url': self.edge_label.url } if self.edge_label else None, 'reaction_probability': self.kv.get('probability'), - # TODO 'start_node_urls': [x.url for x in self.start_nodes.all()], 'end_node_urls': [x.url for x in self.end_nodes.all()], "scenarios": [{'name': s.name, 'url': s.url} for s in self.scenarios.all()], } + for n in self.start_nodes.all(): + app_domain_data = n.get_app_domain_assessment_data() + + if app_domain_data: + for t in app_domain_data['assessment']['transformations']: + if 'edge' in t and t['edge']['uuid'] == str(self.uuid): + passes_app_domain = ( + t['local_compatibility'] >= app_domain_data['ad_params']['local_compatibility_threshold'] + ) and ( + t['reliability'] >= app_domain_data['ad_params']['reliability_threshold'] + ) + + edge_json['app_domain'] = { + 'passes_app_domain': passes_app_domain, + 'local_compatibility': t['local_compatibility'], + 'local_compatibility_threshold': app_domain_data['ad_params']['local_compatibility_threshold'], + 'reliability': t['reliability'], + 'reliability_threshold': app_domain_data['ad_params']['reliability_threshold'], + 'times_triggered': t['times_triggered'], + } + + break + + return edge_json + + @staticmethod def create(pathway, start_nodes: List[Node], end_nodes: List[Node], rule: Optional[Rule] = None, name: Optional[str] = None, description: Optional[str] = None): @@ -1136,6 +1195,14 @@ class Edge(EnviPathModel, AliasMixin, ScenarioMixin): def as_svg(self): return self.edge_label.as_svg if self.edge_label else None + def simple_json(self, include_description=False): + res = super().simple_json() + name = res.get('name', None) + if name == 'no name': + res['name'] = self.edge_label.name + + return res + class EPModel(PolymorphicModel, EnviPathModel): package = models.ForeignKey('epdb.Package', verbose_name='Package', on_delete=models.CASCADE, db_index=True) @@ -1463,6 +1530,7 @@ class MLRelativeReasoning(EPModel): return res + class ApplicabilityDomain(EnviPathModel): model = models.ForeignKey(MLRelativeReasoning, on_delete=models.CASCADE) @@ -1614,7 +1682,7 @@ class ApplicabilityDomain(EnviPathModel): 'model': self.model.simple_json(), 'num_neighbours': self.num_neighbours, 'reliability_threshold': self.reliability_threshold, - 'local_compatibilty_threshold': self.local_compatibilty_threshold, + 'local_compatibility_threshold': self.local_compatibilty_threshold, }, 'assessment': { 'smiles': smiles, diff --git a/epdb/tasks.py b/epdb/tasks.py index 3664caf5..4ca4d183 100644 --- a/epdb/tasks.py +++ b/epdb/tasks.py @@ -58,7 +58,7 @@ def predict(pw_pk: int, pred_setting_pk: int, limit: Optional[int] = None, node_ spw.predict_step(from_depth=level) level += 1 - # break in case we are in incremental model + # break in case we are in incremental mode if limit != -1: if level >= limit: break diff --git a/epdb/views.py b/epdb/views.py index 37a3ae72..290555ee 100644 --- a/epdb/views.py +++ b/epdb/views.py @@ -1454,6 +1454,7 @@ def package_pathway_node(request, package_uuid, pathway_uuid, node_uuid): ] context['node'] = current_node + context['app_domain_assessment_data'] = json.dumps(current_node.get_app_domain_assessment_data()) return render(request, 'objects/node.html', context) diff --git a/static/js/pps.js b/static/js/pps.js index 43ff24dc..d6f064d4 100644 --- a/static/js/pps.js +++ b/static/js/pps.js @@ -638,3 +638,147 @@ function fillPRCurve(modelUri, onclick){ }); } + + +function handleAssessmentResponse(depict_url, data) { + var inside_app_domain = "This compound is " + (data["assessment"]["inside_app_domain"] ? "inside" : "outside") + " the Applicability Domain derived from the chemical (PCA) space constructed using the training data." + ""; + var functionalGroupsImgSrc = null; + var reactivityCentersImgSrc = null; + + if (data['assessment']['node'] !== undefined) { + functionalGroupsImgSrc = ""; + reactivityCentersImgSrc = "" + } else { + functionalGroupsImgSrc = ""; + reactivityCentersImgSrc = "" + } + + tpl = `
+

+ Functional Groups Covered by Model +

+
+
+
+ ${inside_app_domain} +

+
+ ${functionalGroupsImgSrc} +
+
+
+ +
+

+ Reactivity Centers +

+
+
+
+
+ ${reactivityCentersImgSrc} +
+
+
` + + var transformations = ''; + + for (t in data['assessment']['transformations']) { + transObj = data['assessment']['transformations'][t]; + var neighbors = ''; + for (n in transObj['neighbors']) { + neighObj = transObj['neighbors'][n]; + var neighImg = ""; + var objLink = `${neighObj['name']}` + var neighPredProb = "Predicted probability: " + neighObj['probability'].toFixed(2) + ""; + + var pwLinks = ''; + for (pw in neighObj['related_pathways']) { + var pwObj = neighObj['related_pathways'][pw]; + pwLinks += "" + pwObj['name'] + ""; + } + + var expPathways = ` +
+

+ Experimental Pathways +

+
+
+
+ ${pwLinks} +
+
+ ` + + if (pwLinks === '') { + expPathways = '' + } + + neighbors += ` +
+

+ Analog Transformation on ${neighObj['name']} +

+
+
+
+ ${objLink} + ${neighPredProb} + ${expPathways} +

+
+ ${neighImg} +
+
+
+ ` + } + + var panelName = null; + var objLink = null; + if (transObj['is_predicted']) { + panelName = `Predicted Transformation by ${transObj['rule']['name']}`; + objLink = `${transObj['edge']['name']}` + } else { + panelName = `Potential Transformation by applying ${transObj['rule']['name']}`; + objLink = `${transObj['rule']['name']}` + } + + var predProb = "Predicted probability: " + transObj['probability'].toFixed(2) + ""; + var timesTriggered = "This rule has triggered " + transObj['times_triggered'] + " times in the training set"; + var reliability = "Reliability: " + transObj['reliability'].toFixed(2) + " (" + (transObj['reliability'] > data['ad_params']['reliability_threshold'] ? ">" : "<") + " Reliability Threshold of " + data['ad_params']['reliability_threshold'] + ") "; + var localCompatibility = "Local Compatibility: " + transObj['local_compatibility'].toFixed(2) + " (" + (transObj['local_compatibility'] > data['ad_params']['local_compatibilty_threshold'] ? ">" : "<") + " Local Compatibility Threshold of " + data['ad_params']['local_compatibilty_threshold'] + ")"; + + var transImg = ""; + + var transformation = ` +
+

+ ${panelName} +

+
+
+
+ ${objLink} + ${predProb} + ${timesTriggered} + ${reliability} + ${localCompatibility} +

+
+ ${transImg} +
+

+ ${neighbors} +
+
+ ` + transformations += transformation; + } + + res = tpl + transformations; + + $("#appDomainAssessmentResultTable").append(res); + +} \ No newline at end of file diff --git a/static/js/pw.js b/static/js/pw.js index 266e9917..5e362d12 100644 --- a/static/js/pw.js +++ b/static/js/pw.js @@ -1,4 +1,4 @@ -console.log("loaded") +console.log("loaded pw.js") function predictFromNode(url) { @@ -144,6 +144,16 @@ function draw(pathway, elem) { function node_popup(n) { popupContent = "" + n.name + "
"; popupContent += "Depth " + n.depth + "
" + + if (appDomainViewEnabled) { + if(n.app_domain != null) { + popupContent += "This compound is " + (n.app_domain['inside_app_domain'] ? "inside" : "outside") + " the Applicability Domain derived from the chemical (PCA) space constructed using the training data." + "
" + if (n.app_domain['uncovered_functional_groups']) { + popupContent += "Compound contains functional groups not covered by the training set
" + } + } + } + popupContent += "
" if (n.scenarios.length > 0) { popupContent += 'Half-lives and related scenarios:
' @@ -162,6 +172,18 @@ function draw(pathway, elem) { function edge_popup(e) { popupContent = "" + e.name + "
"; + + if(e.app_domain){ + adcontent = "

"; + if(e.app_domain["times_triggered"]) { + adcontent += "This rule triggered " + e.app_domain["times_triggered"] + " times in the training set
"; + } + adcontent += "Reliability " + e.app_domain["reliability"].toFixed(2) + " (" + (e.app_domain["reliability"] > e.app_domain["reliability_threshold"] ? ">" : "<") + " Reliability Threshold of " + e.app_domain["reliability_threshold"] + ")
"; + adcontent += "Local Compatibility " + e.app_domain["local_compatibility"].toFixed(2) + " (" + (e.app_domain["local_compatibility"] > e.app_domain["local_compatibility_threshold"] ? ">" : "<") + " Local Compatibility Threshold of " + e.app_domain["local_compatibility_threshold"] + ")
"; + adcontent += "

"; + } + popupContent += adcontent; + popupContent += "
" if (e.reaction_probability) { popupContent += 'Probability:
' + e.reaction_probability.toFixed(3) + '
'; @@ -233,13 +255,12 @@ function draw(pathway, elem) { .enter().append("line") // Check if target is pseudo and draw marker only if not pseudo .attr("class", d => d.target.pseudo ? "link_no_arrow" : "link") - // .on("mouseover", (event, d) => { - // tooltip.style("visibility", "visible") - // .text(`Link: ${d.source.id} → ${d.target.id}`) - // .style("top", `${event.pageY + 5}px`) - // .style("left", `${event.pageX + 5}px`); - // }) - // .on("mouseout", () => tooltip.style("visibility", "hidden")); + .attr("marker-end", d => d.target.pseudo ? '' : 'url(#arrow)') + + // add element to links array + link.each(function(d) { + d.el = this; // attach the DOM element to the data object + }); pop_add(link, "Reaction", edge_popup); @@ -255,16 +276,6 @@ function draw(pathway, elem) { .on("click", function (event, d) { d3.select(this).select("circle").classed("highlighted", !d3.select(this).select("circle").classed("highlighted")); }) - // .on("mouseover", (event, d) => { - // if (d.pseudo) { - // return - // } - // tooltip.style("visibility", "visible") - // .text(`Node: ${d.id} Depth: ${d.depth}`) - // .style("top", `${event.pageY + 5}px`) - // .style("left", `${event.pageX + 5}px`); - // }) - // .on("mouseout", () => tooltip.style("visibility", "hidden")); // Kreise für die Knoten hinzufügen node.append("circle") @@ -280,5 +291,10 @@ function draw(pathway, elem) { .attr("width", nodeRadius * 2) .attr("height", nodeRadius * 2); - pop_add(node, "Compound", node_popup); + // add element to nodes array + node.each(function(d) { + d.el = this; // attach the DOM element to the data object + }); + + pop_add(node, "Compound", node_popup); } diff --git a/templates/objects/model.html b/templates/objects/model.html index c58359b8..f8d2d484 100644 --- a/templates/objects/model.html +++ b/templates/objects/model.html @@ -269,141 +269,6 @@ + {% endif %} + + + {% endblock content %} diff --git a/templates/objects/pathway.html b/templates/objects/pathway.html index c60d7573..23160ea1 100644 --- a/templates/objects/pathway.html +++ b/templates/objects/pathway.html @@ -4,16 +4,22 @@