From 2d3dca6a75b071c2a78df9a9a8e2ae950da272de Mon Sep 17 00:00:00 2001 From: jebus Date: Thu, 13 Aug 2026 17:37:38 +1200 Subject: [PATCH] [Fix] Adjust Rules according to manual QA, Add missing Rule (#452) Co-authored-by: Tim Lorsbach Reviewed-on: https://git.envipath.com/enviPath/enviPy/pulls/452 --- epdb/migrations/0028_auto_20260812_0902.py | 113 ++++++++++++++++++ epdb/models.py | 12 +- epdb/views.py | 22 ++-- .../objects/model/classification_model.html | 4 +- tests/views/test_model_views.py | 6 +- 5 files changed, 140 insertions(+), 17 deletions(-) create mode 100644 epdb/migrations/0028_auto_20260812_0902.py diff --git a/epdb/migrations/0028_auto_20260812_0902.py b/epdb/migrations/0028_auto_20260812_0902.py new file mode 100644 index 00000000..afa4658c --- /dev/null +++ b/epdb/migrations/0028_auto_20260812_0902.py @@ -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), + ] diff --git a/epdb/models.py b/epdb/models.py index 33cf81e2..68bc3340 100644 --- a/epdb/models.py +++ b/epdb/models.py @@ -854,9 +854,15 @@ class Compound( @property def related_reactions(self): return ( - Reaction.objects.filter(package=self.package, educts__in=[self.default_structure]) - | Reaction.objects.filter(package=self.package, products__in=[self.default_structure]) - ).order_by("name") + ( + Reaction.objects.filter(package=self.package, educts__in=[self.default_structure]) + | Reaction.objects.filter( + package=self.package, products__in=[self.default_structure] + ) + ) + .distinct() + .order_by("name") + ) @property def related_nodes(self): diff --git a/epdb/views.py b/epdb/views.py index f3ad53cf..d8b23fd6 100644 --- a/epdb/views.py +++ b/epdb/views.py @@ -1080,19 +1080,23 @@ def package_model(request, package_uuid, model_uuid): for pr in pred_res: if len(pr) > 0: products = [] + for prod_set in pr.product_sets: logger.debug(f"Checking {prod_set}") products.append(tuple([x for x in prod_set])) - res["pred"].append( - { - "products": list(set(products)), - "probability": pr.probability, - "btrule": {k: getattr(pr.rule, k) for k in ["url", "name"]} - if pr.rule is not None - else None, - } - ) + products = list(set(products)) + + for prod in products: + res["pred"].append( + { + "products": list(prod), + "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 res["pred"] = sorted( diff --git a/templates/objects/model/classification_model.html b/templates/objects/model/classification_model.html index a68c3109..15adf4a3 100644 --- a/templates/objects/model/classification_model.html +++ b/templates/objects/model/classification_model.html @@ -109,12 +109,12 @@ res += "" + cnt + ""; res += "" + - data[transformation]["products"][0].join(", ") + + data[transformation]["products"].join(", ") + ""; res += "" + ""; res += "" + diff --git a/tests/views/test_model_views.py b/tests/views/test_model_views.py index dabf18c2..74962c20 100644 --- a/tests/views/test_model_views.py +++ b/tests/views/test_model_views.py @@ -47,7 +47,7 @@ class ModelViewTest(TestCase): 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, "btrule": { "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, "btrule": { "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, "btrule": { "url": "http://localhost:8000/package/1869d3f0-60bb-41fd-b6f8-afa75ffb09d3/simple-ambit-rule/27a3a353-0b66-4228-bd16-e407949e90df",