Merge remote-tracking branch 'origin/develop' into feature/frontend_update

This commit is contained in:
2025-10-30 14:02:57 +13:00
40 changed files with 1927 additions and 331 deletions

View File

@ -135,6 +135,7 @@ USE_TZ = True
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
EMAIL_SUBJECT_PREFIX = "[enviPath] "
if DEBUG: if DEBUG:
EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend" EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend"
else: else:
@ -144,6 +145,8 @@ else:
EMAIL_HOST_USER = os.environ["EMAIL_HOST_USER"] EMAIL_HOST_USER = os.environ["EMAIL_HOST_USER"]
EMAIL_HOST_PASSWORD = os.environ["EMAIL_HOST_PASSWORD"] EMAIL_HOST_PASSWORD = os.environ["EMAIL_HOST_PASSWORD"]
EMAIL_PORT = 587 EMAIL_PORT = 587
DEFAULT_FROM_EMAIL = os.environ["DEFAULT_FROM_EMAIL"]
SERVER_EMAIL = os.environ["SERVER_EMAIL"]
AUTH_USER_MODEL = "epdb.User" AUTH_USER_MODEL = "epdb.User"
ADMIN_APPROVAL_REQUIRED = os.environ.get("ADMIN_APPROVAL_REQUIRED", "False") == "True" ADMIN_APPROVAL_REQUIRED = os.environ.get("ADMIN_APPROVAL_REQUIRED", "False") == "True"

View File

@ -7,6 +7,7 @@ from .models import (
GroupPackagePermission, GroupPackagePermission,
Package, Package,
MLRelativeReasoning, MLRelativeReasoning,
EnviFormer,
Compound, Compound,
CompoundStructure, CompoundStructure,
SimpleAmbitRule, SimpleAmbitRule,
@ -50,6 +51,10 @@ class MLRelativeReasoningAdmin(EPAdmin):
pass pass
class EnviFormerAdmin(EPAdmin):
pass
class CompoundAdmin(EPAdmin): class CompoundAdmin(EPAdmin):
pass pass
@ -104,6 +109,7 @@ admin.site.register(Group, GroupAdmin)
admin.site.register(GroupPackagePermission, GroupPackagePermissionAdmin) admin.site.register(GroupPackagePermission, GroupPackagePermissionAdmin)
admin.site.register(Package, PackageAdmin) admin.site.register(Package, PackageAdmin)
admin.site.register(MLRelativeReasoning, MLRelativeReasoningAdmin) admin.site.register(MLRelativeReasoning, MLRelativeReasoningAdmin)
admin.site.register(EnviFormer, EnviFormerAdmin)
admin.site.register(Compound, CompoundAdmin) admin.site.register(Compound, CompoundAdmin)
admin.site.register(CompoundStructure, CompoundStructureAdmin) admin.site.register(CompoundStructure, CompoundStructureAdmin)
admin.site.register(SimpleAmbitRule, SimpleAmbitRuleAdmin) admin.site.register(SimpleAmbitRule, SimpleAmbitRuleAdmin)

View File

@ -26,6 +26,7 @@ from epdb.models import (
Compound, Compound,
Reaction, Reaction,
CompoundStructure, CompoundStructure,
EnzymeLink,
) )
from utilities.chem import FormatConverter from utilities.chem import FormatConverter
from utilities.misc import PackageImporter, PackageExporter from utilities.misc import PackageImporter, PackageExporter
@ -617,6 +618,8 @@ class PackageManager(object):
parent_mapping = {} parent_mapping = {}
# Mapping old scen_id to old_obj_id # Mapping old scen_id to old_obj_id
scen_mapping = defaultdict(list) scen_mapping = defaultdict(list)
# Enzymelink Mapping rule_id to enzymelink objects
enzyme_mapping = defaultdict(list)
# Store Scenarios # Store Scenarios
for scenario in data["scenarios"]: for scenario in data["scenarios"]:
@ -648,9 +651,7 @@ class PackageManager(object):
# Broken eP Data # Broken eP Data
if name == "initialmasssediment" and addinf_data == "missing data": if name == "initialmasssediment" and addinf_data == "missing data":
continue continue
if name == "columnheight" and addinf_data == "(2)-(2.5);(6)-(8)":
# TODO Enzymes arent ready yet
if name == "enzyme":
continue continue
try: try:
@ -740,6 +741,9 @@ class PackageManager(object):
for scen in rule["scenarios"]: for scen in rule["scenarios"]:
scen_mapping[scen["id"]].append(r) scen_mapping[scen["id"]].append(r)
for enzyme_link in rule.get("enzymeLinks", []):
enzyme_mapping[r.uuid].append(enzyme_link)
print("Par: ", len(par_rules)) print("Par: ", len(par_rules))
print("Seq: ", len(seq_rules)) print("Seq: ", len(seq_rules))
@ -757,6 +761,9 @@ class PackageManager(object):
for scen in par_rule["scenarios"]: for scen in par_rule["scenarios"]:
scen_mapping[scen["id"]].append(r) scen_mapping[scen["id"]].append(r)
for enzyme_link in par_rule.get("enzymeLinks", []):
enzyme_mapping[r.uuid].append(enzyme_link)
for simple_rule in par_rule["simpleRules"]: for simple_rule in par_rule["simpleRules"]:
if simple_rule["id"] in mapping: if simple_rule["id"] in mapping:
r.simple_rules.add(SimpleRule.objects.get(uuid=mapping[simple_rule["id"]])) r.simple_rules.add(SimpleRule.objects.get(uuid=mapping[simple_rule["id"]]))
@ -777,6 +784,9 @@ class PackageManager(object):
for scen in seq_rule["scenarios"]: for scen in seq_rule["scenarios"]:
scen_mapping[scen["id"]].append(r) scen_mapping[scen["id"]].append(r)
for enzyme_link in seq_rule.get("enzymeLinks", []):
enzyme_mapping[r.uuid].append(enzyme_link)
for i, simple_rule in enumerate(seq_rule["simpleRules"]): for i, simple_rule in enumerate(seq_rule["simpleRules"]):
sro = SequentialRuleOrdering() sro = SequentialRuleOrdering()
sro.simple_rule = simple_rule sro.simple_rule = simple_rule
@ -910,6 +920,39 @@ class PackageManager(object):
print("Scenarios linked...") print("Scenarios linked...")
# Import Enzyme Links
for rule_uuid, enzyme_links in enzyme_mapping.items():
r = Rule.objects.get(uuid=rule_uuid)
for enzyme in enzyme_links:
e = EnzymeLink()
e.uuid = UUID(enzyme["id"].split("/")[-1]) if keep_ids else uuid4()
e.rule = r
e.name = enzyme["name"]
e.ec_number = enzyme["ecNumber"]
e.classification_level = enzyme["classificationLevel"]
e.linking_method = enzyme["linkingMethod"]
e.save()
for reaction in enzyme["reactionLinkEvidence"]:
reaction = Reaction.objects.get(uuid=mapping[reaction["id"]])
e.reaction_evidence.add(reaction)
for edge in enzyme["edgeLinkEvidence"]:
edge = Edge.objects.get(uuid=mapping[edge["id"]])
e.reaction_evidence.add(edge)
for evidence in enzyme["linkEvidence"]:
matches = re.findall(r">(R[0-9]+)<", evidence["evidence"])
if not matches or len(matches) != 1:
logger.warning(f"Could not find reaction id in {evidence['evidence']}")
continue
e.add_kegg_reaction_id(matches[0])
e.save()
print("Enzyme links imported...")
print("Import statistics:") print("Import statistics:")
print("Package {} stored".format(pack.url)) print("Package {} stored".format(pack.url))
print("Imported {} compounds".format(Compound.objects.filter(package=pack).count())) print("Imported {} compounds".format(Compound.objects.filter(package=pack).count()))

View File

@ -7,10 +7,11 @@ from epdb.models import MLRelativeReasoning, EnviFormer, Package
class Command(BaseCommand): class Command(BaseCommand):
"""This command can be run with """This command can be run with
`python manage.py create_ml_models [model_names] -d [data_packages] OPTIONAL: -e [eval_packages]` `python manage.py create_ml_models [model_names] -d [data_packages] FOR MLRR ONLY: -r [rule_packages]
For example, to train both EnviFormer and MLRelativeReasoning on BBD and SOIL and evaluate them on SLUDGE OPTIONAL: -e [eval_packages] -t threshold`
the below command would be used: For example, to train both EnviFormer and MLRelativeReasoning on BBD and SOIL and evaluate them on SLUDGE with a
`python manage.py create_ml_models enviformer mlrr -d bbd soil -e sludge threshold of 0.6, the below command would be used:
`python manage.py create_ml_models enviformer mlrr -d bbd soil -e sludge -t 0.6
""" """
def add_arguments(self, parser): def add_arguments(self, parser):
@ -34,6 +35,13 @@ class Command(BaseCommand):
help="Rule Packages mandatory for MLRR", help="Rule Packages mandatory for MLRR",
default=[], default=[],
) )
parser.add_argument(
"-t",
"--threshold",
type=float,
help="Model prediction threshold",
default=0.5,
)
@transaction.atomic @transaction.atomic
def handle(self, *args, **options): def handle(self, *args, **options):
@ -67,7 +75,11 @@ class Command(BaseCommand):
return packages return packages
# Iteratively create models in options["model_names"] # Iteratively create models in options["model_names"]
print(f"Creating models: {options['model_names']}") print(f"Creating models: {options['model_names']}\n"
f"Data packages: {options['data_packages']}\n"
f"Rule Packages (only for MLRR): {options['rule_packages']}\n"
f"Eval Packages: {options['eval_packages']}\n"
f"Threshold: {options['threshold']:.2f}")
data_packages = decode_packages(options["data_packages"]) data_packages = decode_packages(options["data_packages"])
eval_packages = decode_packages(options["eval_packages"]) eval_packages = decode_packages(options["eval_packages"])
rule_packages = decode_packages(options["rule_packages"]) rule_packages = decode_packages(options["rule_packages"])
@ -78,9 +90,10 @@ class Command(BaseCommand):
pack, pack,
data_packages=data_packages, data_packages=data_packages,
eval_packages=eval_packages, eval_packages=eval_packages,
threshold=0.5, threshold=options['threshold'],
name="EnviFormer - T0.5", name=f"EnviFormer - {', '.join(options['data_packages'])} - T{options['threshold']:.2f}",
description="EnviFormer transformer", description=f"EnviFormer transformer trained on {options['data_packages']} "
f"evaluated on {options['eval_packages']}.",
) )
elif model_name == "mlrr": elif model_name == "mlrr":
model = MLRelativeReasoning.create( model = MLRelativeReasoning.create(
@ -88,9 +101,10 @@ class Command(BaseCommand):
rule_packages=rule_packages, rule_packages=rule_packages,
data_packages=data_packages, data_packages=data_packages,
eval_packages=eval_packages, eval_packages=eval_packages,
threshold=0.5, threshold=options['threshold'],
name="ECC - BBD - T0.5", name=f"ECC - {', '.join(options['data_packages'])} - T{options['threshold']:.2f}",
description="ML Relative Reasoning", description=f"ML Relative Reasoning trained on {options['data_packages']} with rules from "
f"{options['rule_packages']} and evaluated on {options['eval_packages']}.",
) )
else: else:
raise ValueError(f"Cannot create model of type {model_name}, unknown model type") raise ValueError(f"Cannot create model of type {model_name}, unknown model type")
@ -100,6 +114,6 @@ class Command(BaseCommand):
print(f"Training {model_name}") print(f"Training {model_name}")
model.build_model() model.build_model()
print(f"Evaluating {model_name}") print(f"Evaluating {model_name}")
model.evaluate_model() model.evaluate_model(False, eval_packages=eval_packages)
print(f"Saving {model_name}") print(f"Saving {model_name}")
model.save() model.save()

View File

