forked from enviPath/enviPy
852 lines
27 KiB
Python
852 lines
27 KiB
Python
import base64
|
|
import hashlib
|
|
import hmac
|
|
import json
|
|
import logging
|
|
import uuid
|
|
from collections import defaultdict
|
|
from datetime import datetime
|
|
from typing import Any, Dict, List, Optional, TYPE_CHECKING
|
|
|
|
from django.conf import settings as s
|
|
from django.db import transaction
|
|
from ninja import Schema
|
|
from pydantic import HttpUrl, ValidationError
|
|
|
|
from bayer.models import PESCompound, PESStructure
|
|
from epdb.exceptions import PackageImportException
|
|
from epdb.models import (
|
|
AdditionalInformation,
|
|
Compound,
|
|
CompoundStructure,
|
|
Edge,
|
|
EnviFormer,
|
|
EPModel,
|
|
ExternalDatabase,
|
|
ExternalIdentifier,
|
|
License,
|
|
MLRelativeReasoning,
|
|
Node,
|
|
ParallelRule,
|
|
Pathway,
|
|
Reaction,
|
|
Rule,
|
|
RuleBasedRelativeReasoning,
|
|
Scenario,
|
|
Setting,
|
|
SimpleAmbitRule,
|
|
SimpleRDKitRule,
|
|
SimpleRule, Group,
|
|
)
|
|
from utilities.chem import FormatConverter
|
|
|
|
logger = logging.getLogger(__name__)
|
|
Package = s.GET_PACKAGE_MODEL()
|
|
|
|
if TYPE_CHECKING:
|
|
from epdb.logic import SPathway, GroupManager
|
|
|
|
|
|
class LicenseExportSchema(Schema):
|
|
cc_string: str
|
|
link: HttpUrl
|
|
image_link: HttpUrl
|
|
|
|
|
|
##############
|
|
# RefSchemas #
|
|
##############
|
|
class RefExportSchema(Schema):
|
|
uuid: str
|
|
url: str
|
|
|
|
@staticmethod
|
|
def resolve_uuid(obj):
|
|
value = obj.get("uuid") if isinstance(obj, dict) else obj.uuid
|
|
return str(value)
|
|
|
|
|
|
class RefGroupExportSchema(RefExportSchema): ...
|
|
|
|
|
|
class RefCompoundExportSchema(RefExportSchema): ...
|
|
|
|
|
|
class RefCompoundStructureExportSchema(RefExportSchema): ...
|
|
|
|
|
|
class RefReactionExportSchema(RefExportSchema): ...
|
|
|
|
|
|
class RefRuleExportSchema(RefExportSchema): ...
|
|
|
|
|
|
class RefNodeExportSchema(RefExportSchema): ...
|
|
|
|
|
|
class RefEdgeExportSchema(RefExportSchema): ...
|
|
|
|
|
|
class RefPathwayExportSchema(RefExportSchema): ...
|
|
|
|
|
|
class RefScenarioExportSchema(RefExportSchema): ...
|
|
|
|
|
|
class RefEnzymeExportSchema(RefExportSchema): ...
|
|
|
|
|
|
############
|
|
# Compound #
|
|
############
|
|
class CompoundExportSchema(RefCompoundExportSchema):
|
|
name: str
|
|
description: str
|
|
aliases: List[str]
|
|
default_structure: RefCompoundStructureExportSchema
|
|
structures: List["CompoundStructureExportSchema"]
|
|
scenarios: List[RefScenarioExportSchema]
|
|
|
|
|
|
class CompoundStructureExportSchema(RefCompoundStructureExportSchema):
|
|
name: str
|
|
description: str
|
|
aliases: List[str]
|
|
smiles: str
|
|
molfile: Optional[str]
|
|
normalized_structure: bool
|
|
scenarios: List[RefScenarioExportSchema]
|
|
|
|
|
|
class PESCompoundExportSchema(RefCompoundExportSchema):
|
|
name: str
|
|
description: str
|
|
aliases: List[str]
|
|
default_structure: RefCompoundStructureExportSchema
|
|
structures: List["PESCompoundStructureExportSchema"]
|
|
scenarios: List[RefScenarioExportSchema]
|
|
|
|
|
|
class PESCompoundStructureExportSchema(RefCompoundStructureExportSchema):
|
|
name: str
|
|
description: str
|
|
aliases: List[str]
|
|
smiles: str
|
|
molfile: Optional[str]
|
|
normalized_structure: bool
|
|
scenarios: List[RefScenarioExportSchema]
|
|
pes_link: HttpUrl
|
|
|
|
|
|
############
|
|
# Reaction #
|
|
############
|
|
class ReactionExportSchema(RefReactionExportSchema):
|
|
name: str
|
|
description: str
|
|
aliases: List[str]
|
|
educts: List[RefCompoundStructureExportSchema]
|
|
products: List[RefCompoundStructureExportSchema]
|
|
rules: List[RefRuleExportSchema]
|
|
multi_step: bool
|
|
medline_references: List[str] | None
|
|
scenarios: List[RefScenarioExportSchema]
|
|
|
|
|
|
#########
|
|
# Rules #
|
|
#########
|
|
class EnzymeExportSchema(RefEnzymeExportSchema):
|
|
ec_number: str
|
|
classification_level: int
|
|
linking_method: str
|
|
reaction_evidence: List[RefReactionExportSchema]
|
|
edge_evidence: List[RefEdgeExportSchema]
|
|
|
|
|
|
class EnzymeRuleExportSchema(RefRuleExportSchema):
|
|
enzymes: List[EnzymeExportSchema] | None = None
|
|
|
|
@staticmethod
|
|
def resolve_enzymes(obj):
|
|
if isinstance(obj, dict):
|
|
res = []
|
|
for e in obj.get("enzymes", []):
|
|
res.append(EnzymeExportSchema.model_validate(e))
|
|
return res
|
|
return obj.enzymelink_set.all()
|
|
|
|
|
|
class RuleExportSchema(EnzymeRuleExportSchema):
|
|
name: str
|
|
description: str
|
|
aliases: List[str]
|
|
smirks: str
|
|
reactant_filter_smarts: Optional[str]
|
|
product_filter_smarts: Optional[str]
|
|
scenarios: List[RefScenarioExportSchema]
|
|
|
|
|
|
class ParallelRuleExportSchema(EnzymeRuleExportSchema):
|
|
name: str
|
|
description: str
|
|
aliases: List[str]
|
|
simple_rules: List[RefRuleExportSchema]
|
|
scenarios: List[RefScenarioExportSchema]
|
|
|
|
|
|
###########################
|
|
# Pathway / Nodes / Edges #
|
|
###########################
|
|
class NodeExportSchema(RefNodeExportSchema):
|
|
name: str
|
|
description: str
|
|
aliases: List[str]
|
|
default_node_label: RefCompoundStructureExportSchema
|
|
node_labels: List[RefCompoundStructureExportSchema]
|
|
depth: int
|
|
stereo_removed: bool
|
|
scenarios: List[RefScenarioExportSchema]
|
|
|
|
|
|
class EdgeExportSchema(RefEdgeExportSchema):
|
|
name: str
|
|
description: str
|
|
aliases: List[str]
|
|
edge_label: RefReactionExportSchema
|
|
start_nodes: List[RefNodeExportSchema]
|
|
end_nodes: List[RefNodeExportSchema]
|
|
scenarios: List[RefScenarioExportSchema]
|
|
|
|
|
|
class PathwayExportSchema(RefPathwayExportSchema):
|
|
name: str
|
|
description: str
|
|
aliases: List[str]
|
|
predicted: bool
|
|
nodes: List[NodeExportSchema]
|
|
edges: List[EdgeExportSchema]
|
|
scenarios: List[RefScenarioExportSchema]
|
|
|
|
|
|
class ScenarioExportSchema(RefScenarioExportSchema):
|
|
name: str
|
|
description: str
|
|
scenario_date: str
|
|
scenario_type: str
|
|
|
|
|
|
class AdditionalInformationExportSchema(RefExportSchema):
|
|
type: str
|
|
data: dict
|
|
scenario: RefScenarioExportSchema | None = None
|
|
attach_object: RefExportSchema | None = None
|
|
|
|
@staticmethod
|
|
def resolve_attach_object(obj):
|
|
if isinstance(obj, dict):
|
|
if obj.get("attach_object") is None:
|
|
return None
|
|
return RefExportSchema.model_validate(obj["attach_object"])
|
|
return obj.content_object
|
|
|
|
|
|
###########
|
|
# Package #
|
|
###########
|
|
class PackageExportSchema(Schema):
|
|
name: str
|
|
description: str
|
|
uuid: str
|
|
reviewed: bool
|
|
license: LicenseExportSchema | None = None
|
|
classification_level: str = "Internal"
|
|
data_pool: RefGroupExportSchema | None = None
|
|
compounds: List[CompoundExportSchema | PESCompoundExportSchema]
|
|
reactions: List[ReactionExportSchema]
|
|
simple_rules: List[RuleExportSchema]
|
|
composite_rules: List[ParallelRuleExportSchema]
|
|
pathways: List[PathwayExportSchema]
|
|
scenarios: List[ScenarioExportSchema]
|
|
additional_information: List[AdditionalInformationExportSchema]
|
|
|
|
@staticmethod
|
|
def resolve_uuid(obj):
|
|
value = obj.get("uuid") if isinstance(obj, dict) else obj.uuid
|
|
return str(value)
|
|
|
|
@staticmethod
|
|
def resolve_classification_level(obj):
|
|
if isinstance(obj, dict):
|
|
return obj["classification_level"]
|
|
return obj.Classification(obj.classification_level).name
|
|
|
|
|
|
@staticmethod
|
|
def resolve_compounds(obj):
|
|
res = []
|
|
if isinstance(obj, dict):
|
|
for c in obj.get("compounds", []):
|
|
is_pes = any([cs.get("pes_link", None) is not None for cs in c.get("structures", [])])
|
|
if is_pes:
|
|
res.append(PESCompoundExportSchema.model_validate(c))
|
|
else:
|
|
res.append(CompoundExportSchema.model_validate(c))
|
|
else:
|
|
for c in obj.compounds.all():
|
|
if isinstance(c, PESCompound):
|
|
res.append(PESCompoundExportSchema.from_orm(c))
|
|
else:
|
|
res.append(CompoundExportSchema.from_orm(c))
|
|
return res
|
|
|
|
@staticmethod
|
|
def resolve_simple_rules(obj):
|
|
if isinstance(obj, dict):
|
|
result = []
|
|
for r in obj.get("simple_rules", []):
|
|
result.append(RuleExportSchema.model_validate(r))
|
|
return result
|
|
return SimpleAmbitRule.objects.filter(package=obj)
|
|
|
|
@staticmethod
|
|
def resolve_composite_rules(obj):
|
|
if isinstance(obj, dict):
|
|
result = []
|
|
for r in obj.get("composite_rules", []):
|
|
result.append(ParallelRuleExportSchema.model_validate(r))
|
|
return result
|
|
return ParallelRule.objects.filter(package=obj)
|
|
|
|
@staticmethod
|
|
def resolve_additional_information(obj):
|
|
if isinstance(obj, dict):
|
|
result = []
|
|
for ai in obj.get("additional_information", []):
|
|
result.append(AdditionalInformationExportSchema.model_validate(ai))
|
|
return result
|
|
return AdditionalInformation.objects.filter(package=obj)
|
|
|
|
|
|
class PackageExporter:
|
|
def __init__(self, package: Package):
|
|
self._raw_package = package
|
|
|
|
def do_export(self):
|
|
return PackageExporter._export_package_as_json(self._raw_package)
|
|
|
|
@staticmethod
|
|
def _export_package_as_json(package: Package) -> Dict[str, Any]:
|
|
"""
|
|
Dumps a Package and all its related objects as JSON.
|
|
|
|
Args:
|
|
package: The Package instance to dump
|
|
|
|
Returns:
|
|
Dict containing the complete package data as JSON-serializable structure
|
|
"""
|
|
|
|
data = PackageExportSchema.from_orm(package)
|
|
|
|
return data.model_dump(mode="json")
|
|
|
|
|
|
class PackageImporter:
|
|
|
|
def __init__(self, package: Dict[str, Any], preserve_uuids: bool = False):
|
|
self.preserve_uuids = preserve_uuids
|
|
self._raw_package = package
|
|
self._cache = {}
|
|
|
|
def do_import(self) -> Package:
|
|
return self._import_package_from_json()
|
|
|
|
def _import_compounds(
|
|
self,
|
|
package: Package,
|
|
compounds: List[CompoundExportSchema | PESCompoundExportSchema],
|
|
):
|
|
for c in compounds:
|
|
|
|
if isinstance(c, PESCompoundExportSchema):
|
|
c_cls = PESCompound
|
|
else:
|
|
c_cls = Compound
|
|
|
|
new_c = c_cls()
|
|
new_c.uuid = str(uuid.uuid4()) if not self.preserve_uuids else c.uuid
|
|
new_c.package = package
|
|
new_c.name = c.name
|
|
new_c.description = c.description
|
|
new_c.aliases = c.aliases
|
|
new_c.save()
|
|
|
|
self._cache[c.uuid] = new_c
|
|
|
|
for cs in c.structures:
|
|
|
|
if isinstance(cs, PESCompoundStructureExportSchema):
|
|
cs_cls = PESStructure
|
|
else:
|
|
cs_cls = CompoundStructure
|
|
|
|
new_cs = cs_cls()
|
|
new_cs.uuid = str(uuid.uuid4()) if not self.preserve_uuids else cs.uuid
|
|
new_cs.compound = new_c
|
|
new_cs.name = cs.name
|
|
new_cs.description = cs.description
|
|
new_cs.aliases = cs.aliases
|
|
new_cs.smiles = cs.smiles
|
|
new_cs.molfile = cs.molfile
|
|
|
|
if cs_cls == PESStructure:
|
|
new_cs.pes_link = cs.pes_link
|
|
|
|
new_cs.normalized_structure = cs.normalized_structure
|
|
new_cs.save()
|
|
|
|
self._cache[cs.uuid] = new_cs
|
|
|
|
# Now we can assigne the default_structure to the compound
|
|
new_c.default_structure = self._cache[c.default_structure.uuid]
|
|
new_c.save()
|
|
|
|
def _import_simple_rules(
|
|
self,
|
|
package: Package,
|
|
rules: List[RuleExportSchema]
|
|
):
|
|
for r in rules:
|
|
new_r = SimpleAmbitRule()
|
|
new_r.uuid = str(uuid.uuid4()) if not self.preserve_uuids else r.uuid
|
|
new_r.package = package
|
|
new_r.name = r.name
|
|
new_r.description = r.description
|
|
new_r.aliases = r.aliases
|
|
new_r.smirks = r.smirks
|
|
new_r.reactant_filter_smarts = r.reactant_filter_smarts
|
|
new_r.product_filter_smarts = r.product_filter_smarts
|
|
new_r.save()
|
|
|
|
self._cache[r.uuid] = new_r
|
|
|
|
def _import_composite_rules(
|
|
self,
|
|
package: Package,
|
|
rules: List[ParallelRuleExportSchema]
|
|
):
|
|
for r in rules:
|
|
new_r = ParallelRule()
|
|
new_r.uuid = str(uuid.uuid4()) if not self.preserve_uuids else r.uuid
|
|
new_r.package = package
|
|
new_r.name = r.name
|
|
new_r.description = r.description
|
|
new_r.aliases = r.aliases
|
|
new_r.save()
|
|
|
|
for sr in r.simple_rules:
|
|
new_r.simple_rules.add(self._cache[sr.uuid])
|
|
|
|
self._cache[r.uuid] = new_r
|
|
|
|
def _import_reactions(
|
|
self,
|
|
package: Package,
|
|
reactions: List[ReactionExportSchema],
|
|
):
|
|
for r in reactions:
|
|
new_r = Reaction()
|
|
new_r.uuid = str(uuid.uuid4()) if not self.preserve_uuids else r.uuid
|
|
new_r.package = package
|
|
new_r.name = r.name
|
|
new_r.description = r.description
|
|
new_r.aliases = r.aliases
|
|
new_r.multi_step = r.multi_step
|
|
new_r.medline_references = r.medline_references
|
|
new_r.save()
|
|
|
|
for educt in r.educts:
|
|
new_r.educts.add(self._cache[educt.uuid])
|
|
|
|
for product in r.products:
|
|
new_r.products.add(self._cache[product.uuid])
|
|
|
|
for rule in r.rules:
|
|
new_r.rules.add(self._cache[rule.uuid])
|
|
|
|
self._cache[r.uuid] = new_r
|
|
|
|
def _import_pathways(
|
|
self,
|
|
package: Package,
|
|
pathways: List[PathwayExportSchema]
|
|
):
|
|
for pw in pathways:
|
|
new_pw = Pathway()
|
|
new_pw.uuid = str(uuid.uuid4()) if not self.preserve_uuids else pw.uuid
|
|
new_pw.package = package
|
|
new_pw.name = pw.name
|
|
new_pw.description = pw.description
|
|
new_pw.aliases = pw.aliases
|
|
new_pw.predicted = pw.predicted
|
|
new_pw.save()
|
|
|
|
self._cache[pw.uuid] = new_pw
|
|
|
|
for n in pw.nodes:
|
|
new_n = Node()
|
|
new_n.uuid = str(uuid.uuid4()) if not self.preserve_uuids else n.uuid
|
|
new_n.pathway = new_pw
|
|
new_n.name = n.name
|
|
new_n.description = n.description
|
|
new_n.aliases = n.aliases
|
|
new_n.default_node_label = self._cache[n.default_node_label.uuid]
|
|
new_n.depth = n.depth
|
|
new_n.stereo_removed = n.stereo_removed
|
|
new_n.save()
|
|
|
|
for nl in n.node_labels:
|
|
new_n.node_labels.add(self._cache[nl.uuid])
|
|
|
|
self._cache[n.uuid] = new_n
|
|
|
|
for e in pw.edges:
|
|
new_e = Edge()
|
|
new_e.uuid = str(uuid.uuid4()) if not self.preserve_uuids else e.uuid
|
|
new_e.pathway = new_pw
|
|
new_e.name = e.name
|
|
new_e.description = e.description
|
|
new_e.aliases = e.aliases
|
|
new_e.edge_label = self._cache[e.edge_label.uuid]
|
|
new_e.save()
|
|
|
|
for sn in e.start_nodes:
|
|
new_e.start_nodes.add(self._cache[sn.uuid])
|
|
|
|
for en in e.end_nodes:
|
|
new_e.end_nodes.add(self._cache[en.uuid])
|
|
|
|
self._cache[e.uuid] = new_e
|
|
|
|
def _import_scenarios(
|
|
self,
|
|
package: Package,
|
|
scenarios: List[ScenarioExportSchema]
|
|
):
|
|
for s in scenarios:
|
|
new_s = Scenario()
|
|
new_s.uuid = str(uuid.uuid4()) if not self.preserve_uuids else s.uuid
|
|
new_s.package = package
|
|
new_s.name = s.name
|
|
new_s.description = s.description
|
|
new_s.scenario_date = s.scenario_date
|
|
new_s.scenario_type = s.scenario_type
|
|
new_s.save()
|
|
|
|
self._cache[s.uuid] = new_s
|
|
|
|
def _import_additional_information(
|
|
self,
|
|
package: Package,
|
|
additional_information: List[AdditionalInformationExportSchema]
|
|
):
|
|
for ai in additional_information:
|
|
new_ai = AdditionalInformation()
|
|
new_ai.uuid = str(uuid.uuid4()) if not self.preserve_uuids else ai.uuid
|
|
new_ai.package = package
|
|
new_ai.type = ai.type
|
|
new_ai.data = ai.data
|
|
|
|
if ai.scenario:
|
|
new_ai.scenario = self._cache[ai.scenario.uuid]
|
|
|
|
if ai.attach_object:
|
|
new_ai.content_object = self._cache[ai.attach_object.uuid]
|
|
|
|
new_ai.save()
|
|
|
|
self._cache[ai.uuid] = new_ai
|
|
|
|
def _link_scenarios_after_import(self, data: PackageExportSchema):
|
|
collections = [
|
|
data.compounds,
|
|
data.simple_rules,
|
|
data.composite_rules,
|
|
data.reactions,
|
|
data.pathways,
|
|
[n for pw in data.pathways for n in pw.nodes],
|
|
[e for pw in data.pathways for e in pw.edges],
|
|
]
|
|
|
|
for coll in collections:
|
|
for elem in coll:
|
|
elem_obj = self._cache[elem.uuid]
|
|
for s in elem.scenarios:
|
|
elem_obj.scenarios.add(self._cache[s.uuid])
|
|
|
|
|
|
def _import_package_from_json(
|
|
self,
|
|
) -> Package:
|
|
try:
|
|
parsed = PackageExportSchema.model_validate(self._raw_package)
|
|
except ValidationError as e:
|
|
logger.error(f"Error validating package data: {e}")
|
|
raise PackageImportException("Deserialization failed")
|
|
|
|
package_uuid = str(uuid.uuid4())
|
|
if self.preserve_uuids:
|
|
package_uuid = parsed.uuid
|
|
|
|
package_license = License.objects.get(link=parsed.license.link) if parsed.license else None
|
|
package_classification_level = Package.Classification[parsed.classification_level.upper()]
|
|
|
|
print(parsed.classification_level, "->", package_classification_level)
|
|
|
|
package = Package()
|
|
package.uuid = package_uuid
|
|
package.name = f"{parsed.name} - Imported at {datetime.now()}"
|
|
package.description = parsed.description
|
|
package.reviewed = False
|
|
package.license = package_license
|
|
package.classification_level = package_classification_level
|
|
|
|
if package_classification_level == Package.Classification.SECRET:
|
|
package.data_pool = Group.objects.get(uuid=parsed.data_pool.uuid)
|
|
|
|
package.save()
|
|
|
|
# Import Elements
|
|
self._import_compounds(package, parsed.compounds)
|
|
self._import_simple_rules(package, parsed.simple_rules)
|
|
self._import_composite_rules(package, parsed.composite_rules)
|
|
self._import_reactions(package, parsed.reactions)
|
|
self._import_pathways(package, parsed.pathways)
|
|
self._import_scenarios(package, parsed.scenarios)
|
|
self._import_additional_information(package, parsed.additional_information)
|
|
self._link_scenarios_after_import(parsed)
|
|
|
|
return package
|
|
|
|
@staticmethod
|
|
def sign(data: Dict[str, Any], key: str) -> Dict[str, Any]:
|
|
json_str = json.dumps(data, sort_keys=True, separators=(",", ":"))
|
|
signature = hmac.new(key.encode(), json_str.encode(), hashlib.sha256).digest()
|
|
data["_signature"] = base64.b64encode(signature).decode()
|
|
return data
|
|
|
|
@staticmethod
|
|
def verify(data: Dict[str, Any], key: str) -> bool:
|
|
copied_data = data.copy()
|
|
sig = copied_data.pop("_signature")
|
|
signature = base64.b64decode(sig, validate=True)
|
|
json_str = json.dumps(copied_data, sort_keys=True, separators=(",", ":"))
|
|
expected = hmac.new(key.encode(), json_str.encode(), hashlib.sha256).digest()
|
|
return hmac.compare_digest(signature, expected)
|
|
|
|
|
|
class PathwayUtils:
|
|
def __init__(self, pathway: "Pathway"):
|
|
self.pathway = pathway
|
|
|
|
@staticmethod
|
|
def _get_products(smiles: str, rules: List["Rule"]):
|
|
educt_rule_products: Dict[str, Dict[str, List[str]]] = defaultdict(
|
|
lambda: defaultdict(list)
|
|
)
|
|
|
|
for r in rules:
|
|
product_sets = r.apply(smiles)
|
|
for product_set in product_sets:
|
|
for product in product_set:
|
|
educt_rule_products[smiles][r.url].append(product)
|
|
|
|
return educt_rule_products
|
|
|
|
def find_missing_rules(self, rules: List["Rule"]):
|
|
print(f"Processing {self.pathway.name}")
|
|
# compute products for each node / rule combination in the pathway
|
|
educt_rule_products = defaultdict(lambda: defaultdict(list))
|
|
|
|
for node in self.pathway.nodes:
|
|
educt_rule_products.update(**self._get_products(node.default_node_label.smiles, rules))
|
|
|
|
# loop through edges and determine reactions that can't be constructed by
|
|
# any of the rules or a combination of two rules in a chained fashion
|
|
|
|
res: Dict[str, List["Rule"]] = dict()
|
|
|
|
for edge in self.pathway.edges:
|
|
found = False
|
|
reaction = edge.edge_label
|
|
|
|
educts = [cs for cs in reaction.educts.all()]
|
|
products = [cs.smiles for cs in reaction.products.all()]
|
|
rule_chain = []
|
|
|
|
for educt in educts:
|
|
educt = educt.smiles
|
|
triggered_rules = list(educt_rule_products.get(educt, {}).keys())
|
|
for triggered_rule in triggered_rules:
|
|
if rule_products := educt_rule_products[educt][triggered_rule]:
|
|
# check if this rule covers the reaction
|
|
if FormatConverter.smiles_covered_by(
|
|
products, rule_products, standardize=True, canonicalize_tautomers=True
|
|
):
|
|
found = True
|
|
else:
|
|
# Check if another prediction step would cover the reaction
|
|
for product in rule_products:
|
|
prod_rule_products = self._get_products(product, rules)
|
|
prod_triggered_rules = list(
|
|
prod_rule_products.get(product, {}).keys()
|
|
)
|
|
for prod_triggered_rule in prod_triggered_rules:
|
|
if second_step_products := prod_rule_products[product][
|
|
prod_triggered_rule
|
|
]:
|
|
if FormatConverter.smiles_covered_by(
|
|
products,
|
|
second_step_products,
|
|
standardize=True,
|
|
canonicalize_tautomers=True,
|
|
):
|
|
rule_chain.append(
|
|
(
|
|
triggered_rule,
|
|
Rule.objects.get(url=triggered_rule).name,
|
|
)
|
|
)
|
|
rule_chain.append(
|
|
(
|
|
prod_triggered_rule,
|
|
Rule.objects.get(url=prod_triggered_rule).name,
|
|
)
|
|
)
|
|
res[edge.url] = rule_chain
|
|
|
|
if not found:
|
|
res[edge.url] = rule_chain
|
|
|
|
return res
|
|
|
|
def engineer(self, setting: "Setting"):
|
|
from epdb.logic import SPathway
|
|
from utilities.chem import FormatConverter
|
|
from utilities.ml import graph_from_pathway, get_shortest_path
|
|
|
|
# get a fresh copy
|
|
pw = Pathway.objects.get(id=self.pathway.pk)
|
|
|
|
root_nodes = [n.default_node_label.smiles for n in pw.root_nodes]
|
|
|
|
if len(root_nodes) != 1:
|
|
logger.warning(f"Pathway {pw.name} has {len(root_nodes)} root nodes")
|
|
# spw, mapping, intermediates
|
|
return None, {}, []
|
|
|
|
# Predict the Pathway in memory
|
|
spw = SPathway(root_nodes[0], None, setting)
|
|
|
|
level = 0
|
|
while not spw.done:
|
|
spw.predict_step(from_depth=level)
|
|
level += 1
|
|
|
|
# Generate SNode -> Node mapping
|
|
node_mapping = {}
|
|
|
|
for node in pw.nodes:
|
|
for snode in spw.smiles_to_node.values():
|
|
data_smiles = node.default_node_label.smiles
|
|
pred_smiles = snode.smiles
|
|
|
|
# "~" denotes any bond remove and use implicit single bond for comparison
|
|
data_key = FormatConverter.InChIKey(data_smiles.replace("~", ""))
|
|
pred_key = FormatConverter.InChIKey(pred_smiles.replace("~", ""))
|
|
|
|
if data_key == pred_key:
|
|
node_mapping[snode] = node
|
|
|
|
reverse_mapping = {v: k for k, v in node_mapping.items()}
|
|
|
|
graph = graph_from_pathway(spw)
|
|
|
|
intermediate_mapping = []
|
|
|
|
# loop through each edge and each reactant <-> product pair
|
|
# and compute the shortest path on the predicted pathway
|
|
for e in pw.edges:
|
|
for start in e.start_nodes.all():
|
|
if start not in reverse_mapping:
|
|
continue
|
|
|
|
start_snode = reverse_mapping[start]
|
|
|
|
for end in e.end_nodes.all():
|
|
if end not in reverse_mapping:
|
|
continue
|
|
|
|
end_snode = reverse_mapping[end]
|
|
|
|
# If res is non-empty, we've found intermediates
|
|
intermediate_smiles = get_shortest_path(
|
|
graph,
|
|
FormatConverter.standardize(start_snode.smiles, remove_stereo=True),
|
|
FormatConverter.standardize(end_snode.smiles, remove_stereo=True),
|
|
)
|
|
|
|
if intermediate_smiles:
|
|
intermediates = []
|
|
|
|
prev = start_snode.smiles
|
|
|
|
for smi in intermediate_smiles + [end_snode.smiles]:
|
|
for e in spw.get_edge_for_educt_smiles(prev):
|
|
if smi in e.product_smiles():
|
|
intermediates.append(e)
|
|
|
|
prev = smi
|
|
|
|
intermediate_mapping.append(
|
|
(start, end, start_snode, end_snode, intermediates)
|
|
)
|
|
|
|
return spw, reverse_mapping, intermediate_mapping
|
|
|
|
@staticmethod
|
|
def spathway_to_pathway(
|
|
package: "Package", spw: "SPathway", name: str = None, description: str = None
|
|
):
|
|
snode_to_node_mapping = dict()
|
|
|
|
root_nodes = spw.root_nodes
|
|
|
|
pw = Pathway.create(
|
|
package=package,
|
|
smiles=root_nodes[0].smiles,
|
|
name=name,
|
|
description=description,
|
|
predicted=True,
|
|
)
|
|
|
|
pw.setting = spw.prediction_setting
|
|
pw.save()
|
|
|
|
snode_to_node_mapping[root_nodes[0]] = pw.root_nodes[0]
|
|
|
|
if len(root_nodes) > 1:
|
|
for rn in root_nodes[1:]:
|
|
n = Node.create(pw, rn.smiles, depth=0)
|
|
snode_to_node_mapping[rn] = n
|
|
|
|
for snode, node in snode_to_node_mapping.items():
|
|
spw.snode_persist_lookup[snode] = node
|
|
|
|
spw.persist = pw
|
|
|
|
spw._sync_to_pathway()
|
|
|
|
return pw
|