diff --git a/.gitea/workflows/ci.yaml b/.gitea/workflows/ci.yaml
index 7de35661..5e897309 100644
--- a/.gitea/workflows/ci.yaml
+++ b/.gitea/workflows/ci.yaml
@@ -41,7 +41,7 @@ jobs:
EP_DATA_DIR: /opt/enviPy/
ALLOWED_HOSTS: 127.0.0.1,localhost
DEBUG: True
- LOG_LEVEL: DEBUG
+ LOG_LEVEL: INFO
MODEL_BUILDING_ENABLED: True
APPLICABILITY_DOMAIN_ENABLED: True
ENVIFORMER_PRESENT: True
diff --git a/epdb/migrations/0012_node_stereo_removed_pathway_predicted.py b/epdb/migrations/0012_node_stereo_removed_pathway_predicted.py
new file mode 100644
index 00000000..648090d7
--- /dev/null
+++ b/epdb/migrations/0012_node_stereo_removed_pathway_predicted.py
@@ -0,0 +1,22 @@
+# Generated by Django 5.2.7 on 2025-12-02 13:09
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+ dependencies = [
+ ("epdb", "0011_auto_20251111_1413"),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name="node",
+ name="stereo_removed",
+ field=models.BooleanField(default=False),
+ ),
+ migrations.AddField(
+ model_name="pathway",
+ name="predicted",
+ field=models.BooleanField(default=False),
+ ),
+ ]
diff --git a/epdb/models.py b/epdb/models.py
index 67a34829..cfac5d6a 100644
--- a/epdb/models.py
+++ b/epdb/models.py
@@ -1593,6 +1593,7 @@ class Pathway(EnviPathModel, AliasMixin, ScenarioMixin):
setting = models.ForeignKey(
"epdb.Setting", verbose_name="Setting", on_delete=models.CASCADE, null=True, blank=True
)
+ predicted = models.BooleanField(default=False, null=False)
@property
def root_nodes(self):
@@ -1804,6 +1805,7 @@ class Pathway(EnviPathModel, AliasMixin, ScenarioMixin):
smiles: str,
name: Optional[str] = None,
description: Optional[str] = None,
+ predicted: bool = False,
):
pw = Pathway()
pw.package = package
@@ -1816,6 +1818,7 @@ class Pathway(EnviPathModel, AliasMixin, ScenarioMixin):
pw.name = name
if description is not None and description.strip() != "":
pw.description = nh3.clean(description, tags=s.ALLOWED_HTML_TAGS).strip()
+ pw.predicted = predicted
pw.save()
try:
@@ -1946,6 +1949,7 @@ class Node(EnviPathModel, AliasMixin, ScenarioMixin):
)
out_edges = models.ManyToManyField("epdb.Edge", verbose_name="Outgoing Edges")
depth = models.IntegerField(verbose_name="Node depth", null=False, blank=False)
+ stereo_removed = models.BooleanField(default=False, null=False)
def _url(self):
return "{}/node/{}".format(self.pathway.url, self.uuid)
@@ -1955,6 +1959,7 @@ class Node(EnviPathModel, AliasMixin, ScenarioMixin):
return {
"depth": self.depth,
+ "stereo_removed": self.stereo_removed,
"url": self.url,
"node_label_id": self.default_node_label.url,
"image": f"{self.url}?image=svg",
@@ -1980,12 +1985,17 @@ class Node(EnviPathModel, AliasMixin, ScenarioMixin):
name: Optional[str] = None,
description: Optional[str] = None,
):
+ stereo_removed = False
+ if pathway.predicted and FormatConverter.has_stereo(smiles):
+ smiles = FormatConverter.standardize(smiles, remove_stereo=True)
+ stereo_removed = True
c = Compound.create(pathway.package, smiles, name=name, description=description)
if Node.objects.filter(pathway=pathway, default_node_label=c.default_structure).exists():
return Node.objects.get(pathway=pathway, default_node_label=c.default_structure)
n = Node()
+ n.stereo_removed = stereo_removed
n.pathway = pathway
n.depth = depth
diff --git a/epdb/views.py b/epdb/views.py
index accce6f1..335743d5 100644
--- a/epdb/views.py
+++ b/epdb/views.py
@@ -961,9 +961,9 @@ def package_model(request, package_uuid, model_uuid):
# Check if smiles is non empty and valid
if smiles == "":
return JsonResponse({"error": "Received empty SMILES"}, status=400)
-
+ stereo = FormatConverter.has_stereo(smiles)
try:
- stand_smiles = FormatConverter.standardize(smiles)
+ stand_smiles = FormatConverter.standardize(smiles, remove_stereo=True)
except ValueError:
return JsonResponse({"error": f'"{smiles}" is not a valid SMILES'}, status=400)
@@ -974,7 +974,7 @@ def package_model(request, package_uuid, model_uuid):
current_user, predict_simple, current_model.pk, stand_smiles
)
- res = []
+ res = {"pred": [], "stereo": stereo}
for pr in pred_res:
if len(pr) > 0:
@@ -983,7 +983,7 @@ def package_model(request, package_uuid, model_uuid):
logger.debug(f"Checking {prod_set}")
products.append(tuple([x for x in prod_set]))
- res.append(
+ res["pred"].append(
{
"products": list(set(products)),
"probability": pr.probability,
@@ -1931,7 +1931,6 @@ def package_pathways(request, package_uuid):
"Pathway prediction failed!",
"Pathway prediction failed due to missing or empty SMILES",
)
-
try:
stand_smiles = FormatConverter.standardize(smiles)
except ValueError:
@@ -1954,8 +1953,13 @@ def package_pathways(request, package_uuid):
prediction_setting = SettingManager.get_setting_by_url(current_user, prediction_setting)
else:
prediction_setting = current_user.prediction_settings()
-
- pw = Pathway.create(current_package, stand_smiles, name=name, description=description)
+ pw = Pathway.create(
+ current_package,
+ stand_smiles,
+ name=name,
+ description=description,
+ predicted=pw_mode in {"predict", "incremental"},
+ )
# set mode
pw.kv.update({"mode": pw_mode})
diff --git a/fixtures/test_fixtures_incl_model.jsonl.gz b/fixtures/test_fixtures_incl_model.jsonl.gz
index c09475ef..2f24cca5 100644
Binary files a/fixtures/test_fixtures_incl_model.jsonl.gz and b/fixtures/test_fixtures_incl_model.jsonl.gz differ
diff --git a/static/js/pw.js b/static/js/pw.js
index 8c7c49ba..cefabe1b 100644
--- a/static/js/pw.js
+++ b/static/js/pw.js
@@ -360,7 +360,11 @@ function draw(pathway, elem) {
}
function node_popup(n) {
- popupContent = "" + n.name + "
";
+ popupContent = "";
+ if (n.stereo_removed) {
+ popupContent += "Removed stereochemistry for prediction";
+ }
+ popupContent += "" + n.name + "
";
popupContent += "Depth " + n.depth + "
"
if (appDomainViewEnabled) {
diff --git a/templates/objects/model.html b/templates/objects/model.html
index 7693e3df..84524ac8 100644
--- a/templates/objects/model.html
+++ b/templates/objects/model.html
@@ -117,7 +117,9 @@