@ -0,0 +1,59 @@
import json
import os
import tarfile
from tempfile import TemporaryDirectory
from django.conf import settings as s
from django.core.management.base import BaseCommand
from django.db import transaction
from epdb.models import EnviFormer
class Command(BaseCommand):
def add_arguments(self, parser):
parser.add_argument(
"model",
type=str,
help="Model UUID of the Model to Dump",
)
parser.add_argument("--output", type=str)
def package_dict_and_folder(self, dict_data, folder_path, output_path):
with TemporaryDirectory() as tmpdir:
dict_filename = os.path.join(tmpdir, "data.json")
with open(dict_filename, "w", encoding="utf-8") as f:
json.dump(dict_data, f, indent=2)
with tarfile.open(output_path, "w:gz") as tar:
tar.add(dict_filename, arcname="data.json")
tar.add(folder_path, arcname=os.path.basename(folder_path))
os.remove(dict_filename)
@transaction.atomic
def handle(self, *args, **options):
output = options["output"]
if os.path.exists(output):
raise ValueError(f"Output file {output} already exists")
model = EnviFormer.objects.get(uuid=options["model"])
data = {
"uuid": str(model.uuid),
"name": model.name,
"description": model.description,
"kv": model.kv,
"data_packages_uuids": [str(p.uuid) for p in model.data_packages.all()],
"eval_packages_uuids": [str(p.uuid) for p in model.data_packages.all()],
"threshold": model.threshold,
"eval_results": model.eval_results,
"multigen_eval": model.multigen_eval,
"model_status": model.model_status,
}
model_folder = os.path.join(s.MODEL_DIR, "enviformer", str(model.uuid))
self.package_dict_and_folder(data, model_folder, output)

View File

@ -0,0 +1,81 @@
import json
import os
import shutil
import tarfile
from tempfile import TemporaryDirectory
from django.conf import settings as s
from django.core.management.base import BaseCommand
from django.db import transaction
from epdb.models import EnviFormer, Package
class Command(BaseCommand):
def add_arguments(self, parser):
parser.add_argument(
"input",
type=str,
help=".tar.gz file containing the Model dump.",
)
parser.add_argument(
"package",
type=str,
help="Package UUID where the Model should be loaded to.",
)
def read_dict_and_folder_from_archive(self, archive_path, extract_to="extracted_folder"):
with tarfile.open(archive_path, "r:gz") as tar:
tar.extractall(extract_to)
dict_path = os.path.join(extract_to, "data.json")
if not os.path.exists(dict_path):
raise FileNotFoundError("data.json not found in the archive.")
with open(dict_path, "r", encoding="utf-8") as f:
data_dict = json.load(f)
extracted_items = os.listdir(extract_to)
folders = [item for item in extracted_items if item != "data.json"]
folder_path = os.path.join(extract_to, folders[0]) if folders else None
return data_dict, folder_path
@transaction.atomic
def handle(self, *args, **options):
if not os.path.exists(options["input"]):
raise ValueError(f"Input file {options['input']} does not exist.")
target_package = Package.objects.get(uuid=options["package"])
with TemporaryDirectory() as tmpdir:
data, folder = self.read_dict_and_folder_from_archive(options["input"], tmpdir)
model = EnviFormer()
model.package = target_package
# model.uuid = data["uuid"]
model.name = data["name"]
model.description = data["description"]
model.kv = data["kv"]
model.threshold = float(data["threshold"])
model.eval_results = data["eval_results"]
model.multigen_eval = data["multigen_eval"]
model.model_status = data["model_status"]
model.save()
for p_uuid in data["data_packages_uuids"]:
p = Package.objects.get(uuid=p_uuid)
model.data_packages.add(p)
for p_uuid in data["eval_packages_uuids"]:
p = Package.objects.get(uuid=p_uuid)
model.eval_packages.add(p)
target_folder = os.path.join(s.MODEL_DIR, "enviformer", str(model.uuid))
shutil.copytree(folder, target_folder)
os.rename(
os.path.join(s.MODEL_DIR, "enviformer", str(model.uuid), f"{data['uuid']}.ckpt"),
os.path.join(s.MODEL_DIR, "enviformer", str(model.uuid), f"{model.uuid}.ckpt"),
)

View File

@ -1,8 +1,10 @@
from django.apps import apps from django.apps import apps
from django.core.management.base import BaseCommand from django.core.management.base import BaseCommand
from django.db.models import F, Value from django.db.models import F, Value, TextField, JSONField
from django.db.models.functions import Replace from django.db.models.functions import Replace, Cast
from epdb.models import EnviPathModel
class Command(BaseCommand): class Command(BaseCommand):
@ -41,6 +43,7 @@ class Command(BaseCommand):
"RuleBasedRelativeReasoning", "RuleBasedRelativeReasoning",
"EnviFormer", "EnviFormer",
"ApplicabilityDomain", "ApplicabilityDomain",
"EnzymeLink",
] ]
for model in MODELS: for model in MODELS:
obj_cls = apps.get_model("epdb", model) obj_cls = apps.get_model("epdb", model)
@ -48,3 +51,14 @@ class Command(BaseCommand):
obj_cls.objects.update( obj_cls.objects.update(
url=Replace(F("url"), Value(options["old"]), Value(options["new"])) url=Replace(F("url"), Value(options["old"]), Value(options["new"]))
) )
if issubclass(obj_cls, EnviPathModel):
obj_cls.objects.update(
kv=Cast(
Replace(
Cast(F("kv"), output_field=TextField()),
Value(options["old"]),
Value(options["new"]),
),
output_field=JSONField(),
)
)

View File

@ -0,0 +1,38 @@
from datetime import date, timedelta
from django.core.management.base import BaseCommand
from django.db import transaction
from epdb.models import JobLog
class Command(BaseCommand):
def add_arguments(self, parser):
parser.add_argument(
"--cleanup",
type=int,
default=None,
help="Remove all logs older than this number of days. Default is None, which does not remove any logs.",
)
@transaction.atomic
def handle(self, *args, **options):
if options["cleanup"] is not None:
cleanup_dt = date.today() - timedelta(days=options["cleanup"])
print(JobLog.objects.filter(created__lt=cleanup_dt).delete())
logs = JobLog.objects.filter(status="INITIAL")
print(f"Found {logs.count()} logs to update")
updated = 0
for log in logs:
res = log.check_for_update()
if res:
updated += 1
print(f"Updated {updated} logs")
from django.db.models import Count
qs = JobLog.objects.values("status").annotate(total=Count("status"))
for r in qs:
print(r["status"], r["total"])

View File

@ -0,0 +1,64 @@
# Generated by Django 5.2.7 on 2025-10-10 06:58
import django.db.models.deletion
import django.utils.timezone
import model_utils.fields
import uuid
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("epdb", "0007_alter_enviformer_options_enviformer_app_domain_and_more"),
]
operations = [
migrations.CreateModel(
name="EnzymeLink",
fields=[
(
"id",
models.BigAutoField(
auto_created=True, primary_key=True, serialize=False, verbose_name="ID"
),
),
(
"created",
model_utils.fields.AutoCreatedField(
default=django.utils.timezone.now, editable=False, verbose_name="created"
),
),
(
"modified",
model_utils.fields.AutoLastModifiedField(
default=django.utils.timezone.now, editable=False, verbose_name="modified"
),
),
(
"uuid",
models.UUIDField(
default=uuid.uuid4, unique=True, verbose_name="UUID of this object"
),
),
("name", models.TextField(default="no name", verbose_name="Name")),
(
"description",
models.TextField(default="no description", verbose_name="Descriptions"),
),
("url", models.TextField(null=True, unique=True, verbose_name="URL")),
("kv", models.JSONField(blank=True, default=dict, null=True)),
("ec_number", models.TextField(verbose_name="EC Number")),
("classification_level", models.IntegerField(verbose_name="Classification Level")),
("linking_method", models.TextField(verbose_name="Linking Method")),
("edge_evidence", models.ManyToManyField(to="epdb.edge")),
("reaction_evidence", models.ManyToManyField(to="epdb.reaction")),
(
"rule",
models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to="epdb.rule"),
),
],
options={
"abstract": False,
},
),
]

View File

@ -0,0 +1,66 @@
# Generated by Django 5.2.7 on 2025-10-27 09:39
import django.db.models.deletion
import django.utils.timezone
import model_utils.fields
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("epdb", "0008_enzymelink"),
]
operations = [
migrations.CreateModel(
name="JobLog",
fields=[
(
"id",
models.BigAutoField(
auto_created=True, primary_key=True, serialize=False, verbose_name="ID"
),
),
(
"created",
model_utils.fields.AutoCreatedField(
default=django.utils.timezone.now, editable=False, verbose_name="created"
),
),
(
"modified",
model_utils.fields.AutoLastModifiedField(
default=django.utils.timezone.now, editable=False, verbose_name="modified"
),
),
("task_id", models.UUIDField(unique=True)),
("job_name", models.TextField()),
(
"status",
models.CharField(
choices=[
("INITIAL", "Initial"),
("SUCCESS", "Success"),
("FAILURE", "Failure"),
("REVOKED", "Revoked"),
("IGNORED", "Ignored"),
],
default="INITIAL",
max_length=20,
),
),
("done_at", models.DateTimeField(blank=True, default=None, null=True)),
("task_result", models.TextField(blank=True, default=None, null=True)),
(
"user",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL
),
),
],
options={
"abstract": False,
},
),
]

View File

