[Fix] Export IUCLID Properties, Show Model Params and Statistics, Adjust Batch Prediction Settings (#434)

Co-authored-by: Tim Lorsbach <tim@lorsba.ch>
Reviewed-on: enviPath/enviPy#434
This commit is contained in:
2026-07-30 08:30:18 +12:00
parent 7639b23e4e
commit 703f377b7f
11 changed files with 172 additions and 27 deletions

View File

@ -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:
"""

View File

@ -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(

View File

@ -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.

View File

@ -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,
},
)

View File

@ -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):

View File

@ -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 %}

View File

@ -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) {

View File

@ -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>

View File

@ -95,21 +95,23 @@
</div>
<!-- Other Prediction Settings -->
<div class="collapse-arrow bg-base-200 collapse">
<input type="checkbox" />
<div class="collapse-title text-xl font-medium">
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">
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 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>
{% endif %}
</div>
<script>

View File

@ -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,

View File

@ -134,7 +134,7 @@ class EnzymeExportSchema(RefEnzymeExportSchema):
class EnzymeRuleExportSchema(RefRuleExportSchema):
enzymes: List[EnzymeExportSchema] | None = None
enzymes: List[EnzymeExportSchema] = []
@staticmethod
def resolve_enzymes(obj):