forked from enviPath/enviPy
Compare commits
10 Commits
9bf65d4319
...
develop
| Author | SHA1 | Date | |
|---|---|---|---|
| fedd1b5280 | |||
| 2d3dca6a75 | |||
| ada270aa3c | |||
| 093daa5ecf | |||
| f4f284925a | |||
| ca6e926b30 | |||
| 2504d7045b | |||
| 032ebc30a2 | |||
| 703f377b7f | |||
| 7639b23e4e |
62
.gitea/workflows/build-image.yaml
Normal file
62
.gitea/workflows/build-image.yaml
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
name: Build Docker Image
|
||||||
|
|
||||||
|
# Trigger when a PR to main/develop is completed.
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
- develop
|
||||||
|
types:
|
||||||
|
- closed
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-and-push:
|
||||||
|
if: ${{ github.event.pull_request.merged == true }}
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
# Fetch the repository content for the Docker build context.
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
# Enable Buildx for BuildKit features (incl. SSH mount support).
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
|
# Authenticate against the container registry before pushing images.
|
||||||
|
- name: Log in to container registry
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
registry: git.envipath.com
|
||||||
|
username: ${{ secrets.CI_REGISTRY_USER }}
|
||||||
|
password: ${{ secrets.CI_REGISTRY_PASSWORD }}
|
||||||
|
|
||||||
|
# Generate image tags/labels:
|
||||||
|
# - PRs targeting main get "latest" and "main-sha"
|
||||||
|
# - PRs targeting develop get "dev" and "dev-sha"
|
||||||
|
- name: Extract metadata
|
||||||
|
id: meta
|
||||||
|
uses: docker/metadata-action@v5
|
||||||
|
with:
|
||||||
|
images: git.envipath.com/envipath/envipy
|
||||||
|
tags: |
|
||||||
|
type=raw,value=latest,enable=${{ github.event.pull_request.base.ref == 'main' }}
|
||||||
|
type=sha,prefix=main-,enable=${{ github.event.pull_request.base.ref == 'main' }}
|
||||||
|
type=raw,value=dev,enable=${{ github.event.pull_request.base.ref == 'develop' }}
|
||||||
|
type=sha,prefix=dev-,enable=${{ github.event.pull_request.base.ref == 'develop' }}
|
||||||
|
|
||||||
|
# Load SSH key so Docker can pull private git+ssh dependencies during build.
|
||||||
|
- name: Setup SSH for private git dependencies
|
||||||
|
uses: webfactory/ssh-agent@v0.9.0
|
||||||
|
with:
|
||||||
|
ssh-private-key: ${{ secrets.ENVIPY_CI_PRIVATE_KEY }}
|
||||||
|
|
||||||
|
# Build and push the production image; forward SSH agent without registry cache reuse.
|
||||||
|
- name: Build and push Docker image
|
||||||
|
uses: docker/build-push-action@v5
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
file: Dockerfile
|
||||||
|
push: true
|
||||||
|
ssh: default
|
||||||
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
@ -58,6 +58,10 @@ COPY tests tests
|
|||||||
COPY utilities utilities
|
COPY utilities utilities
|
||||||
COPY manage.py .
|
COPY manage.py .
|
||||||
|
|
||||||
|
# Used to run migrations etc
|
||||||
|
COPY entrypoint.sh entrypoint.sh
|
||||||
|
RUN chmod +x entrypoint.sh
|
||||||
|
|
||||||
# Install frontend deps
|
# Install frontend deps
|
||||||
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./
|
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./
|
||||||
|
|
||||||
@ -100,4 +104,5 @@ USER django
|
|||||||
|
|
||||||
EXPOSE 8000
|
EXPOSE 8000
|
||||||
|
|
||||||
CMD ["gunicorn", "envipath.wsgi:application", "--bind", "0.0.0.0:8000", "--workers", "3"]
|
ENTRYPOINT ["/app/entrypoint.sh"]
|
||||||
|
CMD ["gunicorn", "envipath.wsgi:application", "--bind", "0.0.0.0:8000", "--workers", "8"]
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import enum
|
import enum
|
||||||
|
from typing import Any, Dict
|
||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
|
|
||||||
from envipy_additional_information import EnviPyModel
|
from envipy_additional_information import EnviPyModel
|
||||||
@ -69,6 +70,12 @@ class Plugin(ABC):
|
|||||||
|
|
||||||
|
|
||||||
class Property(Plugin):
|
class Property(Plugin):
|
||||||
|
def parameters(self) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Returns the parameters of the PropertyPlugin.
|
||||||
|
"""
|
||||||
|
return {}
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def requires_rule_packages(cls) -> bool:
|
def requires_rule_packages(cls) -> bool:
|
||||||
@ -300,6 +307,12 @@ class Classifier(Plugin):
|
|||||||
"""
|
"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
def parameters(self) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Returns the parameters of the ClassifierPlugin.
|
||||||
|
"""
|
||||||
|
return {}
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def build(self, eP: EnviPyDTO, *args, **kwargs) -> BuildResult | None:
|
def build(self, eP: EnviPyDTO, *args, **kwargs) -> BuildResult | None:
|
||||||
"""
|
"""
|
||||||
|
|||||||
9
entrypoint.sh
Normal file
9
entrypoint.sh
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
if [ "${SKIP_DJANGO_SETUP:-false}" != "true" ]; then
|
||||||
|
python manage.py migrate --no-input
|
||||||
|
python manage.py collectstatic --no-input
|
||||||
|
fi
|
||||||
|
|
||||||
|
exec "$@"
|
||||||
@ -56,7 +56,7 @@ def get_pathway_for_iuclid_export(user, pathway_uuid: UUID) -> PathwayExportDTO:
|
|||||||
|
|
||||||
ai_for_node = []
|
ai_for_node = []
|
||||||
scenario_entries: list[PathwayScenarioDTO] = []
|
scenario_entries: list[PathwayScenarioDTO] = []
|
||||||
for scenario in sorted(node.scenarios.all(), key=lambda item: item.pk):
|
for scenario in sorted(node.get_scenarios(), key=lambda item: item.pk):
|
||||||
ai_for_scenario = list(scenario.get_additional_information(direct_only=True))
|
ai_for_scenario = list(scenario.get_additional_information(direct_only=True))
|
||||||
ai_for_node.extend(ai_for_scenario)
|
ai_for_node.extend(ai_for_scenario)
|
||||||
scenario_entries.append(
|
scenario_entries.append(
|
||||||
|
|||||||
@ -46,6 +46,10 @@ from .models import (
|
|||||||
Package = s.GET_PACKAGE_MODEL()
|
Package = s.GET_PACKAGE_MODEL()
|
||||||
|
|
||||||
|
|
||||||
|
def get_package_for_read(user, package_uuid):
|
||||||
|
return PackageManager.get_package_by_id(user, package_uuid)
|
||||||
|
|
||||||
|
|
||||||
def get_package_for_write(user, package_uuid):
|
def get_package_for_write(user, package_uuid):
|
||||||
p = PackageManager.get_package_by_id(user, package_uuid)
|
p = PackageManager.get_package_by_id(user, package_uuid)
|
||||||
if not PackageManager.writable(user, p):
|
if not PackageManager.writable(user, p):
|
||||||
@ -2291,3 +2295,42 @@ def predict(request, np: Form[NonPersistent]):
|
|||||||
return 403, {
|
return 403, {
|
||||||
"message": f"Getting Setting with id {np.setting_url} failed due to insufficient rights!"
|
"message": f"Getting Setting with id {np.setting_url} failed due to insufficient rights!"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
##########
|
||||||
|
# Export #
|
||||||
|
##########
|
||||||
|
class PackageExportInSchema(Schema):
|
||||||
|
package_uuid: str
|
||||||
|
additional_information_types: List[str] | None = None
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/export", response={200: Any, 403: Error})
|
||||||
|
def export(request, q: Query[PackageExportInSchema]):
|
||||||
|
try:
|
||||||
|
p = get_package_for_read(request.user, q.package_uuid)
|
||||||
|
|
||||||
|
from envipy_additional_information import registry
|
||||||
|
from utilities.misc import PathwayExporter
|
||||||
|
|
||||||
|
ai_types = []
|
||||||
|
if q.additional_information_types is not None:
|
||||||
|
for ai_type in q.additional_information_types:
|
||||||
|
if registry.get_model(ai_type) is None:
|
||||||
|
return 400, {
|
||||||
|
"message": f"Exporting Package with id {q.package_uuid} failed as {ai_type} is not a valid additional information type!"
|
||||||
|
}
|
||||||
|
ai_types.append(ai_type)
|
||||||
|
|
||||||
|
exporter = PathwayExporter(p, add_infs_to_export=ai_types)
|
||||||
|
res = exporter.do_export()
|
||||||
|
|
||||||
|
filename = f"{p.get_name().replace(' ', '_')}_{p.uuid}.tsv"
|
||||||
|
response = HttpResponse(res, content_type="text/csv")
|
||||||
|
response["Content-Disposition"] = f'attachment; filename="{filename}"'
|
||||||
|
|
||||||
|
return response
|
||||||
|
except ValueError:
|
||||||
|
return 403, {
|
||||||
|
"message": f"Exporting Package with id {q.package_uuid} failed due to insufficient rights!"
|
||||||
|
}
|
||||||
|
|||||||
@ -44,20 +44,25 @@ class Command(BaseCommand):
|
|||||||
"EPModel",
|
"EPModel",
|
||||||
"ApplicabilityDomain",
|
"ApplicabilityDomain",
|
||||||
"EnzymeLink",
|
"EnzymeLink",
|
||||||
|
"AdditionalInformation",
|
||||||
]
|
]
|
||||||
for model in MODELS:
|
for model in MODELS:
|
||||||
obj_cls = apps.get_model("epdb", model)
|
obj_cls = apps.get_model("epdb", model)
|
||||||
obj_cls.objects.update(
|
|
||||||
url=Replace(F("url"), Value(options["old"]), Value(options["new"]))
|
update_fields = {"url": Replace(F("url"), Value(options["old"]), Value(options["new"]))}
|
||||||
)
|
if hasattr(obj_cls, "description"):
|
||||||
if issubclass(obj_cls, EnviPathModel):
|
update_fields["description"] = Replace(
|
||||||
obj_cls.objects.update(
|
F("description"), Value(options["old"]), Value(options["new"])
|
||||||
kv=Cast(
|
|
||||||
Replace(
|
|
||||||
Cast(F("kv"), output_field=TextField()),
|
|
||||||
Value(options["old"]),
|
|
||||||
Value(options["new"]),
|
|
||||||
),
|
|
||||||
output_field=JSONField(),
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if issubclass(obj_cls, EnviPathModel):
|
||||||
|
update_fields["kv"] = Cast(
|
||||||
|
Replace(
|
||||||
|
Cast(F("kv"), output_field=TextField()),
|
||||||
|
Value(options["old"]),
|
||||||
|
Value(options["new"]),
|
||||||
|
),
|
||||||
|
output_field=JSONField(),
|
||||||
|
)
|
||||||
|
|
||||||
|
obj_cls.objects.update(**update_fields)
|
||||||
|
|||||||
97
epdb/management/commands/reaction_rule_mapping.py
Normal file
97
epdb/management/commands/reaction_rule_mapping.py
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
import logging
|
||||||
|
|
||||||
|
from django.core.management.base import BaseCommand
|
||||||
|
from django.db import transaction
|
||||||
|
from uuid import uuid4
|
||||||
|
from epdb.models import Package, ReactionExplanation
|
||||||
|
from utilities.chem import FormatConverter
|
||||||
|
from django.utils import timezone
|
||||||
|
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class Command(BaseCommand):
|
||||||
|
def add_arguments(self, parser):
|
||||||
|
parser.add_argument(
|
||||||
|
"--rule-package",
|
||||||
|
action="append",
|
||||||
|
default=["32de3cf4-e3e6-4168-956e-32fa5ddb0ce1"],
|
||||||
|
type=str,
|
||||||
|
help="UUID to process. Can be specified multiple times.",
|
||||||
|
)
|
||||||
|
|
||||||
|
parser.add_argument(
|
||||||
|
"--reaction-package",
|
||||||
|
action="append",
|
||||||
|
default=[
|
||||||
|
"32de3cf4-e3e6-4168-956e-32fa5ddb0ce1", # BBD
|
||||||
|
"f05e38d8-e9b4-4c3e-b0d8-9ab29966eccf", # Sediment
|
||||||
|
"521c547a-fd2a-491c-ad5b-7eaa1577fb65", # Sludge
|
||||||
|
"5882df9c-dae1-4d80-a40e-db4724271456", # Soil
|
||||||
|
"87a49584-d937-482c-9c33-25928dcb02a8", # PFAS
|
||||||
|
],
|
||||||
|
type=str,
|
||||||
|
help="UUID to process. Can be specified multiple times.",
|
||||||
|
)
|
||||||
|
|
||||||
|
parser.add_argument(
|
||||||
|
"--dry-run",
|
||||||
|
default=False,
|
||||||
|
action="store_true",
|
||||||
|
help="Perform dry run",
|
||||||
|
)
|
||||||
|
|
||||||
|
@transaction.atomic
|
||||||
|
def handle(self, *args, **options):
|
||||||
|
RUN_UUID = uuid4()
|
||||||
|
RUN_START = timezone.now()
|
||||||
|
|
||||||
|
rule_packages = Package.objects.filter(uuid__in=options["rule_package"])
|
||||||
|
reaction_packages = Package.objects.filter(uuid__in=options["reaction_package"])
|
||||||
|
|
||||||
|
rules = []
|
||||||
|
for rule_package in rule_packages:
|
||||||
|
rules.extend(rule_package.get_applicable_rules())
|
||||||
|
|
||||||
|
reactions = []
|
||||||
|
for reaction_package in reaction_packages:
|
||||||
|
reactions.extend(reaction_package.reactions)
|
||||||
|
|
||||||
|
logger.debug(f"Collected {len(rules)} rules and {len(reactions)} reactions.")
|
||||||
|
|
||||||
|
for i, reaction in enumerate(reactions):
|
||||||
|
logger.debug(f"Reaction {i} / {len(reactions)}")
|
||||||
|
for j, rule in enumerate(rules):
|
||||||
|
reactants, products = reaction.smirks().split(">>")
|
||||||
|
|
||||||
|
if len(reactants.split(".")) > 1:
|
||||||
|
logger.debug(f"Skipping reaction {reaction.uuid} as it has multiple reactants.")
|
||||||
|
break
|
||||||
|
|
||||||
|
products = products.split(".")
|
||||||
|
|
||||||
|
# Run reaction with rule
|
||||||
|
rule_products = rule.apply(reactants)
|
||||||
|
|
||||||
|
# Check if products match (in both directions if extras are not allowed)
|
||||||
|
for product_set in rule_products:
|
||||||
|
covered, exact = FormatConverter.smiles_covered_by(
|
||||||
|
products,
|
||||||
|
product_set.product_set,
|
||||||
|
standardize=True,
|
||||||
|
canonicalize_tautomers=True,
|
||||||
|
return_exact_match=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
if covered and not options["dry-run"]:
|
||||||
|
logger.debug(f"Reaction {reaction.uuid} explained by rule {rule.uuid}")
|
||||||
|
re = ReactionExplanation()
|
||||||
|
re.run_uuid = RUN_UUID
|
||||||
|
re.run_start = RUN_START
|
||||||
|
re.reaction = reaction
|
||||||
|
re.rule = rule
|
||||||
|
re.exact = exact
|
||||||
|
re.save()
|
||||||
|
# Its explained, if there are more sets skip them
|
||||||
|
break
|
||||||
113
epdb/migrations/0028_auto_20260812_0902.py
Normal file
113
epdb/migrations/0028_auto_20260812_0902.py
Normal file
@ -0,0 +1,113 @@
|
|||||||
|
# Generated by Django 6.0.3 on 2026-08-12 09:02
|
||||||
|
|
||||||
|
from django.conf import settings as s
|
||||||
|
from django.db import migrations
|
||||||
|
from envipy_additional_information import Likelihood, RuleLikelihood
|
||||||
|
|
||||||
|
NEW_RULE = {
|
||||||
|
"parent": "bt0005",
|
||||||
|
"name": "bt0005-3667",
|
||||||
|
"description": "vic-unsubstituted Aromatic > vic-Dihydroxyaromatic",
|
||||||
|
"smirks": "[#8:7]([H])-[#6:1]([H])-1-[#6:2]=[#6:3]-[#6:4]=[#6:5]-[#6:6]([H])-1-[#8:8]([H])>>[#8:7]([H])-[#6:1]=1-[#6:2]=[#6:3]-[#6:4]=[#6:5]-[#6:6]=1-[#8:8]([H])",
|
||||||
|
"scenario_name": "bt0005-3667 aerobic likelihood",
|
||||||
|
"scenario_aerobic_likelihood": RuleLikelihood(likelihood=Likelihood.LIKELY),
|
||||||
|
}
|
||||||
|
|
||||||
|
RULE_FIXES = {
|
||||||
|
"bt0005-4282": "[c:1]([H])1:[c:2]([H]):[#6,#7;a:3]:[c:4]:[c:5]:[c:6]1>>[c:1]([#8])1:[c:2]([#8]):[#6,#7;a:3]:[c:4]:[c:5]:[c:6]1",
|
||||||
|
"bt0014-4215": "[c:1]([H])1[c:8][#6,#7;a:7][c:6][c:5][c:4]1[#8;!$([OH]c:[#6,#7;a:7]([OH])):9]([H])>>[#8:9]([H])[c:4]1:[c:5]:[c:6]:[#6,#7;a:7]:[c:8]:[c:1]1[#8]([H])",
|
||||||
|
# "bt0063-3938": "[#1,#6:6][#7;X3;!$(NC1CC1)!$([N][C]=O)!$([!#8]CNC=O):1]([#1,#6:7])[#6;A;X4:2][H:3]>>[#1,#6:6][#7;X3:1]([H:3])(=[#1,#6:7]).[#6;A:2]=O",
|
||||||
|
# CN1C=NC2=C1C(=O)N(C)C(=O)N2 not working anymore with bt0063-3938 if change above is applied
|
||||||
|
"bt0063-3938": "[#1,#6:6][#7;X3;!$(NC1CC1)!$([N][C]=O)!$([!#8]CNC=O):1]([#1,#6:7])[#6;A;X4:2][H:3]>>[#1,#6:6][#7;X3:1]([#1,#6:7])[H:3].[#6;A:2]=O",
|
||||||
|
"bt0068-3564": "[#7:4]!@-[#6:2](!@-[#7:1])=[O:5]>>[#7:4]-[#6:2](-[O+0H1])=[O:5].[#7H1:1]",
|
||||||
|
"bt0180-2844": "[H][C:2]([#6:5]([H])([H])([H]))([#1,#6:4])!@-[#6:1]([H])([H])-[#6:3](-[#8-:8])=[O:6]>>[#6:5]([H])([H])([H])\\[#6:2](-[#1,#6:4])=[#6H:1]\\[#6:3](-[#8-:8])=[O:6]",
|
||||||
|
"bt0181-1278": "[#8-:1]-[#6:2](=[O:11])-[#6:7]=[#6:8]-[#6:3](-[H])=[#6:5](-Cl)-[#6:6](-[#8-:10])=[O:9]>>[O+0H1:10]-[#6:6](=[O:9])-[#6:5]=[#6:3]-1-[O+0:1]-[#6:2](=[O:11])-[#6:7]=[#6:8]-1",
|
||||||
|
"bt0298-3335": "[#6:1][N+:2]#[C:3]>>[#6:1]-[#7H2:2]-[#6:3]=O",
|
||||||
|
"bt0322-3393": "[H:10]\\[#6:6](=[#6:9](/[#6:1]([H])([H])([H]))-[#6:11]-[#6:12]-[#6:13]=[#6:14])-[#6:5](-[#16:7])=[O:8]>>[H:10]\\[#6:6](-[#6:5](-[#16:7])=[O:8])=[#6:9](\\[#6:11]-[#6:12]-[#6:13]=[#6:14])-[#6:1]-[#6](-[#8-])=O",
|
||||||
|
"bt0343-2675": "[#8-]-[#6](=O)-[c:1]1[c:6][cH:7][c:8](-[#7H2,#8H1:9])[cH:10][c:11]1>>[#8H][c:1]1[c:6][c:7][c:8]([*:9])[c:10][c:11]1",
|
||||||
|
"bt0350-3319": "[#6:6][#7:3][#6;!R:2]=[#7;!R:1][#6:5]>>[#6:5][#7:1][#6:2]=O.[#6:6][#7:3]", # Trig before 5 -> all of them shouldn't
|
||||||
|
"bt0374-4081": "[cH:4]1[c:16][c:15][c:14][c:13][c:3]1[#7,#8:2][c:1]1[c:8][c:9][c:10][c:11][c:12]1>>[#7,#8:2]-[c:1]1[c:12][c:11][c:10][c:9][c:8]1[c:13]1[c:14][c:15][c:16][c:4](-[#8])[c:3]1-[#8]",
|
||||||
|
"bt0378-3188": "[#8-:7][c:1]1[c:6]([#7+]([#8-])=O)[c:5][c:4]([#7+:9]([#8-])=O)[c:3][c:2]1([#7+:8]([#8-])=O)>>[#8+0:7]=[#6:1]1-[#6:6]-[#6:5]-[#6:4]([#7+:9]([#8-])=O)-[#6:3]-[#6:2]1([#7+:8]([#8-])=O)",
|
||||||
|
"bt0379-3190": "[#9,#17,#35,#53]-[#6:1](-[H])-1-[#6:5]-,=[#6:6]-[#6:7]-,=[#6:8]-[#6:2](-[H])-1-[#9,#17,#35,#53]>>[#6:6]~1-[#6:7]~[#6:8]-[#6:2]=[#6:1]-[#6:5]~1",
|
||||||
|
"bt0393-3367": "[#6:5]-[#6:1](-[#7:2](-[H])(-[H]))=[S+:3]-[#8-:6]>>[#6:5]-[#6:1](=[#7H1:2])-[S+0:3](=[O])-[#8+0H1:6]",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def forward_func(apps, schema_editor):
|
||||||
|
ContentType = apps.get_model("contenttypes", "ContentType")
|
||||||
|
|
||||||
|
pkg_class = s.EPDB_PACKAGE_MODEL
|
||||||
|
|
||||||
|
if len(pkg_class.split(".")) != 2:
|
||||||
|
raise ValueError(
|
||||||
|
f"EPDB_PACKAGE_MODEL must be of the form 'app_label.model_name', got {pkg_class}"
|
||||||
|
)
|
||||||
|
|
||||||
|
app_label, model_name = pkg_class.split(".")
|
||||||
|
Package = apps.get_model(app_label, model_name)
|
||||||
|
SimpleAmbitRule = apps.get_model("epdb", "SimpleAmbitRule")
|
||||||
|
ParallelRule = apps.get_model("epdb", "ParallelRule")
|
||||||
|
Scenario = apps.get_model("epdb", "Scenario")
|
||||||
|
AdditionalInformation = apps.get_model("epdb", "AdditionalInformation")
|
||||||
|
simple_ambit_rule_ct = ContentType.objects.get_for_model(SimpleAmbitRule)
|
||||||
|
|
||||||
|
if Package.objects.filter(name="EAWAG-BBD").exists():
|
||||||
|
p = Package.objects.get(name="EAWAG-BBD")
|
||||||
|
|
||||||
|
if not SimpleAmbitRule.objects.filter(package=p, name=NEW_RULE["name"]).exists():
|
||||||
|
# Create Missing Rule
|
||||||
|
new_sr = SimpleAmbitRule()
|
||||||
|
new_sr.polymorphic_ctype = simple_ambit_rule_ct
|
||||||
|
new_sr.package = p
|
||||||
|
new_sr.name = NEW_RULE["name"]
|
||||||
|
new_sr.description = NEW_RULE["description"]
|
||||||
|
new_sr.smirks = NEW_RULE["smirks"]
|
||||||
|
new_sr.save()
|
||||||
|
|
||||||
|
new_sr.url = "{}/simple-ambit-rule/{}".format(new_sr.package.url, new_sr.uuid)
|
||||||
|
new_sr.save()
|
||||||
|
|
||||||
|
# Add likelihood
|
||||||
|
new_scen = Scenario()
|
||||||
|
new_scen.package = p
|
||||||
|
new_scen.name = NEW_RULE["scenario_name"]
|
||||||
|
new_scen.save()
|
||||||
|
|
||||||
|
new_scen.url = "{}/scenario/{}".format(new_scen.package.url, new_scen.uuid)
|
||||||
|
new_scen.save()
|
||||||
|
|
||||||
|
ai = NEW_RULE["scenario_aerobic_likelihood"]
|
||||||
|
new_add_inf = AdditionalInformation()
|
||||||
|
new_add_inf.package = p
|
||||||
|
new_add_inf.type = ai.__class__.__name__
|
||||||
|
new_add_inf.data = ai.model_dump(mode="json")
|
||||||
|
new_add_inf.scenario = new_scen
|
||||||
|
new_add_inf.save()
|
||||||
|
|
||||||
|
new_add_inf.url = "{}/additional-information/{}".format(
|
||||||
|
new_add_inf.scenario.url, new_add_inf.uuid
|
||||||
|
)
|
||||||
|
new_add_inf.save()
|
||||||
|
|
||||||
|
# Link Scenario
|
||||||
|
new_sr.scenarios.add(new_scen)
|
||||||
|
|
||||||
|
# Link to bt0005
|
||||||
|
pr = ParallelRule.objects.get(package=p, name="bt0005")
|
||||||
|
pr.simple_rules.add(new_sr)
|
||||||
|
|
||||||
|
# Update others
|
||||||
|
for rule_name, smirks in RULE_FIXES.items():
|
||||||
|
sr = SimpleAmbitRule.objects.get(package=p, name=rule_name)
|
||||||
|
sr.smirks = smirks
|
||||||
|
sr.save()
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [
|
||||||
|
("epdb", "0027_alter_compound_aliases_and_more"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RunPython(forward_func, reverse_code=migrations.RunPython.noop),
|
||||||
|
]
|
||||||
@ -0,0 +1,63 @@
|
|||||||
|
# Generated by Django 6.0.3 on 2026-08-13 09:58
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
import django.utils.timezone
|
||||||
|
import model_utils.fields
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [
|
||||||
|
("epdb", "0028_auto_20260812_0902"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="ReactionExplanation",
|
||||||
|
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"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("run_uuid", models.UUIDField()),
|
||||||
|
("run_start", models.DateTimeField()),
|
||||||
|
("exact", models.BooleanField(default=False)),
|
||||||
|
(
|
||||||
|
"reaction",
|
||||||
|
models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE, to="epdb.reaction"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"rule",
|
||||||
|
models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to="epdb.rule"),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
"abstract": False,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="reaction",
|
||||||
|
name="explained_by",
|
||||||
|
field=models.ManyToManyField(
|
||||||
|
related_name="explained_reactions",
|
||||||
|
through="epdb.ReactionExplanation",
|
||||||
|
to="epdb.rule",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
||||||
37
epdb/migrations/0030_auto_20260814_0741.py
Normal file
37
epdb/migrations/0030_auto_20260814_0741.py
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
# Generated by Django 6.0.3 on 2026-08-14 07:41
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
def forward_func(apps, schema_editor):
|
||||||
|
ContentType = apps.get_model("contenttypes", "ContentType")
|
||||||
|
AdditionalInformation = apps.get_model("epdb", "AdditionalInformation")
|
||||||
|
|
||||||
|
models = {}
|
||||||
|
|
||||||
|
for c in ContentType.objects.all():
|
||||||
|
try:
|
||||||
|
models[(c.app_label, c.model)] = apps.get_model(c.app_label, c.model)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
for ai in AdditionalInformation.objects.all():
|
||||||
|
if ai.url is None:
|
||||||
|
if ai.content_type is None:
|
||||||
|
ai.url = "{}/additional-information/{}".format(ai.scenario.url, ai.uuid)
|
||||||
|
else:
|
||||||
|
model = models[(ai.content_type.app_label, ai.content_type.model)]
|
||||||
|
obj = model.objects.get(pk=ai.object_id)
|
||||||
|
ai.url = "{}/additional-information/{}".format(obj.url, ai.uuid)
|
||||||
|
|
||||||
|
ai.save()
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [
|
||||||
|
("epdb", "0029_reactionexplanation_reaction_explained_by"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RunPython(forward_func, reverse_code=migrations.RunPython.noop),
|
||||||
|
]
|
||||||
@ -854,9 +854,15 @@ class Compound(
|
|||||||
@property
|
@property
|
||||||
def related_reactions(self):
|
def related_reactions(self):
|
||||||
return (
|
return (
|
||||||
Reaction.objects.filter(package=self.package, educts__in=[self.default_structure])
|
(
|
||||||
| Reaction.objects.filter(package=self.package, products__in=[self.default_structure])
|
Reaction.objects.filter(package=self.package, educts__in=[self.default_structure])
|
||||||
).order_by("name")
|
| Reaction.objects.filter(
|
||||||
|
package=self.package, products__in=[self.default_structure]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.distinct()
|
||||||
|
.order_by("name")
|
||||||
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def related_nodes(self):
|
def related_nodes(self):
|
||||||
@ -1724,6 +1730,14 @@ class SequentialRuleOrdering(models.Model):
|
|||||||
order_index = models.IntegerField(null=False, blank=False)
|
order_index = models.IntegerField(null=False, blank=False)
|
||||||
|
|
||||||
|
|
||||||
|
class ReactionExplanation(TimeStampedModel):
|
||||||
|
run_uuid = models.UUIDField(null=False, blank=False)
|
||||||
|
run_start = models.DateTimeField(null=False, blank=False)
|
||||||
|
reaction = models.ForeignKey("epdb.Reaction", on_delete=models.CASCADE)
|
||||||
|
rule = models.ForeignKey("epdb.Rule", on_delete=models.CASCADE)
|
||||||
|
exact = models.BooleanField(default=False)
|
||||||
|
|
||||||
|
|
||||||
class Reaction(
|
class Reaction(
|
||||||
EnviPathModel, AliasMixin, ScenarioMixin, ReactionIdentifierMixin, AdditionalInformationMixin
|
EnviPathModel, AliasMixin, ScenarioMixin, ReactionIdentifierMixin, AdditionalInformationMixin
|
||||||
):
|
):
|
||||||
@ -1749,6 +1763,12 @@ class Reaction(
|
|||||||
|
|
||||||
external_identifiers = GenericRelation("ExternalIdentifier")
|
external_identifiers = GenericRelation("ExternalIdentifier")
|
||||||
|
|
||||||
|
explained_by = models.ManyToManyField(
|
||||||
|
"epdb.Rule",
|
||||||
|
through="ReactionExplanation",
|
||||||
|
related_name="explained_reactions",
|
||||||
|
)
|
||||||
|
|
||||||
def _url(self):
|
def _url(self):
|
||||||
return "{}/reaction/{}".format(self.package.url, self.uuid)
|
return "{}/reaction/{}".format(self.package.url, self.uuid)
|
||||||
|
|
||||||
@ -2162,7 +2182,7 @@ class Pathway(EnviPathModel, AliasMixin, ScenarioMixin, AdditionalInformationMix
|
|||||||
|
|
||||||
row += [cs.smiles, cs.get_name(), n.depth]
|
row += [cs.smiles, cs.get_name(), n.depth]
|
||||||
|
|
||||||
edges = self.edges.filter(end_nodes__in=[n])
|
edges = self.edges.filter(end_nodes=n)
|
||||||
if len(edges):
|
if len(edges):
|
||||||
for e in edges:
|
for e in edges:
|
||||||
_row = row.copy()
|
_row = row.copy()
|
||||||
@ -2574,7 +2594,7 @@ class Node(EnviPathModel, AliasMixin, ScenarioMixin, AdditionalInformationMixin)
|
|||||||
collected[str(ai.scenario.uuid)]["proposed"] = True
|
collected[str(ai.scenario.uuid)]["proposed"] = True
|
||||||
|
|
||||||
if ai.type == "Confidence":
|
if ai.type == "Confidence":
|
||||||
collected[str(ai.scenario.uuid)]["Confidence"] = ai.get().level
|
collected[str(ai.scenario.uuid)]["Confidence"] = ai.get().level.value
|
||||||
|
|
||||||
if ai.type == "TransformationProductImportance":
|
if ai.type == "TransformationProductImportance":
|
||||||
collected[str(ai.scenario.uuid)]["Transformation product importance"] = (
|
collected[str(ai.scenario.uuid)]["Transformation product importance"] = (
|
||||||
@ -2847,6 +2867,58 @@ class PackageBasedModel(EPModel):
|
|||||||
|
|
||||||
return res
|
return res
|
||||||
|
|
||||||
|
def parameters(self):
|
||||||
|
params = {
|
||||||
|
"Model Evaluation Threshold": f"{self.threshold:.2f}",
|
||||||
|
"Multi Gen Evaluation": "Yes" if self.multigen_eval else "No",
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.app_domain:
|
||||||
|
params["Applicability Domain Num Neighbors"] = f"{self.app_domain.num_neighbours:.2f}"
|
||||||
|
params["Applicability Domain Reliability Threshold"] = (
|
||||||
|
f"{self.app_domain.reliability_threshold:.2f}"
|
||||||
|
)
|
||||||
|
params["Applicability Domain Local Compatibility Threshold"] = (
|
||||||
|
f"{self.app_domain.local_compatibilty_threshold:.2f}"
|
||||||
|
)
|
||||||
|
|
||||||
|
return params
|
||||||
|
|
||||||
|
def statistics(self):
|
||||||
|
from sklearn.metrics import auc
|
||||||
|
|
||||||
|
recall = list(self.eval_results["average_recall_per_threshold"].values())
|
||||||
|
precision = list(self.eval_results["average_precision_per_threshold"].values())
|
||||||
|
mg_recall = list(
|
||||||
|
self.eval_results.get("multigen_average_recall_per_threshold", {}).values()
|
||||||
|
)
|
||||||
|
mg_precision = list(
|
||||||
|
self.eval_results.get("multigen_average_precision_per_threshold", {}).values()
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"accuracy": [
|
||||||
|
self.eval_results["average_accuracy"],
|
||||||
|
self.eval_results.get("multigen_average_accuracy"),
|
||||||
|
],
|
||||||
|
"precision": [
|
||||||
|
self.eval_results["average_precision_per_threshold"][f"{self.threshold:.2f}"],
|
||||||
|
self.eval_results.get("multigen_average_precision_per_threshold", {}).get(
|
||||||
|
f"{self.threshold:.2f}"
|
||||||
|
),
|
||||||
|
],
|
||||||
|
"recall": [
|
||||||
|
self.eval_results["average_recall_per_threshold"][f"{self.threshold:.2f}"],
|
||||||
|
self.eval_results.get("multigen_average_recall_per_threshold", {}).get(
|
||||||
|
f"{self.threshold:.2f}"
|
||||||
|
),
|
||||||
|
],
|
||||||
|
"Area under PR Curve": [
|
||||||
|
auc(recall, precision),
|
||||||
|
auc(mg_recall, mg_precision) if self.multigen_eval else None,
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
def applicable_rules(self) -> List["Rule"]:
|
def applicable_rules(self) -> List["Rule"]:
|
||||||
"""
|
"""
|
||||||
@ -3027,8 +3099,6 @@ class PackageBasedModel(EPModel):
|
|||||||
thresholds.append(np.float64(threshold))
|
thresholds.append(np.float64(threshold))
|
||||||
thresholds.sort()
|
thresholds.sort()
|
||||||
|
|
||||||
logger.info(f"Thresholds: {thresholds}")
|
|
||||||
|
|
||||||
precision = {f"{t:.2f}": [] for t in thresholds}
|
precision = {f"{t:.2f}": [] for t in thresholds}
|
||||||
recall = {f"{t:.2f}": [] for t in thresholds}
|
recall = {f"{t:.2f}": [] for t in thresholds}
|
||||||
|
|
||||||
@ -3077,14 +3147,17 @@ class PackageBasedModel(EPModel):
|
|||||||
for t in thresholds:
|
for t in thresholds:
|
||||||
for true, pred in zip(pathways, pred_pathways):
|
for true, pred in zip(pathways, pred_pathways):
|
||||||
acc, pre, rec = multigen_eval(true, pred, t)
|
acc, pre, rec = multigen_eval(true, pred, t)
|
||||||
if abs(t - threshold) < 0.01:
|
|
||||||
mg_acc = acc
|
if f"{t:.2f}" == f"{threshold:.2f}":
|
||||||
|
mg_acc += acc
|
||||||
|
|
||||||
precision[f"{t:.2f}"].append(pre)
|
precision[f"{t:.2f}"].append(pre)
|
||||||
recall[f"{t:.2f}"].append(rec)
|
recall[f"{t:.2f}"].append(rec)
|
||||||
|
|
||||||
|
avg_mg_acc = mg_acc / len(root_compounds)
|
||||||
precision = {k: sum(v) / len(v) if len(v) > 0 else 0 for k, v in precision.items()}
|
precision = {k: sum(v) / len(v) if len(v) > 0 else 0 for k, v in precision.items()}
|
||||||
recall = {k: sum(v) / len(v) if len(v) > 0 else 0 for k, v in recall.items()}
|
recall = {k: sum(v) / len(v) if len(v) > 0 else 0 for k, v in recall.items()}
|
||||||
return mg_acc, precision, recall
|
return avg_mg_acc, precision, recall
|
||||||
|
|
||||||
# If there are eval packages perform single generation evaluation on them instead of random splits
|
# If there are eval packages perform single generation evaluation on them instead of random splits
|
||||||
if self.eval_packages.count() > 0:
|
if self.eval_packages.count() > 0:
|
||||||
@ -4216,6 +4289,9 @@ class ClassifierPluginModel(PackageBasedModel):
|
|||||||
instance = impl(conf)
|
instance = impl(conf)
|
||||||
return instance
|
return instance
|
||||||
|
|
||||||
|
def parameters(self):
|
||||||
|
return self.instance().parameters()
|
||||||
|
|
||||||
def build_dataset(self):
|
def build_dataset(self):
|
||||||
"""
|
"""
|
||||||
Required by general model contract but actual implementation resides in plugin.
|
Required by general model contract but actual implementation resides in plugin.
|
||||||
@ -4436,6 +4512,9 @@ class PropertyPluginModel(PackageBasedModel):
|
|||||||
instance = impl()
|
instance = impl()
|
||||||
return instance
|
return instance
|
||||||
|
|
||||||
|
def parameters(self):
|
||||||
|
return self.instance().parameters()
|
||||||
|
|
||||||
def build_dataset(self):
|
def build_dataset(self):
|
||||||
"""
|
"""
|
||||||
Required by general model contract but actual implementation resides in plugin.
|
Required by general model contract but actual implementation resides in plugin.
|
||||||
|
|||||||
@ -477,8 +477,7 @@ def batch_predict(
|
|||||||
limit=None,
|
limit=None,
|
||||||
setting_overrides={
|
setting_overrides={
|
||||||
"max_nodes": num_tps,
|
"max_nodes": num_tps,
|
||||||
"max_depth": num_tps,
|
"model_threshold": 0.0,
|
||||||
"model_threshold": 0.001,
|
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@ -1080,19 +1080,23 @@ def package_model(request, package_uuid, model_uuid):
|
|||||||
for pr in pred_res:
|
for pr in pred_res:
|
||||||
if len(pr) > 0:
|
if len(pr) > 0:
|
||||||
products = []
|
products = []
|
||||||
|
|
||||||
for prod_set in pr.product_sets:
|
for prod_set in pr.product_sets:
|
||||||
logger.debug(f"Checking {prod_set}")
|
logger.debug(f"Checking {prod_set}")
|
||||||
products.append(tuple([x for x in prod_set]))
|
products.append(tuple([x for x in prod_set]))
|
||||||
|
|
||||||
res["pred"].append(
|
products = list(set(products))
|
||||||
{
|
|
||||||
"products": list(set(products)),
|
for prod in products:
|
||||||
"probability": pr.probability,
|
res["pred"].append(
|
||||||
"btrule": {k: getattr(pr.rule, k) for k in ["url", "name"]}
|
{
|
||||||
if pr.rule is not None
|
"products": list(prod),
|
||||||
else None,
|
"probability": pr.probability,
|
||||||
}
|
"btrule": {k: getattr(pr.rule, k) for k in ["url", "name"]}
|
||||||
)
|
if pr.rule is not None
|
||||||
|
else None,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
# Sort data by prob desc
|
# Sort data by prob desc
|
||||||
res["pred"] = sorted(
|
res["pred"] = sorted(
|
||||||
|
|||||||
@ -351,7 +351,8 @@ class PathwayMapper:
|
|||||||
|
|
||||||
props = SoilPropertiesData()
|
props = SoilPropertiesData()
|
||||||
|
|
||||||
for ai in ai_list:
|
for ai_obj in ai_list:
|
||||||
|
ai = ai_obj.get()
|
||||||
if isinstance(ai, SoilTexture1) and props.soil_type is None:
|
if isinstance(ai, SoilTexture1) and props.soil_type is None:
|
||||||
props.soil_type = ai.type.value
|
props.soil_type = ai.type.value
|
||||||
elif isinstance(ai, SoilTexture2):
|
elif isinstance(ai, SoilTexture2):
|
||||||
|
|||||||
@ -117,6 +117,39 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
{% if model.parameters %}
|
||||||
|
<!-- Model Parameters Panel -->
|
||||||
|
<div class="collapse-arrow bg-base-200 collapse">
|
||||||
|
<input type="checkbox" checked />
|
||||||
|
<div class="collapse-title text-xl font-medium">Model Parameters</div>
|
||||||
|
<div class="collapse-content">
|
||||||
|
<div class="flex justify-center">
|
||||||
|
<div
|
||||||
|
id="model-stats"
|
||||||
|
class="overflow-x-auto rounded-box shadow-md bg-base-100"
|
||||||
|
>
|
||||||
|
<table class="table table-fixed w-full">
|
||||||
|
<thead class="text-base">
|
||||||
|
<tr>
|
||||||
|
<th class="w-3/5">Parameter</th>
|
||||||
|
<th>Value</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for param, value in model.parameters.items %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ param }}</td>
|
||||||
|
<td>{{ value }}</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block usemodel %}
|
{% block usemodel %}
|
||||||
|
|||||||
@ -109,12 +109,12 @@
|
|||||||
res += "<th scope='row'>" + cnt + "</th>";
|
res += "<th scope='row'>" + cnt + "</th>";
|
||||||
res +=
|
res +=
|
||||||
"<th scope='row'>" +
|
"<th scope='row'>" +
|
||||||
data[transformation]["products"][0].join(", ") +
|
data[transformation]["products"].join(", ") +
|
||||||
"</th>";
|
"</th>";
|
||||||
res +=
|
res +=
|
||||||
"<th scope='row'>" +
|
"<th scope='row'>" +
|
||||||
"<img width='400' src='{% url 'depict' %}?smiles=" +
|
"<img width='400' src='{% url 'depict' %}?smiles=" +
|
||||||
encodeURIComponent(data[transformation]["products"][0].join(".")) +
|
encodeURIComponent(data[transformation]["products"].join(".")) +
|
||||||
"'></th>";
|
"'></th>";
|
||||||
res +=
|
res +=
|
||||||
"<th scope='row'>" +
|
"<th scope='row'>" +
|
||||||
@ -313,6 +313,44 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
<!-- Model Statistics Panel -->
|
||||||
|
<div class="collapse-arrow bg-base-200 collapse">
|
||||||
|
<input type="checkbox" checked />
|
||||||
|
<div class="collapse-title text-xl font-medium">Model Statistics for threshold {{ model.threshold }}</div>
|
||||||
|
<div class="collapse-content">
|
||||||
|
<div class="flex justify-center">
|
||||||
|
<div
|
||||||
|
id="model-stats"
|
||||||
|
class="overflow-x-auto rounded-box shadow-md bg-base-100"
|
||||||
|
>
|
||||||
|
<table class="table table-fixed w-full">
|
||||||
|
<thead class="text-base">
|
||||||
|
<tr>
|
||||||
|
<th class="w-1/5">Metric</th>
|
||||||
|
<th>Single Gen Value</th>
|
||||||
|
{% if model.multigen_eval %}
|
||||||
|
<th>Multi Gen Value</th>
|
||||||
|
{% endif %}
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for metric, value in model.statistics.items %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ metric|upper }}</td>
|
||||||
|
<td>{{ value.0|floatformat:3 }}</td>
|
||||||
|
{% if model.multigen_eval %}
|
||||||
|
<td>{{ value.1|floatformat:3 }}</td>
|
||||||
|
{% endif %}
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<script>
|
<script>
|
||||||
function makeChart(selector, data) {
|
function makeChart(selector, data) {
|
||||||
|
|||||||
@ -139,7 +139,7 @@
|
|||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Model</td>
|
<td>Model</td>
|
||||||
<td>{{ half_lifes.0.model }}</td>
|
<td>{{ half_lifes.0.model.value }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|||||||
@ -535,7 +535,7 @@
|
|||||||
<input type="checkbox" />
|
<input type="checkbox" />
|
||||||
<div class="collapse-title text-xl font-medium">Setting</div>
|
<div class="collapse-title text-xl font-medium">Setting</div>
|
||||||
<div class="collapse-content">
|
<div class="collapse-content">
|
||||||
{% with setting_to_render=pathway.setting can_be_default=False %}
|
{% with setting_to_render=pathway.setting_with_overrides can_be_default=False %}
|
||||||
{% include "objects/setting_template.html" %}
|
{% include "objects/setting_template.html" %}
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -95,21 +95,23 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Other Prediction Settings -->
|
<!-- Other Prediction Settings -->
|
||||||
<div class="collapse-arrow bg-base-200 collapse">
|
{% if meta.available_settings|length > 1 %}
|
||||||
<input type="checkbox" />
|
<div class="collapse-arrow bg-base-200 collapse">
|
||||||
<div class="collapse-title text-xl font-medium">
|
<input type="checkbox" />
|
||||||
Other Prediction Settings
|
<div class="collapse-title text-xl font-medium">
|
||||||
|
Other Prediction Settings
|
||||||
|
</div>
|
||||||
|
<div class="collapse-content space-y-3">
|
||||||
|
{% for setting in meta.available_settings %}
|
||||||
|
{% if setting != user.default_setting %}
|
||||||
|
{% with setting_to_render=setting can_be_default=True %}
|
||||||
|
{% include "objects/setting_template.html" %}
|
||||||
|
{% endwith %}
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="collapse-content space-y-3">
|
{% endif %}
|
||||||
{% for setting in meta.available_settings %}
|
|
||||||
{% if setting != user.default_setting %}
|
|
||||||
{% with setting_to_render=setting can_be_default=True %}
|
|
||||||
{% include "objects/setting_template.html" %}
|
|
||||||
{% endwith %}
|
|
||||||
{% endif %}
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
|||||||
@ -58,7 +58,7 @@ class MultiGenTest(TestCase):
|
|||||||
|
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
pw.setting_with_overrides.max_depth,
|
pw.setting_with_overrides.max_depth,
|
||||||
f"{num_tps} (this is an override for this particular pathway)",
|
5,
|
||||||
)
|
)
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
pw.setting_with_overrides.max_nodes,
|
pw.setting_with_overrides.max_nodes,
|
||||||
|
|||||||
@ -47,7 +47,7 @@ class ModelViewTest(TestCase):
|
|||||||
|
|
||||||
expected = [
|
expected = [
|
||||||
{
|
{
|
||||||
"products": [["CCN(CC)C(=O)C1=CC(C=O)=CC=C1"]],
|
"products": ["CCN(CC)C(=O)C1=CC(C=O)=CC=C1"],
|
||||||
"probability": 0.75,
|
"probability": 0.75,
|
||||||
"btrule": {
|
"btrule": {
|
||||||
"url": "http://localhost:8000/package/1869d3f0-60bb-41fd-b6f8-afa75ffb09d3/simple-ambit-rule/2f2e0c39-e109-4836-959f-2bda2524f022",
|
"url": "http://localhost:8000/package/1869d3f0-60bb-41fd-b6f8-afa75ffb09d3/simple-ambit-rule/2f2e0c39-e109-4836-959f-2bda2524f022",
|
||||||
@ -55,7 +55,7 @@ class ModelViewTest(TestCase):
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"products": [["O=C(O)C1=CC(CO)=CC=C1", "CCNCC"]],
|
"products": ["O=C(O)C1=CC(CO)=CC=C1", "CCNCC"],
|
||||||
"probability": 0.25,
|
"probability": 0.25,
|
||||||
"btrule": {
|
"btrule": {
|
||||||
"url": "http://localhost:8000/package/1869d3f0-60bb-41fd-b6f8-afa75ffb09d3/simple-ambit-rule/0e6e9290-b658-4450-b291-3ec19fa19206",
|
"url": "http://localhost:8000/package/1869d3f0-60bb-41fd-b6f8-afa75ffb09d3/simple-ambit-rule/0e6e9290-b658-4450-b291-3ec19fa19206",
|
||||||
@ -63,7 +63,7 @@ class ModelViewTest(TestCase):
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"products": [["CCNC(=O)C1=CC(CO)=CC=C1", "CC=O"]],
|
"products": ["CCNC(=O)C1=CC(CO)=CC=C1", "CC=O"],
|
||||||
"probability": 0.0,
|
"probability": 0.0,
|
||||||
"btrule": {
|
"btrule": {
|
||||||
"url": "http://localhost:8000/package/1869d3f0-60bb-41fd-b6f8-afa75ffb09d3/simple-ambit-rule/27a3a353-0b66-4228-bd16-e407949e90df",
|
"url": "http://localhost:8000/package/1869d3f0-60bb-41fd-b6f8-afa75ffb09d3/simple-ambit-rule/27a3a353-0b66-4228-bd16-e407949e90df",
|
||||||
|
|||||||
@ -68,6 +68,8 @@ class PredictionResult(object):
|
|||||||
|
|
||||||
|
|
||||||
class FormatConverter(object):
|
class FormatConverter(object):
|
||||||
|
tautomer_enumerator = rdMolStandardize.TautomerEnumerator()
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def mass(smiles):
|
def mass(smiles):
|
||||||
return Descriptors.MolWt(FormatConverter.from_smiles(smiles))
|
return Descriptors.MolWt(FormatConverter.from_smiles(smiles))
|
||||||
@ -240,8 +242,9 @@ class FormatConverter(object):
|
|||||||
Chem.RemoveStereochemistry(res_mol)
|
Chem.RemoveStereochemistry(res_mol)
|
||||||
|
|
||||||
if canonicalize_tautomers:
|
if canonicalize_tautomers:
|
||||||
te = rdMolStandardize.TautomerEnumerator() # idem
|
tautomers = FormatConverter.tautomer_enumerator.Enumerate(res_mol)
|
||||||
res_mol = te.Canonicalize(res_mol)
|
if len(tautomers) >= 1:
|
||||||
|
res_mol = FormatConverter.tautomer_enumerator.PickCanonical(tautomers)
|
||||||
|
|
||||||
return Chem.MolToSmiles(res_mol, kekuleSmiles=True)
|
return Chem.MolToSmiles(res_mol, kekuleSmiles=True)
|
||||||
|
|
||||||
@ -389,7 +392,7 @@ class FormatConverter(object):
|
|||||||
prods.append(p)
|
prods.append(p)
|
||||||
|
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
logger.error(f"Sanitizing and converting failed:\n{e}")
|
logger.debug(f"Sanitizing and converting failed:\n{e}")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if len(prods):
|
if len(prods):
|
||||||
@ -397,7 +400,8 @@ class FormatConverter(object):
|
|||||||
pss.add(ps)
|
pss.add(ps)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Applying {smirks} on {smiles} failed:\n{e}")
|
logger.debug(f"Applying {smirks} on {smiles} failed:\n{e}")
|
||||||
|
pass
|
||||||
|
|
||||||
return list(pss)
|
return list(pss)
|
||||||
|
|
||||||
@ -444,6 +448,7 @@ class FormatConverter(object):
|
|||||||
r_smiles: List[str],
|
r_smiles: List[str],
|
||||||
standardize: bool = True,
|
standardize: bool = True,
|
||||||
canonicalize_tautomers: bool = True,
|
canonicalize_tautomers: bool = True,
|
||||||
|
return_exact_match: bool = False,
|
||||||
) -> bool:
|
) -> bool:
|
||||||
"""
|
"""
|
||||||
Check if all SMILES in the left list are covered by (contained in) the right list.
|
Check if all SMILES in the left list are covered by (contained in) the right list.
|
||||||
@ -482,8 +487,10 @@ class FormatConverter(object):
|
|||||||
if standardize:
|
if standardize:
|
||||||
for smi in l_smiles:
|
for smi in l_smiles:
|
||||||
try:
|
try:
|
||||||
smi = FormatConverter.standardize(
|
smi = FormatConverter.canonicalize(
|
||||||
smi, remove_stereo=True, canonicalize_tautomers=canonicalize_tautomers
|
FormatConverter.standardize(
|
||||||
|
smi, remove_stereo=True, canonicalize_tautomers=canonicalize_tautomers
|
||||||
|
)
|
||||||
)
|
)
|
||||||
except Exception:
|
except Exception:
|
||||||
# :shrug:
|
# :shrug:
|
||||||
@ -497,8 +504,12 @@ class FormatConverter(object):
|
|||||||
if standardize:
|
if standardize:
|
||||||
for smi in r_smiles:
|
for smi in r_smiles:
|
||||||
try:
|
try:
|
||||||
smi = FormatConverter.standardize(
|
smi = FormatConverter.canonicalize(
|
||||||
smi, remove_stereo=True, canonicalize_tautomers=canonicalize_tautomers
|
FormatConverter.standardize(
|
||||||
|
smi,
|
||||||
|
remove_stereo=True,
|
||||||
|
canonicalize_tautomers=canonicalize_tautomers,
|
||||||
|
)
|
||||||
)
|
)
|
||||||
except Exception:
|
except Exception:
|
||||||
# :shrug:
|
# :shrug:
|
||||||
@ -507,8 +518,11 @@ class FormatConverter(object):
|
|||||||
standardized_r_smiles.append(smi)
|
standardized_r_smiles.append(smi)
|
||||||
else:
|
else:
|
||||||
standardized_r_smiles = r_smiles
|
standardized_r_smiles = r_smiles
|
||||||
|
if not return_exact_match:
|
||||||
return len(set(standardized_l_smiles).difference(set(standardized_r_smiles))) == 0
|
return len(set(standardized_l_smiles).difference(set(standardized_r_smiles))) == 0
|
||||||
|
return len(set(standardized_l_smiles).difference(set(standardized_r_smiles))) == 0, set(
|
||||||
|
standardized_l_smiles
|
||||||
|
) == set(standardized_r_smiles)
|
||||||
|
|
||||||
|
|
||||||
class Standardizer(ABC):
|
class Standardizer(ABC):
|
||||||
|
|||||||
@ -4,11 +4,13 @@ import hmac
|
|||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
import uuid
|
import uuid
|
||||||
|
from abc import ABC, abstractmethod
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import Any, Dict, List, Optional, TYPE_CHECKING
|
from typing import Any, Dict, List, Optional, TYPE_CHECKING, Type
|
||||||
|
|
||||||
from django.conf import settings as s
|
from django.conf import settings as s
|
||||||
|
from envipy_additional_information import EnviPyModel, UIConfig
|
||||||
from ninja import Schema
|
from ninja import Schema
|
||||||
from pydantic import HttpUrl, ValidationError
|
from pydantic import HttpUrl, ValidationError
|
||||||
|
|
||||||
@ -132,7 +134,7 @@ class EnzymeExportSchema(RefEnzymeExportSchema):
|
|||||||
|
|
||||||
|
|
||||||
class EnzymeRuleExportSchema(RefRuleExportSchema):
|
class EnzymeRuleExportSchema(RefRuleExportSchema):
|
||||||
enzymes: List[EnzymeExportSchema] | None = None
|
enzymes: List[EnzymeExportSchema] = []
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def resolve_enzymes(obj):
|
def resolve_enzymes(obj):
|
||||||
@ -277,15 +279,20 @@ class PackageExportSchema(Schema):
|
|||||||
return AdditionalInformation.objects.filter(package=obj)
|
return AdditionalInformation.objects.filter(package=obj)
|
||||||
|
|
||||||
|
|
||||||
class PackageExporter:
|
class Exporter(ABC):
|
||||||
def __init__(self, package: Package):
|
def __init__(self, package: Package):
|
||||||
self._raw_package = package
|
self._raw_package = package
|
||||||
|
|
||||||
def do_export(self):
|
def do_export(self):
|
||||||
return PackageExporter._export_package_as_json(self._raw_package)
|
return self._export()
|
||||||
|
|
||||||
@staticmethod
|
@abstractmethod
|
||||||
def _export_package_as_json(package: Package) -> Dict[str, Any]:
|
def _export(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class PackageExporter(Exporter):
|
||||||
|
def _export(self) -> Dict[str, Any]:
|
||||||
"""
|
"""
|
||||||
Dumps a Package and all its related objects as JSON.
|
Dumps a Package and all its related objects as JSON.
|
||||||
|
|
||||||
@ -296,11 +303,126 @@ class PackageExporter:
|
|||||||
Dict containing the complete package data as JSON-serializable structure
|
Dict containing the complete package data as JSON-serializable structure
|
||||||
"""
|
"""
|
||||||
|
|
||||||
data = PackageExportSchema.from_orm(package)
|
data = PackageExportSchema.from_orm(self._raw_package)
|
||||||
|
|
||||||
return data.model_dump(mode="json")
|
return data.model_dump(mode="json")
|
||||||
|
|
||||||
|
|
||||||
|
class PathwayExporter(Exporter):
|
||||||
|
def __init__(self, package: Package, add_infs_to_export: List[str] = []):
|
||||||
|
super().__init__(package)
|
||||||
|
self._add_infs_to_export = add_infs_to_export
|
||||||
|
|
||||||
|
def _flatten_additional_information(self, ai: AdditionalInformation) -> dict[str, Any]:
|
||||||
|
model_cls: Type[EnviPyModel] = type(ai.get())
|
||||||
|
|
||||||
|
def _flatten(d: dict, parent_key: str = "") -> dict[str, Any]:
|
||||||
|
items: dict[str, Any] = {}
|
||||||
|
for key, value in d.items():
|
||||||
|
new_key = f"{parent_key}__{key.lower()}" if parent_key else key.lower()
|
||||||
|
if isinstance(value, dict):
|
||||||
|
items.update(_flatten(value, new_key))
|
||||||
|
else:
|
||||||
|
items[new_key] = value
|
||||||
|
return items
|
||||||
|
|
||||||
|
flat = _flatten(ai.data, ai.type)
|
||||||
|
|
||||||
|
ui_class = getattr(model_cls, "UI", None)
|
||||||
|
if ui_class is None:
|
||||||
|
return flat
|
||||||
|
|
||||||
|
for f in model_cls.model_fields:
|
||||||
|
ui_info = getattr(ui_class, f, None)
|
||||||
|
|
||||||
|
if not isinstance(ui_info, UIConfig) or ui_info.unit is None:
|
||||||
|
continue
|
||||||
|
|
||||||
|
flat[f"{model_cls.__name__}__{f}__unit"] = ui_info.unit
|
||||||
|
|
||||||
|
return flat
|
||||||
|
|
||||||
|
def _export(self):
|
||||||
|
from io import StringIO
|
||||||
|
from csv import DictWriter
|
||||||
|
|
||||||
|
rows = []
|
||||||
|
|
||||||
|
for pw in self._raw_package.pathways.all():
|
||||||
|
for n in pw.nodes:
|
||||||
|
for scen in pw.scenarios.all():
|
||||||
|
row = {
|
||||||
|
"pathway_name": pw.name,
|
||||||
|
"pathway_id": str(pw.url),
|
||||||
|
"node_depth": n.depth,
|
||||||
|
"compound_id": str(n.default_node_label.compound.url),
|
||||||
|
"pubchem_ID": n.default_node_label.pubchem_compound_id,
|
||||||
|
"compound_name": n.default_node_label.compound.name,
|
||||||
|
"compound_smiles": n.default_node_label.smiles,
|
||||||
|
"scenario_id": str(scen.url),
|
||||||
|
"scenario_name": scen.name,
|
||||||
|
"scenario_type": scen.scenario_type,
|
||||||
|
"scenario_description": scen.description,
|
||||||
|
}
|
||||||
|
|
||||||
|
if self._add_infs_to_export:
|
||||||
|
ai_qs = AdditionalInformation.objects.filter(
|
||||||
|
scenario=scen, type__in=self._add_infs_to_export
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
ai_qs = AdditionalInformation.objects.filter(scenario=scen)
|
||||||
|
|
||||||
|
for ai in ai_qs:
|
||||||
|
if ai.type == "ProposedIntermediate" and ai.content_object == n:
|
||||||
|
row.update({"proposed_intermediate": True})
|
||||||
|
elif ai.type == "SpikeCompound":
|
||||||
|
spike = {"SpikeCompound__url": ai.get().url}
|
||||||
|
|
||||||
|
try:
|
||||||
|
struc = CompoundStructure.objects.get(
|
||||||
|
compound__package=self._raw_package, url=ai.get().url
|
||||||
|
)
|
||||||
|
spike["SpikeCompound__smiles"] = struc.smiles
|
||||||
|
except Exception:
|
||||||
|
spike["SpikeCompound__smiles"] = None
|
||||||
|
|
||||||
|
row.update(**spike)
|
||||||
|
else:
|
||||||
|
row.update(self._flatten_additional_information(ai))
|
||||||
|
rows.append(row)
|
||||||
|
|
||||||
|
# Get all header fields
|
||||||
|
all_header_fields = set()
|
||||||
|
for row in rows:
|
||||||
|
all_header_fields.update(row.keys())
|
||||||
|
|
||||||
|
# Per request the CSV should start with these fields
|
||||||
|
header = [
|
||||||
|
"pathway_name",
|
||||||
|
"pathway_id",
|
||||||
|
"node_depth",
|
||||||
|
"compound_id",
|
||||||
|
"pubchem_ID",
|
||||||
|
"compound_name",
|
||||||
|
"compound_smiles",
|
||||||
|
"scenario_id",
|
||||||
|
"scenario_name",
|
||||||
|
"scenario_type",
|
||||||
|
"scenario_description",
|
||||||
|
]
|
||||||
|
|
||||||
|
# User remaining fields and place them after the predefined values in a sorted manner
|
||||||
|
remainder = sorted(list(all_header_fields.difference(set(header))))
|
||||||
|
header.extend(remainder)
|
||||||
|
|
||||||
|
buffer = StringIO()
|
||||||
|
writer = DictWriter(buffer, fieldnames=header, delimiter="\t")
|
||||||
|
writer.writeheader()
|
||||||
|
writer.writerows(rows)
|
||||||
|
buffer.seek(0)
|
||||||
|
return buffer.getvalue()
|
||||||
|
|
||||||
|
|
||||||
class PackageImporter:
|
class PackageImporter:
|
||||||
def __init__(self, package: Dict[str, Any], preserve_uuids: bool = False):
|
def __init__(self, package: Dict[str, Any], preserve_uuids: bool = False):
|
||||||
self.preserve_uuids = preserve_uuids
|
self.preserve_uuids = preserve_uuids
|
||||||
|
|||||||
@ -238,9 +238,11 @@ class RuleBasedDataset(Dataset):
|
|||||||
):
|
):
|
||||||
if feat_funcs is None:
|
if feat_funcs is None:
|
||||||
feat_funcs = [FormatConverter.maccs]
|
feat_funcs = [FormatConverter.maccs]
|
||||||
_structures = set() # Get all the structures
|
|
||||||
|
_structures = set()
|
||||||
for r in reactions:
|
for r in reactions:
|
||||||
_structures.update(r.educts.all())
|
_structures.update(r.educts.all())
|
||||||
|
|
||||||
if not educts_only:
|
if not educts_only:
|
||||||
_structures.update(r.products.all())
|
_structures.update(r.products.all())
|
||||||
|
|
||||||
@ -282,17 +284,14 @@ class RuleBasedDataset(Dataset):
|
|||||||
if key not in triggered:
|
if key not in triggered:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# standardize products from reactions for comparison
|
if FormatConverter.smiles_covered_by(
|
||||||
standardized_products = []
|
[prod.smiles for prod in r.products.all()],
|
||||||
for cs in r.products.all():
|
list(triggered[key]),
|
||||||
smi = cs.smiles
|
standardize=True,
|
||||||
try:
|
canonicalize_tautomers=True,
|
||||||
smi = FormatConverter.standardize(smi, remove_stereo=True)
|
):
|
||||||
except Exception:
|
|
||||||
logger.debug(f"Standardizing SMILES failed for {smi}")
|
|
||||||
standardized_products.append(smi)
|
|
||||||
if len(set(standardized_products).difference(triggered[key])) == 0:
|
|
||||||
observed.add(key)
|
observed.add(key)
|
||||||
|
|
||||||
feat_columns = []
|
feat_columns = []
|
||||||
for feat_func in feat_funcs:
|
for feat_func in feat_funcs:
|
||||||
if isinstance(feat_func, Descriptor):
|
if isinstance(feat_func, Descriptor):
|
||||||
@ -301,6 +300,7 @@ class RuleBasedDataset(Dataset):
|
|||||||
feats = feat_func(compounds[0].smiles)
|
feats = feat_func(compounds[0].smiles)
|
||||||
start_i = len(feat_columns)
|
start_i = len(feat_columns)
|
||||||
feat_columns.extend([f"feature_{start_i + i}" for i, _ in enumerate(feats)])
|
feat_columns.extend([f"feature_{start_i + i}" for i, _ in enumerate(feats)])
|
||||||
|
|
||||||
ds_columns = (
|
ds_columns = (
|
||||||
["structure_id"]
|
["structure_id"]
|
||||||
+ feat_columns
|
+ feat_columns
|
||||||
@ -334,7 +334,9 @@ class RuleBasedDataset(Dataset):
|
|||||||
obs.append(None)
|
obs.append(None)
|
||||||
else:
|
else:
|
||||||
obs.append(0)
|
obs.append(0)
|
||||||
|
|
||||||
rows.append([str(comp.uuid)] + feats + trig + obs)
|
rows.append([str(comp.uuid)] + feats + trig + obs)
|
||||||
|
|
||||||
ds = RuleBasedDataset(len(applicable_rules), ds_columns, data=rows)
|
ds = RuleBasedDataset(len(applicable_rules), ds_columns, data=rows)
|
||||||
return ds
|
return ds
|
||||||
|
|
||||||
@ -680,9 +682,13 @@ class RelativeReasoning:
|
|||||||
|
|
||||||
def predict(self, X):
|
def predict(self, X):
|
||||||
res = np.zeros((len(X), (self.end_index + 1 - self.start_index)))
|
res = np.zeros((len(X), (self.end_index + 1 - self.start_index)))
|
||||||
|
immutable_res = np.zeros((len(X), (self.end_index + 1 - self.start_index)))
|
||||||
|
|
||||||
# Loop through all instances
|
# Loop through all instances
|
||||||
for inst_idx, inst in enumerate(X):
|
for inst_idx, inst in enumerate(X):
|
||||||
|
for i, t in enumerate(inst[self.start_index : self.end_index + 1]):
|
||||||
|
immutable_res[inst_idx][i] = t
|
||||||
|
|
||||||
# Loop through all "triggered" features
|
# Loop through all "triggered" features
|
||||||
for i, t in enumerate(inst[self.start_index : self.end_index + 1]):
|
for i, t in enumerate(inst[self.start_index : self.end_index + 1]):
|
||||||
# Set label
|
# Set label
|
||||||
@ -696,7 +702,7 @@ class RelativeReasoning:
|
|||||||
if i2 in self.winmap.get(i, []):
|
if i2 in self.winmap.get(i, []):
|
||||||
# if thatat rule also triggered, it dominated the current
|
# if thatat rule also triggered, it dominated the current
|
||||||
# set label to 0
|
# set label to 0
|
||||||
if X[inst_idx][i2]:
|
if immutable_res[inst_idx][i2]:
|
||||||
res[inst_idx][i] = 0
|
res[inst_idx][i] = 0
|
||||||
|
|
||||||
return res
|
return res
|
||||||
|
|||||||
Reference in New Issue
Block a user