@ -310,7 +310,7 @@ class ExternalDatabase(TimeStampedModel):
}, },
{ {
"database": ExternalDatabase.objects.get(name="ChEBI"), "database": ExternalDatabase.objects.get(name="ChEBI"),
"placeholder": "ChEBI ID without prefix e.g. 12345", "placeholder": "ChEBI ID without prefix e.g. 10576",
}, },
], ],
"structure": [ "structure": [
@ -328,7 +328,7 @@ class ExternalDatabase(TimeStampedModel):
}, },
{ {
"database": ExternalDatabase.objects.get(name="ChEBI"), "database": ExternalDatabase.objects.get(name="ChEBI"),
"placeholder": "ChEBI ID without prefix e.g. 12345", "placeholder": "ChEBI ID without prefix e.g. 10576",
}, },
], ],
"reaction": [ "reaction": [
@ -342,7 +342,7 @@ class ExternalDatabase(TimeStampedModel):
}, },
{ {
"database": ExternalDatabase.objects.get(name="UniProt"), "database": ExternalDatabase.objects.get(name="UniProt"),
"placeholder": "Query ID for UniPro e.g. rhea:12345", "placeholder": "Query ID for UniProt e.g. rhea:12345",
}, },
], ],
} }
@ -477,7 +477,7 @@ class ChemicalIdentifierMixin(ExternalIdentifierMixin):
return self.add_external_identifier("CAS", cas_number) return self.add_external_identifier("CAS", cas_number)
def get_pubchem_identifiers(self): def get_pubchem_identifiers(self):
return self.get_external_identifier("PubChem Compound") or self.get_external_identifier( return self.get_external_identifier("PubChem Compound") | self.get_external_identifier(
"PubChem Substance" "PubChem Substance"
) )
@ -494,6 +494,20 @@ class ChemicalIdentifierMixin(ExternalIdentifierMixin):
return self.get_external_identifier("CAS") return self.get_external_identifier("CAS")
class KEGGIdentifierMixin(ExternalIdentifierMixin):
@property
def kegg_reaction_links(self):
return self.get_external_identifier("KEGG Reaction")
def add_kegg_reaction_id(self, kegg_id):
return self.add_external_identifier(
"KEGG Reaction", kegg_id, f"https://www.genome.jp/entry/{kegg_id}"
)
class Meta:
abstract = True
class ReactionIdentifierMixin(ExternalIdentifierMixin): class ReactionIdentifierMixin(ExternalIdentifierMixin):
class Meta: class Meta:
abstract = True abstract = True
@ -1014,6 +1028,26 @@ class CompoundStructure(EnviPathModel, AliasMixin, ScenarioMixin, ChemicalIdenti
return self.compound.default_structure == self return self.compound.default_structure == self
class EnzymeLink(EnviPathModel, KEGGIdentifierMixin):
rule = models.ForeignKey("Rule", on_delete=models.CASCADE, db_index=True)
ec_number = models.TextField(blank=False, null=False, verbose_name="EC Number")
classification_level = models.IntegerField(
blank=False, null=False, verbose_name="Classification Level"
)
linking_method = models.TextField(blank=False, null=False, verbose_name="Linking Method")
reaction_evidence = models.ManyToManyField("epdb.Reaction")
edge_evidence = models.ManyToManyField("epdb.Edge")
external_identifiers = GenericRelation("ExternalIdentifier")
def _url(self):
return "{}/enzymelink/{}".format(self.rule.url, self.uuid)
def get_group(self) -> str:
return ".".join(self.ec_number.split(".")[:3]) + ".-"
class Rule(PolymorphicModel, EnviPathModel, AliasMixin, ScenarioMixin): class Rule(PolymorphicModel, EnviPathModel, AliasMixin, ScenarioMixin):
package = models.ForeignKey( package = models.ForeignKey(
"epdb.Package", verbose_name="Package", on_delete=models.CASCADE, db_index=True "epdb.Package", verbose_name="Package", on_delete=models.CASCADE, db_index=True
@ -1095,6 +1129,18 @@ class Rule(PolymorphicModel, EnviPathModel, AliasMixin, ScenarioMixin):
return new_rule return new_rule
def enzymelinks(self):
return self.enzymelink_set.all()
def get_grouped_enzymelinks(self):
res = defaultdict(list)
for el in self.enzymelinks():
key = ".".join(el.ec_number.split(".")[:3]) + ".-"
res[key].append(el)
return dict(res)
class SimpleRule(Rule): class SimpleRule(Rule):
pass pass
@ -1437,6 +1483,16 @@ class Reaction(EnviPathModel, AliasMixin, ScenarioMixin, ReactionIdentifierMixin
id__in=Edge.objects.filter(edge_label=self).values("pathway_id") id__in=Edge.objects.filter(edge_label=self).values("pathway_id")
).order_by("name") ).order_by("name")
def get_related_enzymes(self):
res = []
edges = Edge.objects.filter(edge_label=self)
for e in edges:
for scen in e.scenarios.all():
for ai in scen.additional_information.keys():
if ai == "Enzyme":
res.extend(scen.additional_information[ai])
return res
class Pathway(EnviPathModel, AliasMixin, ScenarioMixin): class Pathway(EnviPathModel, AliasMixin, ScenarioMixin):
package = models.ForeignKey( package = models.ForeignKey(
@ -2169,10 +2225,18 @@ class PackageBasedModel(EPModel):
self.model_status = self.BUILT_NOT_EVALUATED self.model_status = self.BUILT_NOT_EVALUATED
self.save() self.save()
def evaluate_model(self): def evaluate_model(self, multigen: bool, eval_packages: List["Package"] = None):
if self.model_status != self.BUILT_NOT_EVALUATED: if self.model_status != self.BUILT_NOT_EVALUATED:
raise ValueError(f"Can't evaluate a model in state {self.model_status}!") raise ValueError(f"Can't evaluate a model in state {self.model_status}!")
if multigen:
self.multigen_eval = multigen
self.save()
if eval_packages is not None:
for p in eval_packages:
self.eval_packages.add(p)
self.model_status = self.EVALUATING self.model_status = self.EVALUATING
self.save() self.save()
@ -2469,7 +2533,6 @@ class RuleBasedRelativeReasoning(PackageBasedModel):
package: "Package", package: "Package",
rule_packages: List["Package"], rule_packages: List["Package"],
data_packages: List["Package"], data_packages: List["Package"],
eval_packages: List["Package"],
threshold: float = 0.5, threshold: float = 0.5,
min_count: int = 10, min_count: int = 10,
max_count: int = 0, max_count: int = 0,
@ -2518,10 +2581,6 @@ class RuleBasedRelativeReasoning(PackageBasedModel):
for p in rule_packages: for p in rule_packages:
rbrr.data_packages.add(p) rbrr.data_packages.add(p)
if eval_packages:
for p in eval_packages:
rbrr.eval_packages.add(p)
rbrr.save() rbrr.save()
return rbrr return rbrr
@ -2576,7 +2635,6 @@ class MLRelativeReasoning(PackageBasedModel):
package: "Package", package: "Package",
rule_packages: List["Package"], rule_packages: List["Package"],
data_packages: List["Package"], data_packages: List["Package"],
eval_packages: List["Package"],
threshold: float = 0.5, threshold: float = 0.5,
name: "str" = None, name: "str" = None,
description: str = None, description: str = None,
@ -2616,10 +2674,6 @@ class MLRelativeReasoning(PackageBasedModel):
for p in rule_packages: for p in rule_packages:
mlrr.data_packages.add(p) mlrr.data_packages.add(p)
if eval_packages:
for p in eval_packages:
mlrr.eval_packages.add(p)
if build_app_domain: if build_app_domain:
ad = ApplicabilityDomain.create( ad = ApplicabilityDomain.create(
mlrr, mlrr,
@ -2939,7 +2993,6 @@ class EnviFormer(PackageBasedModel):
def create( def create(
package: "Package", package: "Package",
data_packages: List["Package"], data_packages: List["Package"],
eval_packages: List["Package"],
threshold: float = 0.5, threshold: float = 0.5,
name: "str" = None, name: "str" = None,
description: str = None, description: str = None,
@ -2972,10 +3025,6 @@ class EnviFormer(PackageBasedModel):
for p in data_packages: for p in data_packages:
mod.data_packages.add(p) mod.data_packages.add(p)
if eval_packages:
for p in eval_packages:
mod.eval_packages.add(p)
# if build_app_domain: # if build_app_domain:
# ad = ApplicabilityDomain.create(mod, app_domain_num_neighbours, app_domain_reliability_threshold, # ad = ApplicabilityDomain.create(mod, app_domain_num_neighbours, app_domain_reliability_threshold,
# app_domain_local_compatibility_threshold) # app_domain_local_compatibility_threshold)
@ -2989,7 +3038,8 @@ class EnviFormer(PackageBasedModel):
from enviformer import load from enviformer import load
ckpt = os.path.join(s.MODEL_DIR, "enviformer", str(self.uuid), f"{self.uuid}.ckpt") ckpt = os.path.join(s.MODEL_DIR, "enviformer", str(self.uuid), f"{self.uuid}.ckpt")
return load(device=s.ENVIFORMER_DEVICE, ckpt_path=ckpt) mod = load(device=s.ENVIFORMER_DEVICE, ckpt_path=ckpt)
return mod
def predict(self, smiles) -> List["PredictionResult"]: def predict(self, smiles) -> List["PredictionResult"]:
return self.predict_batch([smiles])[0] return self.predict_batch([smiles])[0]
@ -3003,8 +3053,12 @@ class EnviFormer(PackageBasedModel):
for smiles in smiles_list for smiles in smiles_list
] ]
logger.info(f"Submitting {canon_smiles} to {self.name}") logger.info(f"Submitting {canon_smiles} to {self.name}")
start = datetime.now()
products_list = self.model.predict_batch(canon_smiles) products_list = self.model.predict_batch(canon_smiles)
logger.info(f"Got results {products_list}") end = datetime.now()
logger.info(
f"Prediction took {(end - start).total_seconds():.2f} seconds. Got results {products_list}"
)
results = [] results = []
for products in products_list: for products in products_list:
@ -3031,6 +3085,7 @@ class EnviFormer(PackageBasedModel):
start = datetime.now() start = datetime.now()
# Standardise reactions for the training data, EnviFormer ignores stereochemistry currently # Standardise reactions for the training data, EnviFormer ignores stereochemistry currently
co2 = {"C(=O)=O", "O=C=O"}
ds = [] ds = []
for reaction in self._get_reactions(): for reaction in self._get_reactions():
educts = ".".join( educts = ".".join(
@ -3045,6 +3100,7 @@ class EnviFormer(PackageBasedModel):
for smile in reaction.products.all() for smile in reaction.products.all()
] ]
) )
if products not in co2:
ds.append(f"{educts}>>{products}") ds.append(f"{educts}>>{products}")
end = datetime.now() end = datetime.now()
@ -3081,10 +3137,18 @@ class EnviFormer(PackageBasedModel):
args = {"clz": "EnviFormer"} args = {"clz": "EnviFormer"}
return args return args
def evaluate_model(self): def evaluate_model(self, multigen: bool, eval_packages: List["Package"] = None):
if self.model_status != self.BUILT_NOT_EVALUATED: if self.model_status != self.BUILT_NOT_EVALUATED:
raise ValueError(f"Can't evaluate a model in state {self.model_status}!") raise ValueError(f"Can't evaluate a model in state {self.model_status}!")
if multigen:
self.multigen_eval = multigen
self.save()
if eval_packages is not None:
for p in eval_packages:
self.eval_packages.add(p)
self.model_status = self.EVALUATING self.model_status = self.EVALUATING
self.save() self.save()
@ -3241,7 +3305,7 @@ class EnviFormer(PackageBasedModel):
ds = self.load_dataset() ds = self.load_dataset()
n_splits = 20 n_splits = 20
shuff = ShuffleSplit(n_splits=n_splits, test_size=0.25, random_state=42) shuff = ShuffleSplit(n_splits=n_splits, test_size=0.1, random_state=42)
# Single gen eval is done in one loop of train then evaluate rather than storing all n_splits trained models # Single gen eval is done in one loop of train then evaluate rather than storing all n_splits trained models
# this helps reduce the memory footprint. # this helps reduce the memory footprint.
@ -3309,7 +3373,7 @@ class EnviFormer(PackageBasedModel):
# Compute splits of the collected pathway and evaluate. Like single gen we train and evaluate in each # Compute splits of the collected pathway and evaluate. Like single gen we train and evaluate in each
# iteration instead of storing all trained models. # iteration instead of storing all trained models.
for split_id, (train, test) in enumerate( for split_id, (train, test) in enumerate(
ShuffleSplit(n_splits=n_splits, test_size=0.25, random_state=42).split(pathways) ShuffleSplit(n_splits=n_splits, test_size=0.1, random_state=42).split(pathways)
): ):
train_pathways = [pathways[i] for i in train] train_pathways = [pathways[i] for i in train]
test_pathways = [pathways[i] for i in test] test_pathways = [pathways[i] for i in test]
@ -3608,3 +3672,53 @@ class Setting(EnviPathModel):
self.public = True self.public = True
self.global_default = True self.global_default = True
self.save() self.save()
class JobLogStatus(models.TextChoices):
INITIAL = "INITIAL", "Initial"
SUCCESS = "SUCCESS", "Success"
FAILURE = "FAILURE", "Failure"
REVOKED = "REVOKED", "Revoked"
IGNORED = "IGNORED", "Ignored"
class JobLog(TimeStampedModel):
user = models.ForeignKey("epdb.User", models.CASCADE)
task_id = models.UUIDField(unique=True)
job_name = models.TextField(null=False, blank=False)
status = models.CharField(
max_length=20,
choices=JobLogStatus.choices,
default=JobLogStatus.INITIAL,
)
done_at = models.DateTimeField(null=True, blank=True, default=None)
task_result = models.TextField(null=True, blank=True, default=None)
def check_for_update(self):
async_res = self.get_result()
new_status = async_res.state
TERMINAL_STATES = [
"SUCCESS",
"FAILURE",
"REVOKED",
"IGNORED",
]
if new_status != self.status and new_status in TERMINAL_STATES:
self.status = new_status
self.done_at = async_res.date_done
if new_status == "SUCCESS":
self.task_result = async_res.result
self.save()
return True
return False
def get_result(self):
from celery.result import AsyncResult
return AsyncResult(str(self.task_id))

View File

@ -1,12 +1,56 @@
import logging import logging
from typing import Optional from datetime import datetime
from typing import Callable, Optional
from uuid import uuid4
from celery import shared_task from celery import shared_task
from epdb.models import Pathway, Node, EPModel, Setting from celery.utils.functional import LRUCache
from epdb.logic import SPathway
from epdb.logic import SPathway
from epdb.models import EPModel, JobLog, Node, Package, Pathway, Setting, User
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
ML_CACHE = LRUCache(3) # Cache the three most recent ML models to reduce load times.
def get_ml_model(model_pk: int):
if model_pk not in ML_CACHE:
ML_CACHE[model_pk] = EPModel.objects.get(id=model_pk)
return ML_CACHE[model_pk]
def dispatch_eager(user: "User", job: Callable, *args, **kwargs):
try:
x = job(*args, **kwargs)
log = JobLog()
log.user = user
log.task_id = uuid4()
log.job_name = job.__name__
log.status = "SUCCESS"
log.done_at = datetime.now()
log.task_result = str(x) if x else None
log.save()
return x
except Exception as e:
logger.exception(e)
raise e
def dispatch(user: "User", job: Callable, *args, **kwargs):
try:
x = job.delay(*args, **kwargs)
log = JobLog()
log.user = user
log.task_id = x.task_id
log.job_name = job.__name__
log.status = "INITIAL"
log.save()
return x.result
except Exception as e:
logger.exception(e)
raise e
@shared_task(queue="background") @shared_task(queue="background")
@ -16,7 +60,7 @@ def mul(a, b):
@shared_task(queue="predict") @shared_task(queue="predict")
def predict_simple(model_pk: int, smiles: str): def predict_simple(model_pk: int, smiles: str):
mod = EPModel.objects.get(id=model_pk) mod = get_ml_model(model_pk)
res = mod.predict(smiles) res = mod.predict(smiles)
return res return res
@ -26,17 +70,55 @@ def send_registration_mail(user_pk: int):
pass pass
@shared_task(queue="model") @shared_task(bind=True, queue="model")
def build_model(model_pk: int): def build_model(self, model_pk: int):
mod = EPModel.objects.get(id=model_pk) mod = EPModel.objects.get(id=model_pk)
if JobLog.objects.filter(task_id=self.request.id).exists():
JobLog.objects.filter(task_id=self.request.id).update(status="RUNNING", task_result=mod.url)
try:
mod.build_dataset() mod.build_dataset()
mod.build_model() mod.build_model()
except Exception as e:
if JobLog.objects.filter(task_id=self.request.id).exists():
JobLog.objects.filter(task_id=self.request.id).update(
status="FAILED", task_result=mod.url
)
raise e
if JobLog.objects.filter(task_id=self.request.id).exists():
JobLog.objects.filter(task_id=self.request.id).update(status="SUCCESS", task_result=mod.url)
return mod.url
@shared_task(queue="model") @shared_task(bind=True, queue="model")
def evaluate_model(model_pk: int): def evaluate_model(self, model_pk: int, multigen: bool, package_pks: Optional[list] = None):
packages = None
if package_pks:
packages = Package.objects.filter(pk__in=package_pks)
mod = EPModel.objects.get(id=model_pk) mod = EPModel.objects.get(id=model_pk)
mod.evaluate_model() if JobLog.objects.filter(task_id=self.request.id).exists():
JobLog.objects.filter(task_id=self.request.id).update(status="RUNNING", task_result=mod.url)
try:
mod.evaluate_model(multigen, eval_packages=packages)
except Exception as e:
if JobLog.objects.filter(task_id=self.request.id).exists():
JobLog.objects.filter(task_id=self.request.id).update(
status="FAILED", task_result=mod.url
)
raise e
if JobLog.objects.filter(task_id=self.request.id).exists():
JobLog.objects.filter(task_id=self.request.id).update(status="SUCCESS", task_result=mod.url)
return mod.url
@shared_task(queue="model") @shared_task(queue="model")
@ -45,16 +127,26 @@ def retrain(model_pk: int):
mod.retrain() mod.retrain()
@shared_task(queue="predict") @shared_task(bind=True, queue="predict")
def predict( def predict(
pw_pk: int, pred_setting_pk: int, limit: Optional[int] = None, node_pk: Optional[int] = None self,
pw_pk: int,
pred_setting_pk: int,
limit: Optional[int] = None,
node_pk: Optional[int] = None,
) -> Pathway: ) -> Pathway:
pw = Pathway.objects.get(id=pw_pk) pw = Pathway.objects.get(id=pw_pk)
setting = Setting.objects.get(id=pred_setting_pk) setting = Setting.objects.get(id=pred_setting_pk)
# If the setting has a model add/restore it from the cache
if setting.model is not None:
setting.model = get_ml_model(setting.model.pk)
pw.kv.update(**{"status": "running"}) pw.kv.update(**{"status": "running"})
pw.save() pw.save()
if JobLog.objects.filter(task_id=self.request.id).exists():
JobLog.objects.filter(task_id=self.request.id).update(status="RUNNING", task_result=pw.url)
try: try:
# regular prediction # regular prediction
if limit is not None: if limit is not None:
@ -79,7 +171,18 @@ def predict(
except Exception as e: except Exception as e:
pw.kv.update({"status": "failed"}) pw.kv.update({"status": "failed"})
pw.save() pw.save()
if JobLog.objects.filter(task_id=self.request.id).exists():
JobLog.objects.filter(task_id=self.request.id).update(
status="FAILED", task_result=pw.url
)
raise e raise e
pw.kv.update(**{"status": "completed"}) pw.kv.update(**{"status": "completed"})
pw.save() pw.save()
if JobLog.objects.filter(task_id=self.request.id).exists():
JobLog.objects.filter(task_id=self.request.id).update(status="SUCCESS", task_result=pw.url)
return pw.url

View File

@ -1,8 +1,21 @@
from django import template from django import template
from pydantic import AnyHttpUrl, ValidationError
from pydantic.type_adapter import TypeAdapter
register = template.Library() register = template.Library()
url_adapter = TypeAdapter(AnyHttpUrl)
@register.filter @register.filter
def classname(obj): def classname(obj):
return obj.__class__.__name__ return obj.__class__.__name__
@register.filter
def is_url(value):
try:
url_adapter.validate_python(value)
return True
except ValidationError:
return False

View File

@ -1,5 +1,5 @@
from django.urls import path, re_path
from django.contrib.auth import views as auth_views from django.contrib.auth import views as auth_views
from django.urls import path, re_path
from . import views as v from . import views as v
@ -88,20 +88,36 @@ urlpatterns = [
v.package_rule, v.package_rule,
name="package rule detail", name="package rule detail",
), ),
re_path( # re_path(
rf"^package/(?P<package_uuid>{UUID})/simple-rdkit-rule/(?P<rule_uuid>{UUID})$", # rf"^package/(?P<package_uuid>{UUID})/simple-rdkit-rule/(?P<rule_uuid>{UUID})$",
v.package_rule, # v.package_rule,
name="package rule detail", # name="package rule detail",
), # ),
re_path( re_path(
rf"^package/(?P<package_uuid>{UUID})/parallel-rule/(?P<rule_uuid>{UUID})$", rf"^package/(?P<package_uuid>{UUID})/parallel-rule/(?P<rule_uuid>{UUID})$",
v.package_rule, v.package_rule,
name="package rule detail", name="package rule detail",
), ),
# re_path(
# rf"^package/(?P<package_uuid>{UUID})/sequential-rule/(?P<rule_uuid>{UUID})$",
# v.package_rule,
# name="package rule detail",
# ),
# EnzymeLinks
re_path( re_path(
rf"^package/(?P<package_uuid>{UUID})/sequential-rule/(?P<rule_uuid>{UUID})$", rf"^package/(?P<package_uuid>{UUID})/rule/(?P<rule_uuid>{UUID})/enzymelink/(?P<enzymelink_uuid>{UUID})$",
v.package_rule, v.package_rule_enzymelink,
name="package rule detail", name="package rule enzymelink detail",
),
re_path(
rf"^package/(?P<package_uuid>{UUID})/simple-ambit-rule/(?P<rule_uuid>{UUID})/enzymelink/(?P<enzymelink_uuid>{UUID})$",
v.package_rule_enzymelink,
name="package rule enzymelink detail",
),
re_path(
rf"^package/(?P<package_uuid>{UUID})/parallel-rule/(?P<rule_uuid>{UUID})/enzymelink/(?P<enzymelink_uuid>{UUID})$",
v.package_rule_enzymelink,
name="package rule enzymelink detail",
), ),
# Reaction # Reaction
re_path( re_path(
@ -174,15 +190,16 @@ urlpatterns = [
re_path(r"^indigo/dearomatize$", v.dearomatize, name="indigo_dearomatize"), re_path(r"^indigo/dearomatize$", v.dearomatize, name="indigo_dearomatize"),
re_path(r"^indigo/layout$", v.layout, name="indigo_layout"), re_path(r"^indigo/layout$", v.layout, name="indigo_layout"),
re_path(r"^depict$", v.depict, name="depict"), re_path(r"^depict$", v.depict, name="depict"),
re_path(r"^jobs", v.jobs, name="jobs"),
# OAuth Stuff # OAuth Stuff
path("o/userinfo/", v.userinfo, name="oauth_userinfo"), path("o/userinfo/", v.userinfo, name="oauth_userinfo"),
# Static Pages # Static Pages
re_path(r"^terms$", v.terms_of_use, name="terms_of_use"), re_path(r"^terms$", v.static_terms_of_use, name="terms_of_use"),
re_path(r"^privacy$", v.privacy_policy, name="privacy_policy"), re_path(r"^privacy$", v.static_privacy_policy, name="privacy_policy"),
re_path(r"^cookie-policy$", v.cookie_policy, name="cookie_policy"), re_path(r"^cookie-policy$", v.static_cookie_policy, name="cookie_policy"),
re_path(r"^about$", v.about_us, name="about_us"), re_path(r"^about$", v.static_about_us, name="about_us"),
re_path(r"^contact$", v.contact_support, name="contact_support"), re_path(r"^contact$", v.static_contact_support, name="contact_support"),
re_path(r"^jobs$", v.jobs, name="jobs"), re_path(r"^jobs$", v.static_jobs, name="jobs"),
re_path(r"^cite$", v.cite, name="cite"), re_path(r"^cite$", v.static_cite, name="cite"),
re_path(r"^legal$", v.legal, name="legal"), re_path(r"^legal$", v.static_legal, name="legal"),
] ]

View File

@ -46,6 +46,8 @@ from .models import (
Edge, Edge,
ExternalDatabase, ExternalDatabase,
ExternalIdentifier, ExternalIdentifier,
EnzymeLink,
JobLog,
) )
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -756,8 +758,8 @@ def package_models(request, package_uuid):
context["unreviewed_objects"] = unreviewed_model_qs context["unreviewed_objects"] = unreviewed_model_qs
context["model_types"] = { context["model_types"] = {
"ML Relative Reasoning": "ml-relative-reasoning", "ML Relative Reasoning": "mlrr",
"Rule Based Relative Reasoning": "rule-based-relative-reasoning", "Rule Based Relative Reasoning": "rbrr",
} }
if s.FLAGS.get("ENVIFORMER", False): if s.FLAGS.get("ENVIFORMER", False):
@ -777,48 +779,40 @@ def package_models(request, package_uuid):
model_type = request.POST.get("model-type") model_type = request.POST.get("model-type")
if model_type == "enviformer":
threshold = float(request.POST.get(f"{model_type}-threshold", 0.5))
mod = EnviFormer.create(current_package, name, description, threshold)
elif model_type == "ml-relative-reasoning" or model_type == "rule-based-relative-reasoning":
# Generic fields for ML and Rule Based # Generic fields for ML and Rule Based
rule_packages = request.POST.getlist("package-based-relative-reasoning-rule-packages") rule_packages = request.POST.getlist("model-rule-packages")
data_packages = request.POST.getlist("package-based-relative-reasoning-data-packages") data_packages = request.POST.getlist("model-data-packages")
eval_packages = request.POST.getlist(
"package-based-relative-reasoning-evaluation-packages", []
)
# Generic params # Generic params
params = { params = {
"package": current_package, "package": current_package,
"name": name, "name": name,
"description": description, "description": description,
"rule_packages": [
PackageManager.get_package_by_url(current_user, p) for p in rule_packages
],
"data_packages": [ "data_packages": [
PackageManager.get_package_by_url(current_user, p) for p in data_packages PackageManager.get_package_by_url(current_user, p) for p in data_packages
], ],
"eval_packages": [
PackageManager.get_package_by_url(current_user, p) for p in eval_packages
],
} }
if model_type == "ml-relative-reasoning": if model_type == "enviformer":
threshold = float(request.POST.get("model-threshold", 0.5))
params["threshold"] = threshold
mod = EnviFormer.create(**params)
elif model_type == "mlrr":
# ML Specific # ML Specific
threshold = float(request.POST.get(f"{model_type}-threshold", 0.5)) threshold = float(request.POST.get("model-threshold", 0.5))
# TODO handle additional fingerprinter # TODO handle additional fingerprinter
# fingerprinter = request.POST.get(f"{model_type}-fingerprinter") # fingerprinter = request.POST.get("model-fingerprinter")
params["rule_packages"] = [
PackageManager.get_package_by_url(current_user, p) for p in rule_packages
]
# App Domain related parameters # App Domain related parameters
build_ad = request.POST.get("build-app-domain", False) == "on" build_ad = request.POST.get("build-app-domain", False) == "on"
num_neighbors = request.POST.get("num-neighbors", 5) num_neighbors = request.POST.get("num-neighbors", 5)
reliability_threshold = request.POST.get("reliability-threshold", 0.5) reliability_threshold = request.POST.get("reliability-threshold", 0.5)
local_compatibility_threshold = request.POST.get( local_compatibility_threshold = request.POST.get("local-compatibility-threshold", 0.5)
"local-compatibility-threshold", 0.5
)
params["threshold"] = threshold params["threshold"] = threshold
# params['fingerprinter'] = fingerprinter # params['fingerprinter'] = fingerprinter
@ -828,18 +822,24 @@ def package_models(request, package_uuid):
params["app_domain_local_compatibility_threshold"] = local_compatibility_threshold params["app_domain_local_compatibility_threshold"] = local_compatibility_threshold
mod = MLRelativeReasoning.create(**params) mod = MLRelativeReasoning.create(**params)
else: elif model_type == "rbrr":
params["rule_packages"] = [
PackageManager.get_package_by_url(current_user, p) for p in rule_packages
]
mod = RuleBasedRelativeReasoning.create(**params) mod = RuleBasedRelativeReasoning.create(**params)
elif s.FLAGS.get("PLUGINS", False) and model_type in s.CLASSIFIER_PLUGINS.values():
from .tasks import build_model pass
build_model.delay(mod.pk)
else: else:
return error( return error(
request, "Invalid model type.", f'Model type "{model_type}" is not supported."' request, "Invalid model type.", f'Model type "{model_type}" is not supported."'
) )
return redirect(mod.url)
from .tasks import dispatch, build_model
dispatch(current_user, build_model, mod.pk)
return redirect(mod.url)
else: else:
return HttpResponseNotAllowed(["GET", "POST"]) return HttpResponseNotAllowed(["GET", "POST"])
@ -867,6 +867,10 @@ def package_model(request, package_uuid, model_uuid):
return JsonResponse({"error": f'"{smiles}" is not a valid SMILES'}, status=400) return JsonResponse({"error": f'"{smiles}" is not a valid SMILES'}, status=400)
if classify: if classify:
from epdb.tasks import dispatch_eager, predict_simple
res = dispatch_eager(current_user, predict_simple, current_model.pk, stand_smiles)
pred_res = current_model.predict(stand_smiles) pred_res = current_model.predict(stand_smiles)
res = [] res = []
@ -911,9 +915,25 @@ def package_model(request, package_uuid, model_uuid):
current_model.delete() current_model.delete()
return redirect(current_package.url + "/model") return redirect(current_package.url + "/model")
elif hidden == "evaluate": elif hidden == "evaluate":
from .tasks import evaluate_model from .tasks import dispatch, evaluate_model
eval_type = request.POST.get("model-evaluation-type")
if eval_type not in ["sg", "mg"]:
return error(
request,
"Invalid evaluation type",
f'Evaluation type "{eval_type}" is not supported. Only "sg" and "mg" are supported.',
)
multigen = eval_type == "mg"
eval_packages = request.POST.getlist("model-evaluation-packages")
eval_package_ids = [
PackageManager.get_package_by_url(current_user, p).id for p in eval_packages
]
dispatch(current_user, evaluate_model, current_model.pk, multigen, eval_package_ids)
evaluate_model.delay(current_model.pk)
return redirect(current_model.url) return redirect(current_model.url)
else: else:
return HttpResponseBadRequest() return HttpResponseBadRequest()
@ -1253,7 +1273,16 @@ def package_compound_structures(request, package_uuid, compound_uuid):
structure_smiles = request.POST.get("structure-smiles") structure_smiles = request.POST.get("structure-smiles")
structure_description = request.POST.get("structure-description") structure_description = request.POST.get("structure-description")
cs = current_compound.add_structure(structure_smiles, structure_name, structure_description) try:
cs = current_compound.add_structure(
structure_smiles, structure_name, structure_description
)
except ValueError:
return error(
request,
"Adding structure failed!",
"The structure could not be added as normalized structures don't match!",
)
return redirect(cs.url) return redirect(cs.url)
@ -1456,12 +1485,20 @@ def package_rule(request, package_uuid, rule_uuid):
logger.info( logger.info(
f"Rule {current_rule.uuid} returned multiple product sets on {smiles}, picking the first one." f"Rule {current_rule.uuid} returned multiple product sets on {smiles}, picking the first one."
) )
# Some Rules are touching unrelated areas which might result in ~ indicating
smirks = f"{stand_smiles}>>{'.'.join(sorted(res[0]))}" # any bond (-, =, #). For drawing we need a concrete bond. -> use single bond
product_smiles = [x.replace("~", "-") for x in res[0]]
smirks = f"{stand_smiles}>>{'.'.join(sorted(product_smiles))}"
# Usually the functional groups are a mapping of fg -> count # Usually the functional groups are a mapping of fg -> count
# As we are doing it on the fly here fake a high count to ensure that its properly highlighted # As we are doing it on the fly here fake a high count to ensure that its properly highlighted
if isinstance(current_rule, SimpleAmbitRule):
educt_functional_groups = {current_rule.reactants_smarts: 1000}
product_functional_groups = {current_rule.products_smarts: 1000}
else:
educt_functional_groups = {x: 1000 for x in current_rule.reactants_smarts} educt_functional_groups = {x: 1000 for x in current_rule.reactants_smarts}
product_functional_groups = {x: 1000 for x in current_rule.products_smarts} product_functional_groups = {x: 1000 for x in current_rule.products_smarts}
return HttpResponse( return HttpResponse(
IndigoUtils.smirks_to_svg( IndigoUtils.smirks_to_svg(
smirks, smirks,
@ -1531,6 +1568,32 @@ def package_rule(request, package_uuid, rule_uuid):
return HttpResponseNotAllowed(["GET", "POST"]) return HttpResponseNotAllowed(["GET", "POST"])
@package_permission_required()
def package_rule_enzymelink(request, package_uuid, rule_uuid, enzymelink_uuid):
current_user = _anonymous_or_real(request)
current_package = PackageManager.get_package_by_id(current_user, package_uuid)
current_rule = Rule.objects.get(package=current_package, uuid=rule_uuid)
current_enzymelink = EnzymeLink.objects.get(rule=current_rule, uuid=enzymelink_uuid)
if request.method == "GET":
context = get_base_context(request)
context["title"] = f"enviPath - {current_package.name} - {current_rule.name}"
context["meta"]["current_package"] = current_package
context["object_type"] = "enzyme"
context["breadcrumbs"] = breadcrumbs(
current_package, "rule", current_rule, "enzymelink", current_enzymelink
)
context["enzymelink"] = current_enzymelink
context["current_object"] = current_enzymelink
return render(request, "objects/enzymelink.html", context)
return HttpResponseNotAllowed(["GET"])
@package_permission_required() @package_permission_required()
def package_reactions(request, package_uuid): def package_reactions(request, package_uuid):
current_user = _anonymous_or_real(request) current_user = _anonymous_or_real(request)
@ -1768,9 +1831,9 @@ def package_pathways(request, package_uuid):
pw.setting = prediction_setting pw.setting = prediction_setting
pw.save() pw.save()
from .tasks import predict from .tasks import dispatch, predict
predict.delay(pw.pk, prediction_setting.pk, limit=limit) dispatch(current_user, predict, pw.pk, prediction_setting.pk, limit=limit)
return redirect(pw.url) return redirect(pw.url)
@ -1889,10 +1952,16 @@ def package_pathway(request, package_uuid, pathway_uuid):
if node_url: if node_url:
n = current_pathway.get_node(node_url) n = current_pathway.get_node(node_url)
from .tasks import predict from .tasks import dispatch, predict
dispatch(
current_user,
predict,
current_pathway.pk,
current_pathway.setting.pk,
node_pk=n.pk,
)
# Dont delay?
predict(current_pathway.pk, current_pathway.setting.pk, node_pk=n.pk)
return JsonResponse({"success": current_pathway.url}) return JsonResponse({"success": current_pathway.url})
return HttpResponseBadRequest() return HttpResponseBadRequest()
@ -1969,9 +2038,42 @@ def package_pathway_node(request, package_uuid, pathway_uuid, node_uuid):
if request.method == "GET": if request.method == "GET":
is_image_request = request.GET.get("image") is_image_request = request.GET.get("image")
is_highlight_request = request.GET.get("highlight", False)
is_highlight_reactivity = request.GET.get("highlightReactivity", False)
if is_image_request: if is_image_request:
if is_image_request == "svg": if is_image_request == "svg":
# TODO optimize this chain
if is_highlight_request:
# User functional groups covered by the model training data
fgs = {}
if current_pathway.setting:
if current_pathway.setting.model:
if current_pathway.setting.model.app_domain:
fgs = current_pathway.setting.model.app_domain.functional_groups
svg_data = IndigoUtils.mol_to_svg(
current_node.default_node_label.smiles, functional_groups=fgs
)
elif is_highlight_reactivity:
# Use reactant smarts to show all reaction sites
# set a high count to obtain a strong color
ad_data = current_node.get_app_domain_assessment_data()
fgs = {}
for t in ad_data.get("assessment", {}).get("transformations", []):
r = Rule.objects.get(url=t["rule"]["url"])
if isinstance(r, SimpleAmbitRule):
fgs[r.reactants_smarts] = 1000
else:
for sr in r.srs:
fgs[sr.reactants_smarts] = 1000
svg_data = IndigoUtils.mol_to_svg(
current_node.default_node_label.smiles, functional_groups=fgs
)
else:
svg_data = current_node.as_svg svg_data = current_node.as_svg
return HttpResponse(svg_data, content_type="image/svg+xml") return HttpResponse(svg_data, content_type="image/svg+xml")
context = get_base_context(request) context = get_base_context(request)
@ -2631,6 +2733,24 @@ def setting(request, setting_uuid):
pass pass
def jobs(request):
current_user = _anonymous_or_real(request)
context = get_base_context(request)
if request.method == "GET":
context["object_type"] = "joblog"
context["breadcrumbs"] = [
{"Home": s.SERVER_URL},
{"Jobs": s.SERVER_URL + "/jobs"},
]
if current_user.is_superuser:
context["jobs"] = JobLog.objects.all().order_by("-created")
else:
context["jobs"] = JobLog.objects.filter(user=current_user).order_by("-created")
return render(request, "collections/joblog.html", context)
########### ###########
# KETCHER # # KETCHER #
########### ###########
@ -2705,49 +2825,49 @@ def userinfo(request):
# Static Pages # Static Pages
def terms_of_use(request): def static_terms_of_use(request):
context = get_base_context(request) context = get_base_context(request)
context["title"] = "enviPath - Terms of Use" context["title"] = "enviPath - Terms of Use"
return render(request, "static/terms_of_use.html", context) return render(request, "static/terms_of_use.html", context)
def privacy_policy(request): def static_privacy_policy(request):
context = get_base_context(request) context = get_base_context(request)
context["title"] = "enviPath - Privacy Policy" context["title"] = "enviPath - Privacy Policy"
return render(request, "static/privacy_policy.html", context) return render(request, "static/privacy_policy.html", context)
def cookie_policy(request): def static_cookie_policy(request):
context = get_base_context(request) context = get_base_context(request)
context["title"] = "enviPath - Cookie Policy" context["title"] = "enviPath - Cookie Policy"
return render(request, "static/cookie_policy.html", context) return render(request, "static/cookie_policy.html", context)
def about_us(request): def static_about_us(request):
context = get_base_context(request) context = get_base_context(request)
context["title"] = "enviPath - About Us" context["title"] = "enviPath - About Us"
return render(request, "static/about_us.html", context) return render(request, "static/about_us.html", context)
def contact_support(request): def static_contact_support(request):
context = get_base_context(request) context = get_base_context(request)
context["title"] = "enviPath - Contact & Support" context["title"] = "enviPath - Contact & Support"
return render(request, "static/contact.html", context) return render(request, "static/contact.html", context)
def jobs(request): def static_jobs(request):
context = get_base_context(request) context = get_base_context(request)
context["title"] = "enviPath - Jobs & Careers" context["title"] = "enviPath - Jobs & Careers"
return render(request, "static/jobs.html", context) return render(request, "static/jobs.html", context)
def cite(request): def static_cite(request):
context = get_base_context(request) context = get_base_context(request)
context["title"] = "enviPath - How to Cite" context["title"] = "enviPath - How to Cite"
return render(request, "static/cite.html", context) return render(request, "static/cite.html", context)
def legal(request): def static_legal(request):
context = get_base_context(request) context = get_base_context(request)
context["title"] = "enviPath - Legal Information" context["title"] = "enviPath - Legal Information"
return render(request, "static/legal.html", context) return render(request, "static/legal.html", context)

View File

@ -13,7 +13,7 @@ dependencies = [
"django-oauth-toolkit>=3.0.1", "django-oauth-toolkit>=3.0.1",
"django-polymorphic>=4.1.0", "django-polymorphic>=4.1.0",
"django-stubs>=5.2.4", "django-stubs>=5.2.4",
#"enviformer", "enviformer",
"envipy-additional-information", "envipy-additional-information",
"envipy-ambit>=0.1.0", "envipy-ambit>=0.1.0",
"envipy-plugins", "envipy-plugins",
@ -31,9 +31,9 @@ dependencies = [
] ]
[tool.uv.sources] [tool.uv.sources]
#enviformer = { git = "ssh://git@git.envipath.com/enviPath/enviformer.git", rev = "v0.1.2" } enviformer = { git = "ssh://git@git.envipath.com/enviPath/enviformer.git", rev = "v0.1.2" }
envipy-plugins = { git = "ssh://git@git.envipath.com/enviPath/enviPy-plugins.git", rev = "v0.1.0" } envipy-plugins = { git = "ssh://git@git.envipath.com/enviPath/enviPy-plugins.git", rev = "v0.1.0" }
envipy-additional-information = { git = "ssh://git@git.envipath.com/enviPath/enviPy-additional-information.git", rev = "v0.1.4"} envipy-additional-information = { git = "ssh://git@git.envipath.com/enviPath/enviPy-additional-information.git", rev = "v0.1.7"}
envipy-ambit = { git = "ssh://git@git.envipath.com/enviPath/enviPy-ambit.git" } envipy-ambit = { git = "ssh://git@git.envipath.com/enviPath/enviPy-ambit.git" }
[dependency-groups] [dependency-groups]
@ -45,6 +45,8 @@ dev = [
[project.optional-dependencies] [project.optional-dependencies]
ms-login = ["msal>=1.33.0"] ms-login = ["msal>=1.33.0"]
dev = [ dev = [
"celery-stubs==0.1.3",
"django-stubs>=5.2.4",
"poethepoet>=0.37.0", "poethepoet>=0.37.0",
"pre-commit>=4.3.0", "pre-commit>=4.3.0",
"ruff>=0.13.3", "ruff>=0.13.3",

Binary file not shown.

After

Width:  |  Height:  |  Size: 114 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 114 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 53 KiB

After

Width:  |  Height:  |  Size: 53 KiB

View File

@ -646,8 +646,8 @@ function handleAssessmentResponse(depict_url, data) {
var reactivityCentersImgSrc = null; var reactivityCentersImgSrc = null;
if (data['assessment']['node'] !== undefined) { if (data['assessment']['node'] !== undefined) {
functionalGroupsImgSrc = "<img width='400' src='" + data['assessment']['node']['image'] + "'>"; functionalGroupsImgSrc = "<img width='400' src='" + data['assessment']['node']['image'] + "&highlight=true'>";
reactivityCentersImgSrc = "<img width='400' src='" + data['assessment']['node']['image'] + "'>" reactivityCentersImgSrc = "<img width='400' src='" + data['assessment']['node']['image'] + "&highlightReactivity=true'>"
} else { } else {
functionalGroupsImgSrc = "<img width='400' src=\"" + depict_url + "?smiles=" + encodeURIComponent(data['assessment']['smiles']) + "\">"; functionalGroupsImgSrc = "<img width='400' src=\"" + depict_url + "?smiles=" + encodeURIComponent(data['assessment']['smiles']) + "\">";
reactivityCentersImgSrc = "<img width='400' src=\"" + depict_url + "?smiles=" + encodeURIComponent(data['assessment']['smiles']) + "\">" reactivityCentersImgSrc = "<img width='400' src=\"" + depict_url + "?smiles=" + encodeURIComponent(data['assessment']['smiles']) + "\">"

View File

@ -444,6 +444,13 @@ function serializeSVG(svgElement) {
line.setAttribute("fill", style.fill); line.setAttribute("fill", style.fill);
}); });
svgElement.querySelectorAll("line.link_no_arrow").forEach(line => {
const style = getComputedStyle(line);
line.setAttribute("stroke", style.stroke);
line.setAttribute("stroke-width", style.strokeWidth);
line.setAttribute("fill", style.fill);
});
const serializer = new XMLSerializer(); const serializer = new XMLSerializer();
let svgString = serializer.serializeToString(svgElement); let svgString = serializer.serializeToString(svgElement);
@ -455,7 +462,26 @@ function serializeSVG(svgElement) {
return svgString; return svgString;
} }
function shrinkSVG(svgSelector) {
const svg = d3.select(svgSelector);
const node = svg.node();
// Compute bounding box of everything inside the SVG
const bbox = node.getBBox();
const padding = 10;
svg.attr("viewBox",
`${bbox.x - padding} ${bbox.y - padding} ${bbox.width + 2 * padding} ${bbox.height + 2 * padding}`
)
.attr("width", bbox.width + 2 * padding)
.attr("height", bbox.height + 2 * padding);
return bbox;
}
function downloadSVG(svgElement, filename = 'chart.svg') { function downloadSVG(svgElement, filename = 'chart.svg') {
shrinkSVG("#" + svgElement.id);
const svgString = serializeSVG(svgElement); const svgString = serializeSVG(svgElement);
const blob = new Blob([svgString], {type: 'image/svg+xml;charset=utf-8'}); const blob = new Blob([svgString], {type: 'image/svg+xml;charset=utf-8'});
const url = URL.createObjectURL(blob); const url = URL.createObjectURL(blob);

View File

@ -0,0 +1,71 @@
{% extends "framework.html" %}
{% load static %}
{% load envipytags %}
{% block content %}
<div class="panel-group" id="reviewListAccordion">
<div class="panel panel-default">
<div class="panel-heading" id="headingPanel" style="font-size:2rem;height: 46px">
Jobs
</div>
<div class="panel-body">
<p>
Job Logs Desc
</p>
</div>
<div class="panel panel-default panel-heading list-group-item" style="background-color:silver">
<h4 class="panel-title">
<a id="job-accordion-link" data-toggle="collapse" data-parent="#job-accordion" href="#jobs">
Jobs
</a>
</h4>
</div>
<div id="jobs"
class="panel-collapse collapse in">
<div class="panel-body list-group-item" id="job-content">
<table class="table table-bordered table-hover">
<tr style="background-color: rgba(0, 0, 0, 0.08);">
<th scope="col">ID</th>
<th scope="col">Name</th>
<th scope="col">Status</th>
<th scope="col">Queued</th>
<th scope="col">Done</th>
<th scope="col">Result</th>
</tr>
<tbody>
{% for job in jobs %}
<tr>
<td>{{ job.task_id }}</td>
<td>{{ job.job_name }}</td>
<td>{{ job.status }}</td>
<td>{{ job.created }}</td>
<td>{{ job.done_at }}</td>
{% if job.task_result and job.task_result|is_url == True %}
<td><a href="{{ job.task_result }}">Result</a></td>
{% elif job.task_result %}
<td>{{ job.task_result|slice:"40" }}...</td>
{% else %}
<td>Empty</td>
{% endif %}
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
<!-- Unreviewable objects such as User / Group / Setting -->
<ul class='list-group'>
{% for obj in objects %}
{% if object_type == 'user' %}
<a class="list-group-item" href="{{ obj.url }}">{{ obj.username }}</a>
{% else %}
<a class="list-group-item" href="{{ obj.url }}">{{ obj.name }}</a>
{% endif %}
{% endfor %}
</ul>
</div>
</div>
{% endblock content %}

View File

@ -242,19 +242,21 @@
<div class="row"> <div class="row">
<div class="col-lg-12"> <div class="col-lg-12">
<ul class="nav nav-pills nav-justified"> <ul class="nav nav-pills nav-justified">
<li><a href="http://eawag.ch" target="_blank"> <li>
<img id="image-ealogo" <a href="http://ml.auckland.ac.nz" target="_blank">
height="60" <img id="image-uoalogo" height="60" src='{% static "/images/UoA-Logo-Primary-RGB-Small.png" %}'
src='{% static "/images/ealogo.gif" %}' alt="The Univserity of Auckland"/>
alt="Eawag"/>
</a> </a>
</li> </li>
<li> <li>
<a href="http://ml.auckland.ac.nz" target="_blank"> <a href="https://eawag.ch" target="_blank">
<img id="image-uoalogo" <img id="image-ealogo" height="60" src='{% static "/images/ealogo.gif" %}' alt="Eawag"/>
height="60" </a>
src='{% static "/images/uoa.png" %}' </li>
alt="The Univserity of Auckland"/> <li>
<a href="https://www.uzh.ch/" target="_blank">
<img id="image-ufzlogo" height="60" src='{% static "/images/uzh-logo.svg" %}'
alt="University of Zurich"/>
</a> </a>
</li> </li>
</ul> </ul>

View File

@ -18,13 +18,19 @@
prediction. You just need to set a name and the packages prediction. You just need to set a name and the packages
you want the object to be based on. There are multiple types of models available. you want the object to be based on. There are multiple types of models available.
For additional information have a look at our For additional information have a look at our
<a target="_blank" href="https://wiki.envipath.org/index.php/relative-reasoning" role="button">wiki &gt;&gt;</a> <a target="_blank" href="https://wiki.envipath.org/index.php/relative-reasoning" role="button">wiki
&gt;&gt;</a>
</div> </div>
<!-- Name -->
<label for="model-name">Name</label> <label for="model-name">Name</label>
<input id="model-name" name="model-name" class="form-control" placeholder="Name"/> <input id="model-name" name="model-name" class="form-control" placeholder="Name"/>
<!-- Description -->
<label for="model-description">Description</label> <label for="model-description">Description</label>
<input id="model-description" name="model-description" class="form-control" <input id="model-description" name="model-description" class="form-control"
placeholder="Description"/> placeholder="Description"/>
<!-- Model Type -->
<label for="model-type">Model Type</label> <label for="model-type">Model Type</label>
<select id="model-type" name="model-type" class="form-control" data-width='100%'> <select id="model-type" name="model-type" class="form-control" data-width='100%'>
<option disabled selected>Select Model Type</option> <option disabled selected>Select Model Type</option>
@ -32,12 +38,12 @@
<option value="{{ v }}">{{ k }}</option> <option value="{{ v }}">{{ k }}</option>
{% endfor %} {% endfor %}
</select> </select>
<!-- ML and Rule Based Based Form-->
<div id="package-based-relative-reasoning-specific-form">
<!-- Rule Packages --> <!-- Rule Packages -->
<label for="package-based-relative-reasoning-rule-packages">Rule Packages</label> <div id="rule-packages" class="ep-model-param mlrr rbrr">
<select id="package-based-relative-reasoning-rule-packages" name="package-based-relative-reasoning-rule-packages" <label for="model-rule-packages">Rule Packages</label>
data-actions-box='true' class="form-control" multiple data-width='100%'> <select id="model-rule-packages" name="model-rule-packages" data-actions-box='true'
class="form-control" multiple data-width='100%'>
<option disabled>Reviewed Packages</option> <option disabled>Reviewed Packages</option>
{% for obj in meta.readable_packages %} {% for obj in meta.readable_packages %}
{% if obj.reviewed %} {% if obj.reviewed %}
@ -52,10 +58,13 @@
{% endif %} {% endif %}
{% endfor %} {% endfor %}
</select> </select>
</div>
<!-- Data Packages --> <!-- Data Packages -->
<label for="package-based-relative-reasoning-data-packages" >Data Packages</label> <div id="data-packages" class="ep-model-param mlrr rbrr enviformer">
<select id="package-based-relative-reasoning-data-packages" name="package-based-relative-reasoning-data-packages" <label for="model-data-packages">Data Packages</label>
data-actions-box='true' class="form-control" multiple data-width='100%'> <select id="model-data-packages" name="model-data-packages" data-actions-box='true'
class="form-control" multiple data-width='100%'>
<option disabled>Reviewed Packages</option> <option disabled>Reviewed Packages</option>
{% for obj in meta.readable_packages %} {% for obj in meta.readable_packages %}
{% if obj.reviewed %} {% if obj.reviewed %}
@ -70,32 +79,31 @@
{% endif %} {% endif %}
{% endfor %} {% endfor %}
</select> </select>
</div>
<div id="ml-relative-reasoning-specific-form">
<!-- Fingerprinter --> <!-- Fingerprinter -->
<label for="ml-relative-reasoning-fingerprinter">Fingerprinter</label> <div id="fingerprinter" class="ep-model-param mlrr">
<select id="ml-relative-reasoning-fingerprinter" name="ml-relative-reasoning-fingerprinter" <label for="model-fingerprinter">Fingerprinter</label>
class="form-control"> <select id="model-fingerprinter" name="model-fingerprinter" data-actions-box='true'
class="form-control" multiple data-width='100%'>
<option value="MACCS" selected>MACCS Fingerprinter</option> <option value="MACCS" selected>MACCS Fingerprinter</option>
</select>
{% if meta.enabled_features.PLUGINS and additional_descriptors %} {% if meta.enabled_features.PLUGINS and additional_descriptors %}
<!-- Property Plugins go here -->
<label for="ml-relative-reasoning-additional-fingerprinter">Additional Fingerprinter /
Descriptors</label>
<select id="ml-relative-reasoning-additional-fingerprinter"
name="ml-relative-reasoning-additional-fingerprinter" class="form-control">
<option disabled selected>Select Additional Fingerprinter / Descriptor</option> <option disabled selected>Select Additional Fingerprinter / Descriptor</option>
{% for k, v in additional_descriptors.items %} {% for k, v in additional_descriptors.items %}
<option value="{{ v }}">{{ k }}</option> <option value="{{ v }}">{{ k }}</option>
{% endfor %} {% endfor %}
</select>
{% endif %} {% endif %}
</select>
<label for="ml-relative-reasoning-threshold">Threshold</label>
<input type="number" min="0" max="1" step="0.05" value="0.5"
id="ml-relative-reasoning-threshold"
name="ml-relative-reasoning-threshold" class="form-control">
</div> </div>
<!-- Threshold -->
<div id="threshold" class="ep-model-param mlrr enviformer">
<label for="model-threshold">Threshold</label>
<input type="number" min="0" max="1" step="0.05" value="0.5" id="model-threshold"
name="model-threshold" class="form-control">
</div>
<div id="appdomain" class="ep-model-param mlrr">
{% if meta.enabled_features.APPLICABILITY_DOMAIN %} {% if meta.enabled_features.APPLICABILITY_DOMAIN %}
<!-- Build AD? --> <!-- Build AD? -->
<div class="checkbox"> <div class="checkbox">
@ -107,11 +115,13 @@
<div id="ad-params" style="display:none"> <div id="ad-params" style="display:none">
<!-- Num Neighbors --> <!-- Num Neighbors -->
<label for="num-neighbors">Number of Neighbors</label> <label for="num-neighbors">Number of Neighbors</label>
<input id="num-neighbors" name="num-neighbors" type="number" class="form-control" value="5" <input id="num-neighbors" name="num-neighbors" type="number" class="form-control"
value="5"
step="1" min="0" max="10"> step="1" min="0" max="10">
<!-- Local Compatibility --> <!-- Local Compatibility -->
<label for="local-compatibility-threshold">Local Compatibility Threshold</label> <label for="local-compatibility-threshold">Local Compatibility Threshold</label>
<input id="local-compatibility-threshold" name="local-compatibility-threshold" type="number" <input id="local-compatibility-threshold" name="local-compatibility-threshold"
type="number"
class="form-control" value="0.5" step="0.01" min="0" max="1"> class="form-control" value="0.5" step="0.01" min="0" max="1">
<!-- Reliability --> <!-- Reliability -->
<label for="reliability-threshold">Reliability Threshold</label> <label for="reliability-threshold">Reliability Threshold</label>
@ -120,12 +130,6 @@
</div> </div>
{% endif %} {% endif %}
</div> </div>
<!-- EnviFormer-->
<div id="enviformer-specific-form">
<label for="enviformer-threshold">Threshold</label>
<input type="number" min="0" max="1" step="0.05" value="0.5" id="enviformer-threshold"
name="enviformer-threshold" class="form-control">
</div>
</form> </form>
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
@ -138,19 +142,22 @@
<script> <script>
$(function () { $(function () {
// Built in Model Types
var nativeModelTypes = [
"mlrr",
"rbrr",
"enviformer",
]
// Initially hide all "specific" forms // Initially hide all "specific" forms
$("div[id$='-specific-form']").each( function() { $(".ep-model-param").each(function () {
$(this).hide(); $(this).hide();
}); });
$('#model-type').selectpicker(); $('#model-type').selectpicker();
$("#ml-relative-reasoning-fingerprinter").selectpicker(); $("#model-fingerprinter").selectpicker();
$("#package-based-relative-reasoning-rule-packages").selectpicker(); $("#model-rule-packages").selectpicker();
$("#package-based-relative-reasoning-data-packages").selectpicker(); $("#model-data-packages").selectpicker();
$("#package-based-relative-reasoning-evaluation-packages").selectpicker();
if ($('#ml-relative-reasoning-additional-fingerprinter').length > 0) {
$("#ml-relative-reasoning-additional-fingerprinter").selectpicker();
}
$("#build-app-domain").change(function () { $("#build-app-domain").change(function () {
if ($(this).is(":checked")) { if ($(this).is(":checked")) {
@ -162,18 +169,12 @@ $(function() {
// On change hide all and show only selected // On change hide all and show only selected
$("#model-type").change(function () { $("#model-type").change(function () {
$("div[id$='-specific-form']").each( function() { $('.ep-model-param').hide();
$(this).hide(); var modelType = $('#model-type').val();
}); if (nativeModelTypes.indexOf(modelType) !== -1) {
val = $('option:selected', this).val(); $('.' + modelType).show();
if (val === 'ml-relative-reasoning' || val === 'rule-based-relative-reasoning') {
$("#package-based-relative-reasoning-specific-form").show();
if (val === 'ml-relative-reasoning') {
$("#ml-relative-reasoning-specific-form").show();
}
} else { } else {
$("#" + val + "-specific-form").show(); // do nothing
} }
}); });
@ -183,7 +184,4 @@ $(function() {
}); });
}); });
</script> </script>

View File

@ -17,10 +17,10 @@
For evaluation, you need to select the packages you want to use. For evaluation, you need to select the packages you want to use.
While the model is evaluating, you can use the model for predictions. While the model is evaluating, you can use the model for predictions.
</div> </div>
<!-- Evaluation --> <!-- Evaluation Packages -->
<label for="relative-reasoning-evaluation-packages">Evaluation Packages</label> <label for="model-evaluation-packages">Evaluation Packages</label>
<select id="relative-reasoning-evaluation-packages" name=relative-reasoning-evaluation-packages" <select id="model-evaluation-packages" name="model-evaluation-packages" data-actions-box='true'
data-actions-box='true' class="form-control" multiple data-width='100%'> class="form-control" multiple data-width='100%'>
<option disabled>Reviewed Packages</option> <option disabled>Reviewed Packages</option>
{% for obj in meta.readable_packages %} {% for obj in meta.readable_packages %}
{% if obj.reviewed %} {% if obj.reviewed %}
@ -35,6 +35,15 @@
{% endif %} {% endif %}
{% endfor %} {% endfor %}
</select> </select>
<!-- Eval Type -->
<label for="model-evaluation-type">Evaluation Type</label>
<select id="model-evaluation-type" name="model-evaluation-type" class="form-control">
<option disabled selected>Select evaluation type</option>
<option value="sg">Single Generation</option>
<option value="mg">Multiple Generations</option>
</select>
<input type="hidden" name="hidden" value="evaluate"> <input type="hidden" name="hidden" value="evaluate">
</form> </form>
</div> </div>
@ -50,7 +59,7 @@
$(function () { $(function () {
$("#relative-reasoning-evaluation-packages").selectpicker(); $("#model-evaluation-packages").selectpicker();
$('#evaluate_model_form_submit').on('click', function (e) { $('#evaluate_model_form_submit').on('click', function (e) {
e.preventDefault(); e.preventDefault();

View File

@ -18,6 +18,7 @@
<select id="scenario-select" name="selected-scenarios" data-actions-box='true' class="form-control" <select id="scenario-select" name="selected-scenarios" data-actions-box='true' class="form-control"
multiple data-width='100%'> multiple data-width='100%'>
<option disabled>Select Scenarios</option> <option disabled>Select Scenarios</option>
<option value="" hidden></option>
</select> </select>
</form> </form>
</div> </div>
@ -65,7 +66,7 @@
$('#set_scenario_modal_form_submit').on('click', function (e) { $('#set_scenario_modal_form_submit').on('click', function (e) {
e.preventDefault(); e.preventDefault();
if ($('#scenario-select').val().length == 0) { if ($('#scenario-select').val().length == 0) {
$('#scenario-select').val(['']) $('#scenario-select').val("")
} }
$('#set_scenario_modal_form').submit(); $('#set_scenario_modal_form').submit();
}); });

View File

@ -29,7 +29,7 @@
</div> </div>
<div class="panel-body"> <div class="panel-body">
<p> <p>
{{ rule.description }} {{ rule.description|safe }}
</p> </p>
</div> </div>
@ -87,6 +87,7 @@
</div> </div>
{% endif %} {% endif %}
{% if rule.enzymelinks %}
<!-- EC Numbers --> <!-- EC Numbers -->
<div class="panel panel-default panel-heading list-group-item" style="background-color:silver"> <div class="panel panel-default panel-heading list-group-item" style="background-color:silver">
<h4 class="panel-title"> <h4 class="panel-title">
@ -94,12 +95,33 @@
href="#rule-ec-numbers">EC Numbers</a> href="#rule-ec-numbers">EC Numbers</a>
</h4> </h4>
</div> </div>
<div id="rule-ec-numbers" class="panel-collapse collapse"> <div id="rule-ec-numbers" class="panel-collapse collapse in">
<div class="panel-body list-group-item"> <div class="panel-body list-group-item">
{% for k, v in rule.get_grouped_enzymelinks.items %}
<div class="panel panel-default panel-heading list-group-item" style="background-color:silver">
<h4 class="panel-title">
<a id="{{ k|slugify }}_Link" data-toggle="collapse"
data-parent="#{{ k|slugify }}_Accordion"
href="#{{ k|slugify }}">
{{ k }}
</a>
</h4>
</div>
<div id="{{ k|slugify }}" class="panel-collapse collapse in">
<div class="panel-body list-group-item">
{% for enzyme in v %}
<a class="list-group-item" href="{{ enzyme.url }}">
{{ enzyme.ec_number }}
<div style="position:absolute;bottom:10px;left:100px;">{{ enzyme.name }}</div>
<div style="float:right;">{{ enzyme.linking_method }}</div>
</a>
{% endfor %}
</div> </div>
</div> </div>
{% endfor %}
</div>
</div>
{% endif %}
</div> </div>
</div> </div>
{% endblock content %} {% endblock content %}

View File

@ -183,7 +183,7 @@
</div> </div>
<div id="compound-external-identifier" class="panel-collapse collapse in"> <div id="compound-external-identifier" class="panel-collapse collapse in">
<div class="panel-body list-group-item"> <div class="panel-body list-group-item">
{% if compound.get_pubchem_identifiers %} {% if compound.get_pubchem_compound_identifiers %}
<div class="panel panel-default panel-heading list-group-item" <div class="panel panel-default panel-heading list-group-item"
style="background-color:silver"> style="background-color:silver">
<h4 class="panel-title"> <h4 class="panel-title">
@ -193,12 +193,28 @@
</h4> </h4>
</div> </div>
<div id="compound-pubchem-identifier" class="panel-collapse collapse in"> <div id="compound-pubchem-identifier" class="panel-collapse collapse in">
{% for eid in compound.get_pubchem_identifiers %} {% for eid in compound.get_pubchem_compound_identifiers %}
<a class="list-group-item" <a class="list-group-item"
href="{{ eid.external_url }}">CID{{ eid.identifier_value }}</a> href="{{ eid.external_url }}">CID{{ eid.identifier_value }}</a>
{% endfor %} {% endfor %}
</div> </div>
{% endif %} {% endif %}
{% if compound.get_pubchem_substance_identifiers %}
<div class="panel panel-default panel-heading list-group-item"
style="background-color:silver">
<h4 class="panel-title">
<a id="compound-pubchem-identifier-link" data-toggle="collapse"
data-parent="#compound-external-identifier"
href="#compound-pubchem-identifier">PubChem Substance Identifier</a>
</h4>
</div>
<div id="compound-pubchem-identifier" class="panel-collapse collapse in">
{% for eid in compound.get_pubchem_substance_identifiers %}
<a class="list-group-item"
href="{{ eid.external_url }}">SID{{ eid.identifier_value }}</a>
{% endfor %}
</div>
{% endif %}
{% if compound.get_chebi_identifiers %} {% if compound.get_chebi_identifiers %}
<div class="panel panel-default panel-heading list-group-item" <div class="panel panel-default panel-heading list-group-item"
style="background-color:silver"> style="background-color:silver">

View File

@ -0,0 +1,105 @@
{% extends "framework.html" %}
{% block content %}
<div class="panel-group" id="enzyme-detail">
<div class="panel panel-default">
<div class="panel-heading" id="headingPanel" style="font-size:2rem;height: 46px">
{{ enzymelink.ec_number }}
</div>
<!-- Name -->
<div class="panel panel-default panel-heading list-group-item" style="background-color:silver">
<h4 class="panel-title">
<a id="enzyme-name-link" data-toggle="collapse" data-parent="#enzyme-detail"
href="#enzyme-name">Enzyme Name</a>
</h4>
</div>
<div id="enzyme-name" class="panel-collapse collapse in">
<div class="panel-body list-group-item">
{{ enzymelink.name }}
</div>
</div>
<!-- Linking Method -->
<div class="panel panel-default panel-heading list-group-item" style="background-color:silver">
<h4 class="panel-title">
<a id="enzyme-linking-link" data-toggle="collapse" data-parent="#enzyme-detail"
href="#enzyme-linking">Linking Method</a>
</h4>
</div>
<div id="enzyme-linking" class="panel-collapse collapse in">
<div class="panel-body list-group-item">
{{ enzymelink.linking_method }}. &nbsp;<a
href="https://wiki.envipath.org/index.php/Rules#EnzymeLinks" target="#">Learn more &gt;&gt;</a>
</div>
</div>
{% if enzymelink.kegg_reaction_links %}
<div class="panel panel-default panel-heading list-group-item" style="background-color:silver">
<h4 class="panel-title">
<a id="enzyme-evidence-link" data-toggle="collapse" data-parent="#enzyme-detail"
href="#enzyme-evidence">Linking Evidence</a>
</h4>
</div>
<div id="enzyme-evidence" class="panel-collapse collapse in">
<div class="panel-body list-group-item">
{% for kl in enzymelink.kegg_reaction_links %}
<a class="list-group-item"
href="{{ kl.external_url }}">{{ kl.identifier_value }}</a>
{% endfor %}
</div>
</div>
{% endif %}
{% if enzymelink.reaction_evidence.all %}
<div class="panel panel-default panel-heading list-group-item" style="background-color:silver">
<h4 class="panel-title">
<a id="enzyme-reaction-evidence-link" data-toggle="collapse" data-parent="#enzyme-detail"
href="#enzyme-reaction-evidence">Linking Evidence - enviPath Reactions</a>
</h4>
</div>
<div id="enzyme-reaction-evidence" class="panel-collapse collapse in">
<div class="panel-body list-group-item">
{% for r in enzymelink.reaction_evidence.all %}
<a class="list-group-item" href="{{ r.url }}">{{ r.name }} <i>({{ r.package.name }})</i></a>
{% endfor %}
</div>
</div>
{% endif %}
{% if enzymelink.edge_evidence.all %}
<div class="panel panel-default panel-heading list-group-item" style="background-color:silver">
<h4 class="panel-title">
<a id="enzyme-edge-evidence-link" data-toggle="collapse" data-parent="#enzyme-detail"
href="#enzyme-edge-evidence">Linking Evidence - enviPath Pathways</a>
</h4>
</div>
<div id="enzyme-edge-evidence" class="panel-collapse collapse in">
<div class="panel-body list-group-item">
{% for e in enzymelink.edge_evidence.all %}
<a class="list-group-item" href="{{ e.pathway.url }}">{{ e.pathway.name }}
<i>({{ r.package.name }})</i></a>
{% endfor %}
</div>
</div>
{% endif %}
<!-- External DB Reference -->
<div class="panel panel-default panel-heading list-group-item" style="background-color:silver">
<h4 class="panel-title">
<a id="enzyme-external-identifier-link" data-toggle="collapse" data-parent="#enzyme-detail"
href="#enzyme-external-identifier">External DB References</a>
</h4>
</div>
<div id="enzyme-external-identifier" class="panel-collapse collapse in">
<div class="panel-body list-group-item">
<a class="list-group-item"
href="http://www.brenda-enzymes.org/enzyme.php?ecno={{ enzymelink.ec_number }}"
target="_blank"> Brenda entry for {{ enzymelink.ec_number }}</a>
</div>
</div>
</div>
</div>
{% endblock content %}

View File

@ -117,7 +117,7 @@
<!-- End Predict Panel --> <!-- End Predict Panel -->
{% endif %} {% endif %}
{% if model.app_domain %} {% if model.ready_for_prediction and model.app_domain %}
<!-- App Domain --> <!-- App Domain -->
<div class="panel panel-default panel-heading list-group-item" style="background-color:silver"> <div class="panel panel-default panel-heading list-group-item" style="background-color:silver">
<h4 class="panel-title"> <h4 class="panel-title">

View File

@ -177,9 +177,6 @@
</nav> </nav>
<div id="vizdiv" > <div id="vizdiv" >
<svg id="pwsvg"> <svg id="pwsvg">
{% if debug %}
<rect width="100%" height="100%" fill="aliceblue"/>
{% endif %}
<defs> <defs>
<marker id="arrow" viewBox="0 0 10 10" refX="43" refY="5" markerWidth="6" markerHeight="6" <marker id="arrow" viewBox="0 0 10 10" refX="43" refY="5" markerWidth="6" markerHeight="6"
orient="auto-start-reverse" markerUnits="userSpaceOnUse"> orient="auto-start-reverse" markerUnits="userSpaceOnUse">

View File

@ -124,6 +124,23 @@
</div> </div>
{% endif %} {% endif %}
{% if reaction.get_related_enzymes %}
<!-- EC Numbers -->
<div class="panel panel-default panel-heading list-group-item" style="background-color:silver">
<h4 class="panel-title">
<a id="rule-ec-numbers-link" data-toggle="collapse" data-parent="#rule-detail"
href="#rule-ec-numbers">EC Numbers</a>
</h4>
</div>
<div id="rule-ec-numbers" class="panel-collapse collapse in">
<div class="panel-body list-group-item">
{% for e in reaction.get_related_enzymes %}
<a class="list-group-item" href="http://www.brenda-enzymes.org/enzyme.php?ecno={{ e.ec_number }}">{{ e.name }}</a>
{% endfor %}
</div>
</div>
{% endif %}
{% if reaction.related_pathways %} {% if reaction.related_pathways %}
<!-- Pathways --> <!-- Pathways -->
<div class="panel panel-default panel-heading list-group-item" style="background-color:silver"> <div class="panel panel-default panel-heading list-group-item" style="background-color:silver">

View File

@ -201,6 +201,43 @@
</div> </div>
</div> </div>
{% endif %} {% endif %}
{% if rule.enzymelinks %}
<!-- EC Numbers -->
<div class="panel panel-default panel-heading list-group-item" style="background-color:silver">
<h4 class="panel-title">
<a id="rule-ec-numbers-link" data-toggle="collapse" data-parent="#rule-detail"
href="#rule-ec-numbers">EC Numbers</a>
</h4>
</div>
<div id="rule-ec-numbers" class="panel-collapse collapse in">
<div class="panel-body list-group-item">
{% for k, v in rule.get_grouped_enzymelinks.items %}
<div class="panel panel-default panel-heading list-group-item"
style="background-color:silver">
<h4 class="panel-title">
<a id="{{ k|slugify }}_Link" data-toggle="collapse"
data-parent="#{{ k|slugify }}_Accordion"
href="#{{ k|slugify }}">
{{ k }}
</a>
</h4>
</div>
<div id="{{ k|slugify }}" class="panel-collapse collapse in">
<div class="panel-body list-group-item">
{% for enzyme in v %}
<a class="list-group-item" href="{{ enzyme.url }}">
{{ enzyme.ec_number }}
<div style="position:absolute;bottom:10px;left:100px;">{{ enzyme.name }}</div>
<div style="float:right;">{{ enzyme.linking_method }}</div>
</a>
{% endfor %}
</div>
</div>
{% endfor %}
</div>
</div>
{% endif %}
</div> </div>
</div> </div>
{% endblock content %} {% endblock content %}

View File

@ -1,7 +1,27 @@
from collections import defaultdict
from datetime import datetime
from tempfile import TemporaryDirectory from tempfile import TemporaryDirectory
from django.test import TestCase, tag from django.test import TestCase, tag
from epdb.logic import PackageManager from epdb.logic import PackageManager
from epdb.models import User, EnviFormer, Package from epdb.models import User, EnviFormer, Package, Setting
from epdb.tasks import predict_simple, predict
def measure_predict(mod, pathway_pk=None):
# Measure and return the prediction time
start = datetime.now()
if pathway_pk:
s = Setting()
s.model = mod
s.model_threshold = 0.2
s.max_depth = 4
s.max_nodes = 20
s.save()
pred_result = predict.delay(pathway_pk, s.pk, limit=s.max_depth)
else:
pred_result = predict_simple.delay(mod.pk, "C1=CC=C(CSCC2=CC=CC=C2)C=C1")
_ = pred_result.get()
return round((datetime.now() - start).total_seconds(), 2)
@tag("slow") @tag("slow")
@ -28,8 +48,41 @@ class EnviFormerTest(TestCase):
mod.build_dataset() mod.build_dataset()
mod.build_model() mod.build_model()
mod.multigen_eval = True mod.evaluate_model(True, eval_packages_objs)
mod.save()
mod.evaluate_model()
mod.predict("CCN(CC)C(=O)C1=CC(=CC=C1)C") mod.predict("CCN(CC)C(=O)C1=CC(=CC=C1)C")
def test_predict_runtime(self):
with TemporaryDirectory() as tmpdir:
with self.settings(MODEL_DIR=tmpdir):
threshold = float(0.5)
data_package_objs = [self.BBD_SUBSET]
eval_packages_objs = [self.BBD_SUBSET]
mods = []
for _ in range(4):
mod = EnviFormer.create(
self.package, data_package_objs, eval_packages_objs, threshold=threshold
)
mod.build_dataset()
mod.build_model()
mods.append(mod)
# Test prediction time drops after first prediction
times = [measure_predict(mods[0]) for _ in range(5)]
print(f"First prediction took {times[0]} seconds, subsequent ones took {times[1:]}")
# Test pathway prediction
times = [measure_predict(mods[1], self.BBD_SUBSET.pathways[0].pk) for _ in range(5)]
print(
f"First pathway prediction took {times[0]} seconds, subsequent ones took {times[1:]}"
)
# Test eviction by performing three prediction with every model, twice.
times = defaultdict(list)
for _ in range(
2
): # Eviction should cause the second iteration here to have to reload the models
for mod in mods:
for _ in range(3):
times[mod.pk].append(measure_predict(mod))
print(times)

View File

@ -30,7 +30,6 @@ class ModelTest(TestCase):
self.package, self.package,
rule_package_objs, rule_package_objs,
data_package_objs, data_package_objs,
eval_packages_objs,
threshold=threshold, threshold=threshold,
name="ECC - BBD - 0.5", name="ECC - BBD - 0.5",
description="Created MLRelativeReasoning in Testcase", description="Created MLRelativeReasoning in Testcase",
@ -50,9 +49,7 @@ class ModelTest(TestCase):
mod.build_dataset() mod.build_dataset()
mod.build_model() mod.build_model()
mod.multigen_eval = True mod.evaluate_model(True, eval_packages_objs)
mod.save()
mod.evaluate_model()
results = mod.predict("CCN(CC)C(=O)C1=CC(=CC=C1)C") results = mod.predict("CCN(CC)C(=O)C1=CC(=CC=C1)C")

View File

@ -6,7 +6,7 @@ from epdb.logic import UserManager
from epdb.models import Package, User from epdb.models import Package, User
@override_settings(MODEL_DIR=s.FIXTURE_DIRS[0] / "models") @override_settings(MODEL_DIR=s.FIXTURE_DIRS[0] / "models", CELERY_TASK_ALWAYS_EAGER=True)
class PathwayViewTest(TestCase): class PathwayViewTest(TestCase):
fixtures = ["test_fixtures_incl_model.jsonl.gz"] fixtures = ["test_fixtures_incl_model.jsonl.gz"]

View File

@ -6,7 +6,7 @@ from epdb.logic import UserManager, PackageManager
from epdb.models import Pathway, Edge from epdb.models import Pathway, Edge
@override_settings(MODEL_DIR=s.FIXTURE_DIRS[0] / "models") @override_settings(MODEL_DIR=s.FIXTURE_DIRS[0] / "models", CELERY_TASK_ALWAYS_EAGER=True)
class PathwayViewTest(TestCase): class PathwayViewTest(TestCase):
fixtures = ["test_fixtures_incl_model.jsonl.gz"] fixtures = ["test_fixtures_incl_model.jsonl.gz"]

View File

@ -729,6 +729,7 @@ class IndigoUtils(object):
height: int = 0, height: int = 0,
educt_functional_groups: Dict[str, int] = None, educt_functional_groups: Dict[str, int] = None,
product_functional_groups: Dict[str, int] = None, product_functional_groups: Dict[str, int] = None,
debug: bool = False,
): ):
if educt_functional_groups is None: if educt_functional_groups is None:
educt_functional_groups = {} educt_functional_groups = {}
@ -739,6 +740,11 @@ class IndigoUtils(object):
i = Indigo() i = Indigo()
renderer = IndigoRenderer(i) renderer = IndigoRenderer(i)
if debug:
i.setOption("render-atom-ids-visible", True)
i.setOption("render-bond-ids-visible", False)
i.setOption("render-atom-bond-ids-from-one", True)
i.setOption("render-output-format", "svg") i.setOption("render-output-format", "svg")
i.setOption("render-coloring", True) i.setOption("render-coloring", True)
i.setOption("render-image-size", width, height) i.setOption("render-image-size", width, height)