[Feature] PEPPER in enviPath (#332)

Co-authored-by: Tim Lorsbach <tim@lorsba.ch>
Reviewed-on: enviPath/enviPy#332
This commit is contained in:
2026-03-06 22:11:22 +13:00
parent 6e00926371
commit c6ff97694d
43 changed files with 3793 additions and 371 deletions

View File

@ -1,7 +1,7 @@
import json
import logging
from datetime import datetime
from typing import Any, Dict, List
from typing import Any, Dict, List, Iterable
import nh3
from django.conf import settings as s
@ -28,6 +28,7 @@ from .logic import (
UserManager,
)
from .models import (
AdditionalInformation,
APIToken,
Compound,
CompoundStructure,
@ -46,6 +47,7 @@ from .models import (
Node,
Pathway,
Permission,
PropertyPluginModel,
Reaction,
Rule,
RuleBasedRelativeReasoning,
@ -401,7 +403,7 @@ def breadcrumbs(
def set_scenarios(current_user, attach_object, scenario_urls: List[str]):
scens = []
for scenario_url in scenario_urls:
# As empty lists will be removed in POST request well send ['']
# As empty lists will be removed in POST request we'll send ['']
if scenario_url == "":
continue
@ -413,6 +415,7 @@ def set_scenarios(current_user, attach_object, scenario_urls: List[str]):
def set_aliases(current_user, attach_object, aliases: List[str]):
# As empty lists will be removed in POST request we'll send ['']
if aliases == [""]:
aliases = []
@ -421,7 +424,7 @@ def set_aliases(current_user, attach_object, aliases: List[str]):
def copy_object(current_user, target_package: "Package", source_object_url: str):
# Ensures that source is readable
# Ensures that source object is readable
source_package = PackageManager.get_package_by_url(current_user, source_object_url)
if source_package == target_package:
@ -429,7 +432,7 @@ def copy_object(current_user, target_package: "Package", source_object_url: str)
parser = EPDBURLParser(source_object_url)
# if the url won't contain a package or is a plain package
# if the url don't contain a package or is a plain package
if not parser.contains_package_url():
raise ValueError(f"Object {source_object_url} can't be copied!")
@ -714,12 +717,36 @@ def models(request):
# Keep model_types for potential modal/action use
context["model_types"] = {
"ML Relative Reasoning": "ml-relative-reasoning",
"Rule Based Relative Reasoning": "rule-based-relative-reasoning",
"EnviFormer": "enviformer",
"ML Relative Reasoning": {
"type": "ml-relative-reasoning",
"requires_rule_packages": True,
"requires_data_packages": True,
},
"Rule Based Relative Reasoning": {
"type": "rule-based-relative-reasoning",
"requires_rule_packages": True,
"requires_data_packages": True,
},
"EnviFormer": {
"type": "enviformer",
"requires_rule_packages": False,
"requires_data_packages": True,
},
}
for k, v in s.CLASSIFIER_PLUGINS.items():
context["model_types"][v.display()] = k
if s.FLAGS.get("PLUGINS", False):
for k, v in s.CLASSIFIER_PLUGINS.items():
context["model_types"][v().display()] = {
"type": k,
"requires_rule_packages": True,
"requires_data_packages": True,
}
for k, v in s.PROPERTY_PLUGINS.items():
context["model_types"][v().display()] = {
"type": k,
"requires_rule_packages": v().requires_rule_packages,
"requires_data_packages": v().requires_data_packages,
}
# Context for paginated template
context["entity_type"] = "model"
@ -830,16 +857,36 @@ def package_models(request, package_uuid):
)
context["model_types"] = {
"ML Relative Reasoning": "mlrr",
"Rule Based Relative Reasoning": "rbrr",
"ML Relative Reasoning": {
"type": "ml-relative-reasoning",
"requires_rule_packages": True,
"requires_data_packages": True,
},
"Rule Based Relative Reasoning": {
"type": "rule-based-relative-reasoning",
"requires_rule_packages": True,
"requires_data_packages": True,
},
"EnviFormer": {
"type": "enviformer",
"requires_rule_packages": False,
"requires_data_packages": True,
},
}
if s.FLAGS.get("ENVIFORMER", False):
context["model_types"]["EnviFormer"] = "enviformer"
if s.FLAGS.get("PLUGINS", False):
for k, v in s.CLASSIFIER_PLUGINS.items():
context["model_types"][v.display()] = k
context["model_types"][v().display()] = {
"type": k,
"requires_rule_packages": True,
"requires_data_packages": True,
}
for k, v in s.PROPERTY_PLUGINS.items():
context["model_types"][v().display()] = {
"type": k,
"requires_rule_packages": v().requires_rule_packages,
"requires_data_packages": v().requires_data_packages,
}
return render(request, "collections/models_paginated.html", context)
@ -900,8 +947,24 @@ def package_models(request, package_uuid):
]
mod = RuleBasedRelativeReasoning.create(**params)
elif s.FLAGS.get("PLUGINS", False) and model_type in s.CLASSIFIER_PLUGINS.values():
elif s.FLAGS.get("PLUGINS", False) and model_type in s.CLASSIFIER_PLUGINS:
pass
elif s.FLAGS.get("PLUGINS", False) and model_type in s.PROPERTY_PLUGINS:
params["plugin_identifier"] = model_type
impl = s.PROPERTY_PLUGINS[model_type]
inst = impl()
if inst.requires_rule_packages():
params["rule_packages"] = [
PackageManager.get_package_by_url(current_user, p) for p in rule_packages
]
else:
params["rule_packages"] = []
if not inst.requires_data_packages():
del params["data_packages"]
mod = PropertyPluginModel.create(**params)
else:
return error(
request, "Invalid model type.", f'Model type "{model_type}" is not supported."'
@ -925,14 +988,18 @@ def package_model(request, package_uuid, model_uuid):
if request.method == "GET":
classify = request.GET.get("classify", False)
ad_assessment = request.GET.get("app-domain-assessment", False)
# TODO this needs to be generic
half_life = request.GET.get("half_life", False)
if classify or ad_assessment:
if any([classify, ad_assessment, half_life]):
smiles = request.GET.get("smiles", "").strip()
# 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, remove_stereo=True)
except ValueError:
@ -966,6 +1033,19 @@ def package_model(request, package_uuid, model_uuid):
return JsonResponse(res, safe=False)
elif half_life:
from epdb.tasks import dispatch_eager, predict_simple
_, run_res = dispatch_eager(
current_user, predict_simple, current_model.pk, stand_smiles, include_svg=True
)
# Here we expect a single result
if isinstance(run_res.result, Iterable):
return JsonResponse(run_res.result[0].model_dump(mode="json"), safe=False)
return JsonResponse(run_res.result.model_dump(mode="json"), safe=False)
else:
app_domain_assessment = current_model.app_domain.assess(stand_smiles)
return JsonResponse(app_domain_assessment, safe=False)
@ -980,7 +1060,11 @@ def package_model(request, package_uuid, model_uuid):
context["model"] = current_model
context["current_object"] = current_model
return render(request, "objects/model.html", context)
if isinstance(current_model, PropertyPluginModel):
context["plugin_identifier"] = current_model.plugin_identifier
return render(request, "objects/model/property_model.html", context)
else:
return render(request, "objects/model/classification_model.html", context)
elif request.method == "POST":
if hidden := request.POST.get("hidden", None):
@ -1940,6 +2024,7 @@ 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,
@ -2504,8 +2589,10 @@ def package_scenario(request, package_uuid, scenario_uuid):
context["breadcrumbs"] = breadcrumbs(current_package, "scenario", current_scenario)
context["scenario"] = current_scenario
# Get scenarios that have current_scenario as a parent
context["children"] = current_scenario.scenario_set.order_by("name")
context["associated_additional_information"] = AdditionalInformation.objects.filter(
scenario=current_scenario
)
# Note: Modals now fetch schemas and data from API endpoints
# Keeping these for backwards compatibility if needed elsewhere
@ -2612,11 +2699,22 @@ def user(request, user_uuid):
context["user"] = requested_user
model_qs = EPModel.objects.none()
for p in PackageManager.get_all_readable_packages(requested_user, include_reviewed=True):
model_qs |= p.models
accessible_packages = PackageManager.get_all_readable_packages(
requested_user, include_reviewed=True
)
context["models"] = model_qs
property_models = PropertyPluginModel.objects.filter(
package__in=accessible_packages
).order_by("name")
tp_prediction_models = (
EPModel.objects.filter(package__in=accessible_packages)
.exclude(id__in=[pm.id for pm in property_models])
.order_by("name")
)
context["models"] = tp_prediction_models
context["property_models"] = property_models
context["tokens"] = APIToken.objects.filter(user=requested_user)
@ -2853,6 +2951,18 @@ def settings(request):
else:
raise BadRequest("Neither Model-Based nor Rule-Based as Method selected!")
property_model_urls = request.POST.getlist("prediction-setting-property-models")
if property_model_urls:
mods = []
for pm_url in property_model_urls:
model = PropertyPluginModel.objects.get(url=pm_url)
if PackageManager.readable(current_user, model.package):
mods.append(model)
params["property_models"] = mods
created_setting = SettingManager.create_setting(
current_user,
name=name,