forked from enviPath/enviPy
Compare commits
3 Commits
7632b3a029
...
032ebc30a2
| Author | SHA1 | Date | |
|---|---|---|---|
| 032ebc30a2 | |||
| 703f377b7f | |||
| 7639b23e4e |
@ -1,4 +1,5 @@
|
||||
import enum
|
||||
from typing import Any, Dict
|
||||
from abc import ABC, abstractmethod
|
||||
|
||||
from envipy_additional_information import EnviPyModel
|
||||
@ -69,6 +70,12 @@ class Plugin(ABC):
|
||||
|
||||
|
||||
class Property(Plugin):
|
||||
def parameters(self) -> Dict[str, Any]:
|
||||
"""
|
||||
Returns the parameters of the PropertyPlugin.
|
||||
"""
|
||||
return {}
|
||||
|
||||
@classmethod
|
||||
@abstractmethod
|
||||
def requires_rule_packages(cls) -> bool:
|
||||
@ -300,6 +307,12 @@ class Classifier(Plugin):
|
||||
"""
|
||||
pass
|
||||
|
||||
def parameters(self) -> Dict[str, Any]:
|
||||
"""
|
||||
Returns the parameters of the ClassifierPlugin.
|
||||
"""
|
||||
return {}
|
||||
|
||||
@abstractmethod
|
||||
def build(self, eP: EnviPyDTO, *args, **kwargs) -> BuildResult | None:
|
||||
"""
|
||||
|
||||
@ -56,7 +56,7 @@ def get_pathway_for_iuclid_export(user, pathway_uuid: UUID) -> PathwayExportDTO:
|
||||
|
||||
ai_for_node = []
|
||||
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_node.extend(ai_for_scenario)
|
||||
scenario_entries.append(
|
||||
|
||||
@ -46,6 +46,10 @@ from .models import (
|
||||
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):
|
||||
p = PackageManager.get_package_by_id(user, package_uuid)
|
||||
if not PackageManager.writable(user, p):
|
||||
@ -2291,3 +2295,42 @@ def predict(request, np: Form[NonPersistent]):
|
||||
return 403, {
|
||||
"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!"
|
||||
}
|
||||
|
||||
@ -2162,7 +2162,7 @@ class Pathway(EnviPathModel, AliasMixin, ScenarioMixin, AdditionalInformationMix
|
||||
|
||||
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):
|
||||
for e in edges:
|
||||
_row = row.copy()
|
||||
@ -2847,6 +2847,58 @@ class PackageBasedModel(EPModel):
|
||||
|
||||
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
|
||||
def applicable_rules(self) -> List["Rule"]:
|
||||
"""
|
||||
@ -3027,8 +3079,6 @@ class PackageBasedModel(EPModel):
|
||||
thresholds.append(np.float64(threshold))
|
||||
thresholds.sort()
|
||||
|
||||
logger.info(f"Thresholds: {thresholds}")
|
||||
|
||||
precision = {f"{t:.2f}": [] for t in thresholds}
|
||||
recall = {f"{t:.2f}": [] for t in thresholds}
|
||||
|
||||
@ -3077,14 +3127,17 @@ class PackageBasedModel(EPModel):
|
||||
for t in thresholds:
|
||||
for true, pred in zip(pathways, pred_pathways):
|
||||
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)
|
||||
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()}
|
||||
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 self.eval_packages.count() > 0:
|
||||
@ -4216,6 +4269,9 @@ class ClassifierPluginModel(PackageBasedModel):
|
||||
instance = impl(conf)
|
||||
return instance
|
||||
|
||||
def parameters(self):
|
||||
return self.instance().parameters()
|
||||
|
||||
def build_dataset(self):
|
||||
"""
|
||||
Required by general model contract but actual implementation resides in plugin.
|
||||
@ -4436,6 +4492,9 @@ class PropertyPluginModel(PackageBasedModel):
|
||||
instance = impl()
|
||||
return instance
|
||||
|
||||
def parameters(self):
|
||||
return self.instance().parameters()
|
||||
|
||||
def build_dataset(self):
|
||||
"""
|
||||
Required by general model contract but actual implementation resides in plugin.
|
||||
|
||||
@ -477,8 +477,7 @@ def batch_predict(
|
||||
limit=None,
|
||||
setting_overrides={
|
||||
"max_nodes": num_tps,
|
||||
"max_depth": num_tps,
|
||||
"model_threshold": 0.001,
|
||||
"model_threshold": 0.0,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@ -351,7 +351,8 @@ class PathwayMapper:
|
||||
|
||||
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:
|
||||
props.soil_type = ai.type.value
|
||||
elif isinstance(ai, SoilTexture2):
|
||||
|
||||
@ -117,6 +117,39 @@
|
||||
</div>
|
||||
</div>
|
||||
{% 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 %}
|
||||
|
||||
{% block usemodel %}
|
||||
|
||||
@ -313,6 +313,44 @@
|
||||
</div>
|
||||
</div>
|
||||
{% 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 %}
|
||||
<script>
|
||||
function makeChart(selector, data) {
|
||||
|
||||
@ -139,7 +139,7 @@
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Model</td>
|
||||
<td>{{ half_lifes.0.model }}</td>
|
||||
<td>{{ half_lifes.0.model.value }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
@ -535,7 +535,7 @@
|
||||
<input type="checkbox" />
|
||||
<div class="collapse-title text-xl font-medium">Setting</div>
|
||||
<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" %}
|
||||
{% endwith %}
|
||||
</div>
|
||||
|
||||
@ -95,6 +95,7 @@
|
||||
</div>
|
||||
|
||||
<!-- Other Prediction Settings -->
|
||||
{% if meta.available_settings|length > 1 %}
|
||||
<div class="collapse-arrow bg-base-200 collapse">
|
||||
<input type="checkbox" />
|
||||
<div class="collapse-title text-xl font-medium">
|
||||
@ -110,6 +111,7 @@
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<script>
|
||||
|
||||
@ -58,7 +58,7 @@ class MultiGenTest(TestCase):
|
||||
|
||||
self.assertEqual(
|
||||
pw.setting_with_overrides.max_depth,
|
||||
f"{num_tps} (this is an override for this particular pathway)",
|
||||
5,
|
||||
)
|
||||
self.assertEqual(
|
||||
pw.setting_with_overrides.max_nodes,
|
||||
|
||||
@ -4,11 +4,13 @@ import hmac
|
||||
import json
|
||||
import logging
|
||||
import uuid
|
||||
from abc import ABC, abstractmethod
|
||||
from collections import defaultdict
|
||||
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 envipy_additional_information import EnviPyModel, UIConfig
|
||||
from ninja import Schema
|
||||
from pydantic import HttpUrl, ValidationError
|
||||
|
||||
@ -132,7 +134,7 @@ class EnzymeExportSchema(RefEnzymeExportSchema):
|
||||
|
||||
|
||||
class EnzymeRuleExportSchema(RefRuleExportSchema):
|
||||
enzymes: List[EnzymeExportSchema] | None = None
|
||||
enzymes: List[EnzymeExportSchema] = []
|
||||
|
||||
@staticmethod
|
||||
def resolve_enzymes(obj):
|
||||
@ -277,15 +279,20 @@ class PackageExportSchema(Schema):
|
||||
return AdditionalInformation.objects.filter(package=obj)
|
||||
|
||||
|
||||
class PackageExporter:
|
||||
class Exporter(ABC):
|
||||
def __init__(self, package: Package):
|
||||
self._raw_package = package
|
||||
|
||||
def do_export(self):
|
||||
return PackageExporter._export_package_as_json(self._raw_package)
|
||||
return self._export()
|
||||
|
||||
@staticmethod
|
||||
def _export_package_as_json(package: Package) -> Dict[str, Any]:
|
||||
@abstractmethod
|
||||
def _export(self):
|
||||
pass
|
||||
|
||||
|
||||
class PackageExporter(Exporter):
|
||||
def _export(self) -> Dict[str, Any]:
|
||||
"""
|
||||
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
|
||||
"""
|
||||
|
||||
data = PackageExportSchema.from_orm(package)
|
||||
data = PackageExportSchema.from_orm(self._raw_package)
|
||||
|
||||
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:
|
||||
def __init__(self, package: Dict[str, Any], preserve_uuids: bool = False):
|
||||
self.preserve_uuids = preserve_uuids
|
||||
|
||||
Reference in New Issue
Block a user