2 Commits

Author SHA1 Message Date
04f9c9252a ... 2025-11-04 10:15:06 +01:00
c2d45917ce clean up d3_json 2025-10-31 09:38:41 +01:00
65 changed files with 755 additions and 1284 deletions

View File

@ -16,5 +16,3 @@ POSTGRES_PORT=
# MAIL # MAIL
EMAIL_HOST_USER= EMAIL_HOST_USER=
EMAIL_HOST_PASSWORD= EMAIL_HOST_PASSWORD=
# MATOMO
MATOMO_SITE_ID

View File

@ -1,116 +0,0 @@
name: CI
on:
pull_request:
branches:
- develop
workflow_dispatch:
jobs:
test:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:16
env:
POSTGRES_USER: ${{ vars.POSTGRES_USER }}
POSTGRES_PASSWORD: ${{ secrets.POSTGRES_PASSWORD }}
POSTGRES_DB: ${{ vars.POSTGRES_DB }}
ports:
- ${{ vars.POSTGRES_PORT}}:5432
options: >-
--health-cmd="pg_isready -U postgres"
--health-interval=10s
--health-timeout=5s
--health-retries=5
#redis:
# image: redis:7
# ports:
# - 6379:6379
# options: >-
# --health-cmd "redis-cli ping"
# --health-interval=10s
# --health-timeout=5s
# --health-retries=5
env:
RUNNER_TOOL_CACHE: /toolcache
EP_DATA_DIR: /opt/enviPy/
ALLOWED_HOSTS: 127.0.0.1,localhost
DEBUG: True
LOG_LEVEL: DEBUG
MODEL_BUILDING_ENABLED: True
APPLICABILITY_DOMAIN_ENABLED: True
ENVIFORMER_PRESENT: True
ENVIFORMER_DEVICE: cpu
FLAG_CELERY_PRESENT: False
PLUGINS_ENABLED: True
SERVER_URL: http://localhost:8000
ADMIN_APPROVAL_REQUIRED: True
REGISTRATION_MANDATORY: True
LOG_DIR: ''
# DB
POSTGRES_SERVICE_NAME: postgres
POSTGRES_DB: ${{ vars.POSTGRES_DB }}
POSTGRES_USER: ${{ vars.POSTGRES_USER }}
POSTGRES_PASSWORD: ${{ secrets.POSTGRES_PASSWORD }}
POSTGRES_PORT: 5432
# SENTRY
SENTRY_ENABLED: False
# MS ENTRA
MS_ENTRA_ENABLED: False
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Install system tools via apt
run: |
sudo apt-get update
sudo apt-get install -y postgresql-client redis-tools openjdk-11-jre-headless
- name: Setup ssh
run: |
echo "${{ secrets.ENVIPY_CI_PRIVATE_KEY }}" > ~/.ssh/id_ed25519
chmod 600 ~/.ssh/id_ed25519
ssh-keyscan git.envipath.com >> ~/.ssh/known_hosts
eval $(ssh-agent -s)
ssh-add ~/.ssh/id_ed25519
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 10
- name: Use Node.js
uses: actions/setup-node@v4
with:
node-version: 20
cache: "pnpm"
- name: Install uv
uses: astral-sh/setup-uv@v6
with:
enable-cache: true
- name: Setup venv
run: |
uv sync --locked --all-extras --dev
- name: Wait for services
run: |
until pg_isready -h postgres -U postgres; do sleep 2; done
# until redis-cli -h redis ping; do sleep 2; done
- name: Run Django migrations
run: |
source .venv/bin/activate
python manage.py migrate --noinput
- name: Run Django tests
run: |
source .venv/bin/activate
python manage.py test tests --exclude-tag slow

View File

@ -92,8 +92,6 @@ TEMPLATES = [
}, },
] ]
ALLOWED_HTML_TAGS = {'b', 'i', 'u', 'br', 'em', 'mark', 'p', 's', 'strong'}
WSGI_APPLICATION = "envipath.wsgi.application" WSGI_APPLICATION = "envipath.wsgi.application"
# Database # Database
@ -359,6 +357,3 @@ if MS_ENTRA_ENABLED:
MS_ENTRA_AUTHORITY = f"https://login.microsoftonline.com/{MS_ENTRA_TENANT_ID}" MS_ENTRA_AUTHORITY = f"https://login.microsoftonline.com/{MS_ENTRA_TENANT_ID}"
MS_ENTRA_REDIRECT_URI = os.environ["MS_REDIRECT_URI"] MS_ENTRA_REDIRECT_URI = os.environ["MS_REDIRECT_URI"]
MS_ENTRA_SCOPES = os.environ.get("MS_SCOPES", "").split(",") MS_ENTRA_SCOPES = os.environ.get("MS_SCOPES", "").split(",")
# Site ID 10 -> beta.envipath.org
MATOMO_SITE_ID = os.environ.get("MATOMO_SITE_ID", "10")

View File

@ -4,7 +4,6 @@ import json
from typing import Union, List, Optional, Set, Dict, Any from typing import Union, List, Optional, Set, Dict, Any
from uuid import UUID from uuid import UUID
import nh3
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from django.db import transaction from django.db import transaction
from django.conf import settings as s from django.conf import settings as s
@ -186,12 +185,6 @@ class UserManager(object):
def create_user( def create_user(
username, email, password, set_setting=True, add_to_group=True, *args, **kwargs username, email, password, set_setting=True, add_to_group=True, *args, **kwargs
): ):
# Clean for potential XSS
clean_username = nh3.clean(username).strip()
clean_email = nh3.clean(email).strip()
if clean_username != username or clean_email != email:
# This will be caught by the try in view.py/register
raise ValueError("Invalid username or password")
# avoid circular import :S # avoid circular import :S
from .tasks import send_registration_mail from .tasks import send_registration_mail
@ -269,9 +262,8 @@ class GroupManager(object):
@staticmethod @staticmethod
def create_group(current_user, name, description): def create_group(current_user, name, description):
g = Group() g = Group()
# Clean for potential XSS g.name = name
g.name = nh3.clean(name, tags=s.ALLOWED_HTML_TAGS).strip() g.description = description
g.description = nh3.clean(description, tags=s.ALLOWED_HTML_TAGS).strip()
g.owner = current_user g.owner = current_user
g.save() g.save()
@ -526,13 +518,8 @@ class PackageManager(object):
@transaction.atomic @transaction.atomic
def create_package(current_user, name: str, description: str = None): def create_package(current_user, name: str, description: str = None):
p = Package() p = Package()
p.name = name
# Clean for potential XSS p.description = description
p.name = nh3.clean(name, tags=s.ALLOWED_HTML_TAGS).strip()
if description is not None and description.strip() != "":
p.description = nh3.clean(description.strip(), tags=s.ALLOWED_HTML_TAGS).strip()
p.save() p.save()
up = UserPackagePermission() up = UserPackagePermission()
@ -1107,29 +1094,28 @@ class SettingManager(object):
model: EPModel = None, model: EPModel = None,
model_threshold: float = None, model_threshold: float = None,
): ):
new_s = Setting() s = Setting()
# Clean for potential XSS s.name = name
new_s.name = nh3.clean(name, tags=s.ALLOWED_HTML_TAGS).strip() s.description = description
new_s.description = nh3.clean(description, tags=s.ALLOWED_HTML_TAGS).strip() s.max_nodes = max_nodes
new_s.max_nodes = max_nodes s.max_depth = max_depth
new_s.max_depth = max_depth s.model = model
new_s.model = model s.model_threshold = model_threshold
new_s.model_threshold = model_threshold
new_s.save() s.save()
if rule_packages is not None: if rule_packages is not None:
for r in rule_packages: for r in rule_packages:
new_s.rule_packages.add(r) s.rule_packages.add(r)
new_s.save() s.save()
usp = UserSettingPermission() usp = UserSettingPermission()
usp.user = user usp.user = user
usp.setting = new_s usp.setting = s
usp.permission = Permission.ALL[0] usp.permission = Permission.ALL[0]
usp.save() usp.save()
return new_s return s
@staticmethod @staticmethod
def get_default_setting(user: User): def get_default_setting(user: User):
@ -1558,7 +1544,7 @@ class SPathway(object):
if self.prediction_setting.model.app_domain: if self.prediction_setting.model.app_domain:
app_domain_assessment = self.prediction_setting.model.app_domain.assess( app_domain_assessment = self.prediction_setting.model.app_domain.assess(
sub.smiles sub.smiles
) )[0]
if self.persist is not None: if self.persist is not None:
n = self.snode_persist_lookup[sub] n = self.snode_persist_lookup[sub]
@ -1591,7 +1577,9 @@ class SPathway(object):
if self.prediction_setting.model: if self.prediction_setting.model:
if self.prediction_setting.model.app_domain: if self.prediction_setting.model.app_domain:
app_domain_assessment = ( app_domain_assessment = (
self.prediction_setting.model.app_domain.assess(c) self.prediction_setting.model.app_domain.assess(c)[
0
]
) )
self.smiles_to_node[c] = SNode( self.smiles_to_node[c] = SNode(

View File

@ -11,7 +11,6 @@ from typing import Union, List, Optional, Dict, Tuple, Set, Any
from uuid import uuid4 from uuid import uuid4
import math import math
import joblib import joblib
import nh3
import numpy as np import numpy as np
from django.conf import settings as s from django.conf import settings as s
from django.contrib.auth.models import AbstractUser from django.contrib.auth.models import AbstractUser
@ -29,14 +28,7 @@ from sklearn.metrics import precision_score, recall_score, jaccard_score
from sklearn.model_selection import ShuffleSplit from sklearn.model_selection import ShuffleSplit
from utilities.chem import FormatConverter, ProductSet, PredictionResult, IndigoUtils from utilities.chem import FormatConverter, ProductSet, PredictionResult, IndigoUtils
from utilities.ml import ( from utilities.ml import Dataset, ApplicabilityDomainPCA, EnsembleClassifierChain, RelativeReasoning
RuleBasedDataset,
ApplicabilityDomainPCA,
EnsembleClassifierChain,
RelativeReasoning,
EnviFormerDataset,
Dataset,
)
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -810,16 +802,14 @@ class Compound(EnviPathModel, AliasMixin, ScenarioMixin, ChemicalIdentifierMixin
c = Compound() c = Compound()
c.package = package c.package = package
if name is not None: if name is None or name.strip() == "":
# Clean for potential XSS
name = nh3.clean(name, tags=s.ALLOWED_HTML_TAGS).strip()
if name is None or name == "":
name = f"Compound {Compound.objects.filter(package=package).count() + 1}" name = f"Compound {Compound.objects.filter(package=package).count() + 1}"
c.name = name c.name = name
# We have a default here only set the value if it carries some payload # We have a default here only set the value if it carries some payload
if description is not None and description.strip() != "": if description is not None and description.strip() != "":
c.description = nh3.clean(description, tags=s.ALLOWED_HTML_TAGS).strip() c.description = description.strip()
c.save() c.save()
@ -991,11 +981,11 @@ class CompoundStructure(EnviPathModel, AliasMixin, ScenarioMixin, ChemicalIdenti
raise ValueError("Unpersisted Compound! Persist compound first!") raise ValueError("Unpersisted Compound! Persist compound first!")
cs = CompoundStructure() cs = CompoundStructure()
# Clean for potential XSS
if name is not None: if name is not None:
cs.name = nh3.clean(name, tags=s.ALLOWED_HTML_TAGS).strip() cs.name = name
if description is not None: if description is not None:
cs.description = nh3.clean(description, tags=s.ALLOWED_HTML_TAGS).strip() cs.description = description
cs.smiles = smiles cs.smiles = smiles
cs.compound = compound cs.compound = compound
@ -1197,29 +1187,21 @@ class SimpleAmbitRule(SimpleRule):
r = SimpleAmbitRule() r = SimpleAmbitRule()
r.package = package r.package = package
if name is not None: if name is None or name.strip() == "":
name = nh3.clean(name, tags=s.ALLOWED_HTML_TAGS).strip()
if name is None or name == "":
name = f"Rule {Rule.objects.filter(package=package).count() + 1}" name = f"Rule {Rule.objects.filter(package=package).count() + 1}"
r.name = name r.name = name
if description is not None and description.strip() != "": if description is not None and description.strip() != "":
r.description = nh3.clean(description, tags=s.ALLOWED_HTML_TAGS).strip() r.description = description
r.smirks = smirks r.smirks = smirks
if reactant_filter_smarts is not None and reactant_filter_smarts.strip() != "": if reactant_filter_smarts is not None and reactant_filter_smarts.strip() != "":
if not FormatConverter.is_valid_smarts(reactant_filter_smarts.strip()): r.reactant_filter_smarts = reactant_filter_smarts
raise ValueError(f'Reactant Filter SMARTS "{reactant_filter_smarts}" is invalid!')
else:
r.reactant_filter_smarts = reactant_filter_smarts.strip()
if product_filter_smarts is not None and product_filter_smarts.strip() != "": if product_filter_smarts is not None and product_filter_smarts.strip() != "":
if not FormatConverter.is_valid_smarts(product_filter_smarts.strip()): r.product_filter_smarts = product_filter_smarts
raise ValueError(f'Product Filter SMARTS "{product_filter_smarts}" is invalid!')
else:
r.product_filter_smarts = product_filter_smarts.strip()
r.save() r.save()
return r return r
@ -1420,11 +1402,12 @@ class Reaction(EnviPathModel, AliasMixin, ScenarioMixin, ReactionIdentifierMixin
r = Reaction() r = Reaction()
r.package = package r.package = package
# Clean for potential XSS
if name is not None and name.strip() != "": if name is not None and name.strip() != "":
r.name = nh3.clean(name, tags=s.ALLOWED_HTML_TAGS).strip() r.name = name
if description is not None and name.strip() != "": if description is not None and name.strip() != "":
r.description = nh3.clean(description, tags=s.ALLOWED_HTML_TAGS).strip() r.description = description
r.multi_step = multi_step r.multi_step = multi_step
@ -1590,14 +1573,12 @@ class Pathway(EnviPathModel, AliasMixin, ScenarioMixin):
while len(queue): while len(queue):
current = queue.pop() current = queue.pop()
processed.add(current) processed.add(current)
nodes.append(current.d3_json()) nodes.append(current.d3_json())
for e in self.edges: for e in self.edges.filter(start_nodes=current).distinct():
if current in e.start_nodes.all(): for prod in e.end_nodes.all():
for prod in e.end_nodes.all(): if prod not in queue and prod not in processed:
if prod not in queue and prod not in processed: queue.append(prod)
queue.append(prod)
# We shouldn't lose or make up nodes... # We shouldn't lose or make up nodes...
assert len(nodes) == len(self.nodes) assert len(nodes) == len(self.nodes)
@ -1732,15 +1713,14 @@ class Pathway(EnviPathModel, AliasMixin, ScenarioMixin):
): ):
pw = Pathway() pw = Pathway()
pw.package = package pw.package = package
if name is not None:
# Clean for potential XSS if name is None or name.strip() == "":
name = nh3.clean(name, tags=s.ALLOWED_HTML_TAGS).strip()
if name is None or name == "":
name = f"Pathway {Pathway.objects.filter(package=package).count() + 1}" name = f"Pathway {Pathway.objects.filter(package=package).count() + 1}"
pw.name = name pw.name = name
if description is not None and description.strip() != "": if description is not None and description.strip() != "":
pw.description = nh3.clean(description, tags=s.ALLOWED_HTML_TAGS).strip() pw.description = description
pw.save() pw.save()
try: try:
@ -2035,16 +2015,11 @@ class Edge(EnviPathModel, AliasMixin, ScenarioMixin):
for node in end_nodes: for node in end_nodes:
e.end_nodes.add(node) e.end_nodes.add(node)
# Clean for potential XSS if name is None:
# Cleaning technically not needed as it is also done in Reaction.create, including it here for consistency
if name is not None:
name = nh3.clean(name, tags=s.ALLOWED_HTML_TAGS).strip()
if name is None or name == "":
name = f"Reaction {pathway.package.reactions.count() + 1}" name = f"Reaction {pathway.package.reactions.count() + 1}"
if description is None: if description is None:
description = s.DEFAULT_VALUES["description"] description = s.DEFAULT_VALUES["description"]
description = nh3.clean(description, tags=s.ALLOWED_HTML_TAGS).strip()
r = Reaction.create( r = Reaction.create(
pathway.package, pathway.package,
@ -2198,7 +2173,7 @@ class PackageBasedModel(EPModel):
applicable_rules = self.applicable_rules applicable_rules = self.applicable_rules
reactions = list(self._get_reactions()) reactions = list(self._get_reactions())
ds = RuleBasedDataset.generate_dataset(reactions, applicable_rules, educts_only=True) ds = Dataset.generate_dataset(reactions, applicable_rules, educts_only=True)
end = datetime.now() end = datetime.now()
logger.debug(f"build_dataset took {(end - start).total_seconds()} seconds") logger.debug(f"build_dataset took {(end - start).total_seconds()} seconds")
@ -2207,7 +2182,7 @@ class PackageBasedModel(EPModel):
ds.save(f) ds.save(f)
return ds return ds
def load_dataset(self) -> "Dataset | RuleBasedDataset | EnviFormerDataset": def load_dataset(self) -> "Dataset":
ds_path = os.path.join(s.MODEL_DIR, f"{self.uuid}_ds.pkl") ds_path = os.path.join(s.MODEL_DIR, f"{self.uuid}_ds.pkl")
return Dataset.load(ds_path) return Dataset.load(ds_path)
@ -2248,7 +2223,7 @@ class PackageBasedModel(EPModel):
self.model_status = self.BUILT_NOT_EVALUATED self.model_status = self.BUILT_NOT_EVALUATED
self.save() self.save()
def evaluate_model(self, multigen: bool, eval_packages: List["Package"] = None, **kwargs): def evaluate_model(self, multigen: bool, eval_packages: List["Package"] = None):
if self.model_status != self.BUILT_NOT_EVALUATED: if self.model_status != self.BUILT_NOT_EVALUATED:
raise ValueError(f"Can't evaluate a model in state {self.model_status}!") raise ValueError(f"Can't evaluate a model in state {self.model_status}!")
@ -2366,39 +2341,37 @@ class PackageBasedModel(EPModel):
eval_reactions = list( eval_reactions = list(
Reaction.objects.filter(package__in=self.eval_packages.all()).distinct() Reaction.objects.filter(package__in=self.eval_packages.all()).distinct()
) )
ds = RuleBasedDataset.generate_dataset( ds = Dataset.generate_dataset(eval_reactions, self.applicable_rules, educts_only=True)
eval_reactions, self.applicable_rules, educts_only=True
)
if isinstance(self, RuleBasedRelativeReasoning): if isinstance(self, RuleBasedRelativeReasoning):
X = ds.X(exclude_id_col=False, na_replacement=None).to_numpy() X = np.array(ds.X(exclude_id_col=False, na_replacement=None))
y = ds.y(na_replacement=np.nan).to_numpy() y = np.array(ds.y(na_replacement=np.nan))
else: else:
X = ds.X(na_replacement=np.nan).to_numpy() X = np.array(ds.X(na_replacement=np.nan))
y = ds.y(na_replacement=np.nan).to_numpy() y = np.array(ds.y(na_replacement=np.nan))
single_gen_result = evaluate_sg(self.model, X, y, np.arange(len(X)), self.threshold) single_gen_result = evaluate_sg(self.model, X, y, np.arange(len(X)), self.threshold)
self.eval_results = self.compute_averages([single_gen_result]) self.eval_results = self.compute_averages([single_gen_result])
else: else:
ds = self.load_dataset() ds = self.load_dataset()
if isinstance(self, RuleBasedRelativeReasoning): if isinstance(self, RuleBasedRelativeReasoning):
X = ds.X(exclude_id_col=False, na_replacement=None).to_numpy() X = np.array(ds.X(exclude_id_col=False, na_replacement=None))
y = ds.y(na_replacement=np.nan).to_numpy() y = np.array(ds.y(na_replacement=np.nan))
else: else:
X = ds.X(na_replacement=np.nan).to_numpy() X = np.array(ds.X(na_replacement=np.nan))
y = ds.y(na_replacement=np.nan).to_numpy() y = np.array(ds.y(na_replacement=np.nan))
n_splits = kwargs.get("n_splits", 20) n_splits = 20
shuff = ShuffleSplit(n_splits=n_splits, test_size=0.25, random_state=42) shuff = ShuffleSplit(n_splits=n_splits, test_size=0.25, random_state=42)
splits = list(shuff.split(X)) splits = list(shuff.split(X))
from joblib import Parallel, delayed from joblib import Parallel, delayed
models = Parallel(n_jobs=min(10, len(splits)))( models = Parallel(n_jobs=10)(
delayed(train_func)(X, y, train_index, self._model_args()) delayed(train_func)(X, y, train_index, self._model_args())
for train_index, _ in splits for train_index, _ in splits
) )
evaluations = Parallel(n_jobs=min(10, len(splits)))( evaluations = Parallel(n_jobs=10)(
delayed(evaluate_sg)(model, X, y, test_index, self.threshold) delayed(evaluate_sg)(model, X, y, test_index, self.threshold)
for model, (_, test_index) in zip(models, splits) for model, (_, test_index) in zip(models, splits)
) )
@ -2566,15 +2539,14 @@ class RuleBasedRelativeReasoning(PackageBasedModel):
): ):
rbrr = RuleBasedRelativeReasoning() rbrr = RuleBasedRelativeReasoning()
rbrr.package = package rbrr.package = package
if name is not None:
# Clean for potential XSS if name is None or name.strip() == "":
name = nh3.clean(name, tags=s.ALLOWED_HTML_TAGS).strip()
if name is None or name == "":
name = f"RuleBasedRelativeReasoning {RuleBasedRelativeReasoning.objects.filter(package=package).count() + 1}" name = f"RuleBasedRelativeReasoning {RuleBasedRelativeReasoning.objects.filter(package=package).count() + 1}"
rbrr.name = name rbrr.name = name
if description is not None and description.strip() != "": if description is not None and description.strip() != "":
rbrr.description = nh3.clean(description, tags=s.ALLOWED_HTML_TAGS).strip() rbrr.description = description
if threshold is None or (threshold <= 0 or 1 <= threshold): if threshold is None or (threshold <= 0 or 1 <= threshold):
raise ValueError("Threshold must be a float between 0 and 1.") raise ValueError("Threshold must be a float between 0 and 1.")
@ -2611,11 +2583,11 @@ class RuleBasedRelativeReasoning(PackageBasedModel):
return rbrr return rbrr
def _fit_model(self, ds: RuleBasedDataset): def _fit_model(self, ds: Dataset):
X, y = ds.X(exclude_id_col=False, na_replacement=None), ds.y(na_replacement=None) X, y = ds.X(exclude_id_col=False, na_replacement=None), ds.y(na_replacement=None)
model = RelativeReasoning( model = RelativeReasoning(
start_index=ds.triggered()[0], start_index=ds.triggered()[0],
end_index=ds.triggered()[-1], end_index=ds.triggered()[1],
) )
model.fit(X, y) model.fit(X, y)
return model return model
@ -2625,7 +2597,7 @@ class RuleBasedRelativeReasoning(PackageBasedModel):
return { return {
"clz": "RuleBaseRelativeReasoning", "clz": "RuleBaseRelativeReasoning",
"start_index": ds.triggered()[0], "start_index": ds.triggered()[0],
"end_index": ds.triggered()[-1], "end_index": ds.triggered()[1],
} }
def _save_model(self, model): def _save_model(self, model):
@ -2671,15 +2643,14 @@ class MLRelativeReasoning(PackageBasedModel):
): ):
mlrr = MLRelativeReasoning() mlrr = MLRelativeReasoning()
mlrr.package = package mlrr.package = package
if name is not None:
# Clean for potential XSS if name is None or name.strip() == "":
name = nh3.clean(name, tags=s.ALLOWED_HTML_TAGS).strip()
if name is None or name == "":
name = f"MLRelativeReasoning {MLRelativeReasoning.objects.filter(package=package).count() + 1}" name = f"MLRelativeReasoning {MLRelativeReasoning.objects.filter(package=package).count() + 1}"
mlrr.name = name mlrr.name = name
if description is not None and description.strip() != "": if description is not None and description.strip() != "":
mlrr.description = nh3.clean(description, tags=s.ALLOWED_HTML_TAGS).strip() mlrr.description = description
if threshold is None or (threshold <= 0 or 1 <= threshold): if threshold is None or (threshold <= 0 or 1 <= threshold):
raise ValueError("Threshold must be a float between 0 and 1.") raise ValueError("Threshold must be a float between 0 and 1.")
@ -2714,11 +2685,11 @@ class MLRelativeReasoning(PackageBasedModel):
return mlrr return mlrr
def _fit_model(self, ds: RuleBasedDataset): def _fit_model(self, ds: Dataset):
X, y = ds.X(na_replacement=np.nan), ds.y(na_replacement=np.nan) X, y = ds.X(na_replacement=np.nan), ds.y(na_replacement=np.nan)
model = EnsembleClassifierChain(**s.DEFAULT_MODEL_PARAMS) model = EnsembleClassifierChain(**s.DEFAULT_MODEL_PARAMS)
model.fit(X.to_numpy(), y.to_numpy()) model.fit(X, y)
return model return model
def _model_args(self): def _model_args(self):
@ -2741,7 +2712,7 @@ class MLRelativeReasoning(PackageBasedModel):
start = datetime.now() start = datetime.now()
ds = self.load_dataset() ds = self.load_dataset()
classify_ds, classify_prods = ds.classification_dataset([smiles], self.applicable_rules) classify_ds, classify_prods = ds.classification_dataset([smiles], self.applicable_rules)
pred = self.model.predict_proba(classify_ds.X().to_numpy()) pred = self.model.predict_proba(classify_ds.X())
res = MLRelativeReasoning.combine_products_and_probs( res = MLRelativeReasoning.combine_products_and_probs(
self.applicable_rules, pred[0], classify_prods[0] self.applicable_rules, pred[0], classify_prods[0]
@ -2786,9 +2757,7 @@ class ApplicabilityDomain(EnviPathModel):
@cached_property @cached_property
def training_set_probs(self): def training_set_probs(self):
ds = self.model.load_dataset() return joblib.load(os.path.join(s.MODEL_DIR, f"{self.model.uuid}_train_probs.pkl"))
col_ids = ds.block_indices("prob")
return ds[:, col_ids]
def build(self): def build(self):
ds = self.model.load_dataset() ds = self.model.load_dataset()
@ -2796,9 +2765,9 @@ class ApplicabilityDomain(EnviPathModel):
start = datetime.now() start = datetime.now()
# Get Trainingset probs and dump them as they're required when using the app domain # Get Trainingset probs and dump them as they're required when using the app domain
probs = self.model.model.predict_proba(ds.X().to_numpy()) probs = self.model.model.predict_proba(ds.X())
ds.add_probs(probs) f = os.path.join(s.MODEL_DIR, f"{self.model.uuid}_train_probs.pkl")
ds.save(os.path.join(s.MODEL_DIR, f"{self.model.uuid}_ds.pkl")) joblib.dump(probs, f)
ad = ApplicabilityDomainPCA(num_neighbours=self.num_neighbours) ad = ApplicabilityDomainPCA(num_neighbours=self.num_neighbours)
ad.build(ds) ad.build(ds)
@ -2821,20 +2790,15 @@ class ApplicabilityDomain(EnviPathModel):
joblib.dump(ad, f) joblib.dump(ad, f)
def assess(self, structure: Union[str, "CompoundStructure"]): def assess(self, structure: Union[str, "CompoundStructure"]):
return self.assess_batch([structure])[0]
def assess_batch(self, structures: List["CompoundStructure | str"]):
ds = self.model.load_dataset() ds = self.model.load_dataset()
smiles = [] if isinstance(structure, CompoundStructure):
for struct in structures: smiles = structure.smiles
if isinstance(struct, CompoundStructure): else:
smiles.append(structures.smiles) smiles = structure
else:
smiles.append(structures)
assessment_ds, assessment_prods = ds.classification_dataset( assessment_ds, assessment_prods = ds.classification_dataset(
structures, self.model.applicable_rules [structure], self.model.applicable_rules
) )
# qualified_neighbours_per_rule is a nested dictionary structured as: # qualified_neighbours_per_rule is a nested dictionary structured as:
@ -2848,61 +2812,82 @@ class ApplicabilityDomain(EnviPathModel):
# it identifies all training structures that have the same trigger reaction activated (i.e., value 1). # it identifies all training structures that have the same trigger reaction activated (i.e., value 1).
# This is used to find "qualified neighbours" — training examples that share the same triggered feature # This is used to find "qualified neighbours" — training examples that share the same triggered feature
# with a given assessment structure under a particular rule. # with a given assessment structure under a particular rule.
qualified_neighbours_per_rule: Dict = {} qualified_neighbours_per_rule: Dict[int, Dict[int, List[int]]] = defaultdict(
lambda: defaultdict(list)
)
import polars as pl for rule_idx, feature_index in enumerate(range(*assessment_ds.triggered())):
feature = ds.columns[feature_index]
if feature.startswith("trig_"):
# TODO unroll loop
for i, cx in enumerate(assessment_ds.X(exclude_id_col=False)):
if int(cx[feature_index]) == 1:
for j, tx in enumerate(ds.X(exclude_id_col=False)):
if int(tx[feature_index]) == 1:
qualified_neighbours_per_rule[i][rule_idx].append(j)
# Select only the triggered columns probs = self.training_set_probs
for i, row in enumerate(assessment_ds[:, assessment_ds.triggered()].iter_rows(named=True)): # preds = self.model.model.predict_proba(assessment_ds.X())
# Find the rules the structure triggers. For each rule, filter the training dataset to rows that also
# trigger that rule.
train_trig = {
trig_uuid.split("_")[-1]: ds.filter(pl.col(trig_uuid).eq(1))
for trig_uuid, value in row.items()
if value == 1
}
qualified_neighbours_per_rule[i] = train_trig
rule_to_i = {str(r.uuid): i for i, r in enumerate(self.model.applicable_rules)}
preds = self.model.combine_products_and_probs( preds = self.model.combine_products_and_probs(
self.model.applicable_rules, self.model.applicable_rules,
self.model.model.predict_proba(assessment_ds.X().to_numpy())[0], self.model.model.predict_proba(assessment_ds.X())[0],
assessment_prods[0], assessment_prods[0],
) )
assessments = list() assessments = list()
# loop through our assessment dataset # loop through our assessment dataset
for i, instance in enumerate(assessment_ds[:, assessment_ds.struct_features()]): for i, instance in enumerate(assessment_ds):
rule_reliabilities = dict() rule_reliabilities = dict()
local_compatibilities = dict() local_compatibilities = dict()
neighbours_per_rule = dict() neighbours_per_rule = dict()
neighbor_probs_per_rule = dict() neighbor_probs_per_rule = dict()
# loop through rule indices together with the collected neighbours indices from train dataset # loop through rule indices together with the collected neighbours indices from train dataset
for rule_uuid, train_instances in qualified_neighbours_per_rule[i].items(): for rule_idx, vals in qualified_neighbours_per_rule[i].items():
# compute tanimoto distance for all neighbours and add to dataset # collect the train dataset instances and store it along with the index (a.k.a. row number) of the
# train dataset
train_instances = []
for v in vals:
train_instances.append((v, ds.at(v)))
# sf is a tuple with start/end index of the features
sf = ds.struct_features()
# compute tanimoto distance for all neighbours
# result ist a list of tuples with train index and computed distance
dists = self._compute_distances( dists = self._compute_distances(
assessment_ds[i, assessment_ds.struct_features()].to_numpy()[0], instance.X()[0][sf[0] : sf[1]],
train_instances[:, train_instances.struct_features()].to_numpy(), [ti[1].X()[0][sf[0] : sf[1]] for ti in train_instances],
) )
train_instances = train_instances.with_columns(dist=pl.Series(dists))
dists_with_index = list()
for ti, dist in zip(train_instances, dists):
dists_with_index.append((ti[0], dist[1]))
# sort them in a descending way and take at most `self.num_neighbours` # sort them in a descending way and take at most `self.num_neighbours`
# TODO: Should this be descending? If we want the most similar then we want values close to zero (ascending) dists_with_index = sorted(dists_with_index, key=lambda x: x[1], reverse=True)
train_instances = train_instances.sort("dist", descending=True)[ dists_with_index = dists_with_index[: self.num_neighbours]
: self.num_neighbours
]
# compute average distance # compute average distance
rule_reliabilities[rule_uuid] = ( rule_reliabilities[rule_idx] = (
train_instances.select(pl.mean("dist")).fill_nan(0.0).item() sum([d[1] for d in dists_with_index]) / len(dists_with_index)
if len(dists_with_index) > 0
else 0.0
) )
# for local_compatibility we'll need the datasets for the indices having the highest similarity # for local_compatibility we'll need the datasets for the indices having the highest similarity
local_compatibilities[rule_uuid] = self._compute_compatibility( neighbour_datasets = [(d[0], ds.at(d[0])) for d in dists_with_index]
rule_uuid, train_instances local_compatibilities[rule_idx] = self._compute_compatibility(
rule_idx, probs, neighbour_datasets
) )
neighbours_per_rule[rule_uuid] = list( neighbours_per_rule[rule_idx] = [
CompoundStructure.objects.filter(uuid__in=train_instances["structure_id"]) CompoundStructure.objects.get(uuid=ds[1].structure_id())
) for ds in neighbour_datasets
neighbor_probs_per_rule[rule_uuid] = train_instances[f"prob_{rule_uuid}"].to_list() ]
neighbor_probs_per_rule[rule_idx] = [
probs[d[0]][rule_idx] for d in dists_with_index
]
ad_res = { ad_res = {
"ad_params": { "ad_params": {
@ -2913,21 +2898,23 @@ class ApplicabilityDomain(EnviPathModel):
"local_compatibility_threshold": self.local_compatibilty_threshold, "local_compatibility_threshold": self.local_compatibilty_threshold,
}, },
"assessment": { "assessment": {
"smiles": smiles[i], "smiles": smiles,
"inside_app_domain": self.pca.is_applicable(assessment_ds[i])[0], "inside_app_domain": self.pca.is_applicable(instance)[0],
}, },
} }
transformations = list() transformations = list()
for rule_uuid in rule_reliabilities.keys(): for rule_idx in rule_reliabilities.keys():
rule = Rule.objects.get(uuid=rule_uuid) rule = Rule.objects.get(
uuid=instance.columns[instance.observed()[0] + rule_idx].replace("obs_", "")
)
rule_data = rule.simple_json() rule_data = rule.simple_json()
rule_data["image"] = f"{rule.url}?image=svg" rule_data["image"] = f"{rule.url}?image=svg"
neighbors = [] neighbors = []
for n, n_prob in zip( for n, n_prob in zip(
neighbours_per_rule[rule_uuid], neighbor_probs_per_rule[rule_uuid] neighbours_per_rule[rule_idx], neighbor_probs_per_rule[rule_idx]
): ):
neighbor = n.simple_json() neighbor = n.simple_json()
neighbor["image"] = f"{n.url}?image=svg" neighbor["image"] = f"{n.url}?image=svg"
@ -2944,14 +2931,14 @@ class ApplicabilityDomain(EnviPathModel):
transformation = { transformation = {
"rule": rule_data, "rule": rule_data,
"reliability": rule_reliabilities[rule_uuid], "reliability": rule_reliabilities[rule_idx],
# We're setting it here to False, as we don't know whether "assess" is called during pathway # We're setting it here to False, as we don't know whether "assess" is called during pathway
# prediction or from Model Page. For persisted Nodes this field will be overwritten at runtime # prediction or from Model Page. For persisted Nodes this field will be overwritten at runtime
"is_predicted": False, "is_predicted": False,
"local_compatibility": local_compatibilities[rule_uuid], "local_compatibility": local_compatibilities[rule_idx],
"probability": preds[rule_to_i[rule_uuid]].probability, "probability": preds[rule_idx].probability,
"transformation_products": [ "transformation_products": [
x.product_set for x in preds[rule_to_i[rule_uuid]].product_sets x.product_set for x in preds[rule_idx].product_sets
], ],
"times_triggered": ds.times_triggered(str(rule.uuid)), "times_triggered": ds.times_triggered(str(rule.uuid)),
"neighbors": neighbors, "neighbors": neighbors,
@ -2969,24 +2956,32 @@ class ApplicabilityDomain(EnviPathModel):
def _compute_distances(classify_instance: List[int], train_instances: List[List[int]]): def _compute_distances(classify_instance: List[int], train_instances: List[List[int]]):
from utilities.ml import tanimoto_distance from utilities.ml import tanimoto_distance
distances = [tanimoto_distance(classify_instance, train) for train in train_instances] distances = [
(i, tanimoto_distance(classify_instance, train))
for i, train in enumerate(train_instances)
]
return distances return distances
def _compute_compatibility(self, rule_idx: int, neighbours: "RuleBasedDataset"): @staticmethod
def _compute_compatibility(rule_idx: int, preds, neighbours: List[Tuple[int, "Dataset"]]):
tp, tn, fp, fn = 0.0, 0.0, 0.0, 0.0
accuracy = 0.0 accuracy = 0.0
import polars as pl
obs_pred = neighbours.select( for n in neighbours:
obs=pl.col(f"obs_{rule_idx}").cast(pl.Boolean), obs = n[1].y()[0][rule_idx]
pred=pl.col(f"prob_{rule_idx}") >= self.model.threshold, pred = preds[n[0]][rule_idx]
) if obs and pred:
# Compute tp, tn, fp, fn using polars expressions tp += 1
tp = obs_pred.filter((pl.col("obs")) & (pl.col("pred"))).height elif not obs and pred:
tn = obs_pred.filter((~pl.col("obs")) & (~pl.col("pred"))).height fp += 1
fp = obs_pred.filter((~pl.col("obs")) & (pl.col("pred"))).height elif obs and not pred:
fn = obs_pred.filter((pl.col("obs")) & (~pl.col("pred"))).height fn += 1
if tp + tn > 0.0: else:
accuracy = (tp + tn) / (tp + tn + fp + fn) tn += 1
# Jaccard Index
if tp + tn > 0.0:
accuracy = (tp + tn) / (tp + tn + fp + fn)
return accuracy return accuracy
@ -3006,15 +3001,14 @@ class EnviFormer(PackageBasedModel):
): ):
mod = EnviFormer() mod = EnviFormer()
mod.package = package mod.package = package
if name is not None:
# Clean for potential XSS if name is None or name.strip() == "":
name = nh3.clean(name, tags=s.ALLOWED_HTML_TAGS).strip()
if name is None or name == "":
name = f"EnviFormer {EnviFormer.objects.filter(package=package).count() + 1}" name = f"EnviFormer {EnviFormer.objects.filter(package=package).count() + 1}"
mod.name = name mod.name = name
if description is not None and description.strip() != "": if description is not None and description.strip() != "":
mod.description = nh3.clean(description, tags=s.ALLOWED_HTML_TAGS).strip() mod.description = description
if threshold is None or (threshold <= 0 or 1 <= threshold): if threshold is None or (threshold <= 0 or 1 <= threshold):
raise ValueError("Threshold must be a float between 0 and 1.") raise ValueError("Threshold must be a float between 0 and 1.")
@ -3088,24 +3082,44 @@ class EnviFormer(PackageBasedModel):
self.save() self.save()
start = datetime.now() start = datetime.now()
ds = EnviFormerDataset.generate_dataset(self._get_reactions()) # Standardise reactions for the training data, EnviFormer ignores stereochemistry currently
co2 = {"C(=O)=O", "O=C=O"}
ds = []
for reaction in self._get_reactions():
educts = ".".join(
[
FormatConverter.standardize(smile.smiles, remove_stereo=True)
for smile in reaction.educts.all()
]
)
products = ".".join(
[
FormatConverter.standardize(smile.smiles, remove_stereo=True)
for smile in reaction.products.all()
]
)
if products not in co2:
ds.append(f"{educts}>>{products}")
end = datetime.now() end = datetime.now()
logger.debug(f"build_dataset took {(end - start).total_seconds()} seconds") logger.debug(f"build_dataset took {(end - start).total_seconds()} seconds")
f = os.path.join(s.MODEL_DIR, f"{self.uuid}_ds.json") f = os.path.join(s.MODEL_DIR, f"{self.uuid}_ds.json")
ds.save(f) with open(f, "w") as d_file:
json.dump(ds, d_file)
return ds return ds
def load_dataset(self): def load_dataset(self) -> "Dataset":
ds_path = os.path.join(s.MODEL_DIR, f"{self.uuid}_ds.json") ds_path = os.path.join(s.MODEL_DIR, f"{self.uuid}_ds.json")
return EnviFormerDataset.load(ds_path) with open(ds_path) as d_file:
ds = json.load(d_file)
return ds
def _fit_model(self, ds): def _fit_model(self, ds):
# Call to enviFormer's fine_tune function and return the model # Call to enviFormer's fine_tune function and return the model
from enviformer.finetune import fine_tune from enviformer.finetune import fine_tune
start = datetime.now() start = datetime.now()
model = fine_tune(ds.X(), ds.y(), s.MODEL_DIR, str(self.uuid), device=s.ENVIFORMER_DEVICE) model = fine_tune(ds, s.MODEL_DIR, str(self.uuid), device=s.ENVIFORMER_DEVICE)
end = datetime.now() end = datetime.now()
logger.debug(f"EnviFormer finetuning took {(end - start).total_seconds():.2f} seconds") logger.debug(f"EnviFormer finetuning took {(end - start).total_seconds():.2f} seconds")
return model return model
@ -3121,7 +3135,7 @@ class EnviFormer(PackageBasedModel):
args = {"clz": "EnviFormer"} args = {"clz": "EnviFormer"}
return args return args
def evaluate_model(self, multigen: bool, eval_packages: List["Package"] = None, **kwargs): def evaluate_model(self, multigen: bool, eval_packages: List["Package"] = None):
if self.model_status != self.BUILT_NOT_EVALUATED: if self.model_status != self.BUILT_NOT_EVALUATED:
raise ValueError(f"Can't evaluate a model in state {self.model_status}!") raise ValueError(f"Can't evaluate a model in state {self.model_status}!")
@ -3136,20 +3150,21 @@ class EnviFormer(PackageBasedModel):
self.model_status = self.EVALUATING self.model_status = self.EVALUATING
self.save() self.save()
def evaluate_sg(test_ds, predictions, model_thresh): def evaluate_sg(test_reactions, predictions, model_thresh):
# Group the true products of reactions with the same reactant together # Group the true products of reactions with the same reactant together
assert len(test_ds) == len(predictions)
true_dict = {} true_dict = {}
for r in test_ds: for r in test_reactions:
reactant, true_product_set = r reactant, true_product_set = r.split(">>")
true_product_set = {p for p in true_product_set.split(".")} true_product_set = {p for p in true_product_set.split(".")}
true_dict[reactant] = true_dict.setdefault(reactant, []) + [true_product_set] true_dict[reactant] = true_dict.setdefault(reactant, []) + [true_product_set]
assert len(test_reactions) == len(predictions)
assert sum(len(v) for v in true_dict.values()) == len(test_reactions)
# Group the predicted products of reactions with the same reactant together # Group the predicted products of reactions with the same reactant together
pred_dict = {} pred_dict = {}
for k, pred in enumerate(predictions): for k, pred in enumerate(predictions):
pred_smiles, pred_proba = zip(*pred.items()) pred_smiles, pred_proba = zip(*pred.items())
reactant, _ = test_ds[k, "educts"], test_ds[k, "products"] reactant, true_product = test_reactions[k].split(">>")
pred_dict.setdefault(reactant, {"predict": [], "scores": []}) pred_dict.setdefault(reactant, {"predict": [], "scores": []})
for smiles, proba in zip(pred_smiles, pred_proba): for smiles, proba in zip(pred_smiles, pred_proba):
smiles = set(smiles.split(".")) smiles = set(smiles.split("."))
@ -3184,7 +3199,7 @@ class EnviFormer(PackageBasedModel):
break break
# Recall is TP (correct) / TP + FN (len(test_reactions)) # Recall is TP (correct) / TP + FN (len(test_reactions))
rec = {f"{k:.2f}": v / len(test_ds) for k, v in correct.items()} rec = {f"{k:.2f}": v / len(test_reactions) for k, v in correct.items()}
# Precision is TP (correct) / TP + FP (predicted) # Precision is TP (correct) / TP + FP (predicted)
prec = { prec = {
f"{k:.2f}": v / predicted[k] if predicted[k] > 0 else 0 for k, v in correct.items() f"{k:.2f}": v / predicted[k] if predicted[k] > 0 else 0 for k, v in correct.items()
@ -3263,35 +3278,47 @@ class EnviFormer(PackageBasedModel):
# If there are eval packages perform single generation evaluation on them instead of random splits # If there are eval packages perform single generation evaluation on them instead of random splits
if self.eval_packages.count() > 0: if self.eval_packages.count() > 0:
ds = EnviFormerDataset.generate_dataset( ds = []
Reaction.objects.filter(package__in=self.eval_packages.all()).distinct() for reaction in Reaction.objects.filter(
) package__in=self.eval_packages.all()
test_result = self.model.predict_batch(ds.X()) ).distinct():
educts = ".".join(
[
FormatConverter.standardize(smile.smiles, remove_stereo=True)
for smile in reaction.educts.all()
]
)
products = ".".join(
[
FormatConverter.standardize(smile.smiles, remove_stereo=True)
for smile in reaction.products.all()
]
)
ds.append(f"{educts}>>{products}")
test_result = self.model.predict_batch([smirk.split(">>")[0] for smirk in ds])
single_gen_result = evaluate_sg(ds, test_result, self.threshold) single_gen_result = evaluate_sg(ds, test_result, self.threshold)
self.eval_results = self.compute_averages([single_gen_result]) self.eval_results = self.compute_averages([single_gen_result])
else: else:
from enviformer.finetune import fine_tune from enviformer.finetune import fine_tune
ds = self.load_dataset() ds = self.load_dataset()
n_splits = kwargs.get("n_splits", 20) n_splits = 20
shuff = ShuffleSplit(n_splits=n_splits, test_size=0.1, random_state=42) shuff = ShuffleSplit(n_splits=n_splits, test_size=0.1, random_state=42)
# Single gen eval is done in one loop of train then evaluate rather than storing all n_splits trained models # Single gen eval is done in one loop of train then evaluate rather than storing all n_splits trained models
# this helps reduce the memory footprint. # this helps reduce the memory footprint.
single_gen_results = [] single_gen_results = []
for split_id, (train_index, test_index) in enumerate(shuff.split(ds)): for split_id, (train_index, test_index) in enumerate(shuff.split(ds)):
train = ds[train_index] train = [ds[i] for i in train_index]
test = ds[test_index] test = [ds[i] for i in test_index]
start = datetime.now() start = datetime.now()
model = fine_tune( model = fine_tune(train, s.MODEL_DIR, str(split_id), device=s.ENVIFORMER_DEVICE)
train.X(), train.y(), s.MODEL_DIR, str(split_id), device=s.ENVIFORMER_DEVICE
)
end = datetime.now() end = datetime.now()
logger.debug( logger.debug(
f"EnviFormer finetuning took {(end - start).total_seconds():.2f} seconds" f"EnviFormer finetuning took {(end - start).total_seconds():.2f} seconds"
) )
model.to(s.ENVIFORMER_DEVICE) model.to(s.ENVIFORMER_DEVICE)
test_result = model.predict_batch(test.X()) test_result = model.predict_batch([smirk.split(">>")[0] for smirk in test])
single_gen_results.append(evaluate_sg(test, test_result, self.threshold)) single_gen_results.append(evaluate_sg(test, test_result, self.threshold))
self.eval_results = self.compute_averages(single_gen_results) self.eval_results = self.compute_averages(single_gen_results)
@ -3370,12 +3397,23 @@ class EnviFormer(PackageBasedModel):
): ):
overlap += 1 overlap += 1
continue continue
train_reactions.append(reaction) educts = ".".join(
train_ds = EnviFormerDataset.generate_dataset(train_reactions) [
FormatConverter.standardize(smile.smiles, remove_stereo=True)
for smile in reaction.educts.all()
]
)
products = ".".join(
[
FormatConverter.standardize(smile.smiles, remove_stereo=True)
for smile in reaction.products.all()
]
)
train_reactions.append(f"{educts}>>{products}")
logging.debug( logging.debug(
f"{overlap} compounds had to be removed from multigen split due to overlap within pathways" f"{overlap} compounds had to be removed from multigen split due to overlap within pathways"
) )
model = fine_tune(train_ds.X(), train_ds.y(), s.MODEL_DIR, f"mg_{split_id}") model = fine_tune(train_reactions, s.MODEL_DIR, f"mg_{split_id}")
multi_gen_results.append(evaluate_mg(model, test_pathways, self.threshold)) multi_gen_results.append(evaluate_mg(model, test_pathways, self.threshold))
self.eval_results.update( self.eval_results.update(
@ -3424,44 +3462,41 @@ class Scenario(EnviPathModel):
scenario_type: str, scenario_type: str,
additional_information: List["EnviPyModel"], additional_information: List["EnviPyModel"],
): ):
new_s = Scenario() s = Scenario()
new_s.package = package s.package = package
if name is not None:
# Clean for potential XSS if name is None or name.strip() == "":
name = nh3.clean(name, tags=s.ALLOWED_HTML_TAGS).strip()
if name is None or name == "":
name = f"Scenario {Scenario.objects.filter(package=package).count() + 1}" name = f"Scenario {Scenario.objects.filter(package=package).count() + 1}"
new_s.name = name
s.name = name
if description is not None and description.strip() != "": if description is not None and description.strip() != "":
new_s.description = nh3.clean(description, tags=s.ALLOWED_HTML_TAGS).strip() s.description = description
if scenario_date is not None and scenario_date.strip() != "": if scenario_date is not None and scenario_date.strip() != "":
new_s.scenario_date = nh3.clean(scenario_date).strip() s.scenario_date = scenario_date
if scenario_type is not None and scenario_type.strip() != "": if scenario_type is not None and scenario_type.strip() != "":
new_s.scenario_type = scenario_type s.scenario_type = scenario_type
add_inf = defaultdict(list) add_inf = defaultdict(list)
for info in additional_information: for info in additional_information:
cls_name = info.__class__.__name__ cls_name = info.__class__.__name__
# Clean for potential XSS hidden in the additional information fields. ai_data = json.loads(info.model_dump_json())
ai_data = json.loads(nh3.clean(info.model_dump_json()).strip())
ai_data["uuid"] = f"{uuid4()}" ai_data["uuid"] = f"{uuid4()}"
add_inf[cls_name].append(ai_data) add_inf[cls_name].append(ai_data)
new_s.additional_information = add_inf s.additional_information = add_inf
new_s.save() s.save()
return new_s return s
@transaction.atomic @transaction.atomic
def add_additional_information(self, data: "EnviPyModel"): def add_additional_information(self, data: "EnviPyModel"):
cls_name = data.__class__.__name__ cls_name = data.__class__.__name__
# Clean for potential XSS hidden in the additional information fields. ai_data = json.loads(data.model_dump_json())
ai_data = json.loads(nh3.clean(data.model_dump_json()).strip())
ai_data["uuid"] = f"{uuid4()}" ai_data["uuid"] = f"{uuid4()}"
if cls_name not in self.additional_information: if cls_name not in self.additional_information:
@ -3496,8 +3531,7 @@ class Scenario(EnviPathModel):
new_ais = defaultdict(list) new_ais = defaultdict(list)
for k, vals in data.items(): for k, vals in data.items():
for v in vals: for v in vals:
# Clean for potential XSS hidden in the additional information fields. ai_data = json.loads(v.model_dump_json())
ai_data = json.loads(nh3.clean(v.model_dump_json()).strip())
if hasattr(v, "uuid"): if hasattr(v, "uuid"):
ai_data["uuid"] = str(v.uuid) ai_data["uuid"] = str(v.uuid)
else: else:

View File

@ -172,6 +172,7 @@ def predict(
except Exception as e: except Exception as e:
pw.kv.update({"status": "failed"}) pw.kv.update({"status": "failed"})
pw.kv.update(**{"error": str(e)})
pw.save() pw.save()
if JobLog.objects.filter(task_id=self.request.id).exists(): if JobLog.objects.filter(task_id=self.request.id).exists():

View File

@ -10,7 +10,6 @@ from django.urls import reverse
from django.views.decorators.csrf import csrf_exempt from django.views.decorators.csrf import csrf_exempt
from envipy_additional_information import NAME_MAPPING from envipy_additional_information import NAME_MAPPING
from oauth2_provider.decorators import protected_resource from oauth2_provider.decorators import protected_resource
import nh3
from utilities.chem import FormatConverter, IndigoUtils from utilities.chem import FormatConverter, IndigoUtils
from utilities.decorators import package_permission_required from utilities.decorators import package_permission_required
@ -86,10 +85,7 @@ def login(request):
from django.contrib.auth import authenticate from django.contrib.auth import authenticate
from django.contrib.auth import login from django.contrib.auth import login
username = request.POST.get("username").strip() username = request.POST.get("username")
if username != request.POST.get("username"):
context["message"] = "Login failed!"
return render(request, "static/login.html", context)
password = request.POST.get("password") password = request.POST.get("password")
# Get email for username and check if the account is active # Get email for username and check if the account is active
@ -241,7 +237,6 @@ def get_base_context(request, for_user=None) -> Dict[str, Any]:
"enabled_features": s.FLAGS, "enabled_features": s.FLAGS,
"debug": s.DEBUG, "debug": s.DEBUG,
"external_databases": ExternalDatabase.get_databases(), "external_databases": ExternalDatabase.get_databases(),
"site_id": s.MATOMO_SITE_ID,
}, },
} }
@ -674,8 +669,7 @@ def search(request):
if request.method == "GET": if request.method == "GET":
package_urls = request.GET.getlist("packages") package_urls = request.GET.getlist("packages")
searchterm = request.GET.get("search").strip() searchterm = request.GET.get("search")
mode = request.GET.get("mode") mode = request.GET.get("mode")
# add HTTP_ACCEPT check to differentiate between index and ajax call # add HTTP_ACCEPT check to differentiate between index and ajax call
@ -776,6 +770,7 @@ def package_models(request, package_uuid):
elif request.method == "POST": elif request.method == "POST":
log_post_params(request) log_post_params(request)
name = request.POST.get("model-name") name = request.POST.get("model-name")
description = request.POST.get("model-description") description = request.POST.get("model-description")
@ -896,7 +891,7 @@ def package_model(request, package_uuid, model_uuid):
return JsonResponse(res, safe=False) return JsonResponse(res, safe=False)
else: else:
app_domain_assessment = current_model.app_domain.assess(stand_smiles) app_domain_assessment = current_model.app_domain.assess(stand_smiles)[0]
return JsonResponse(app_domain_assessment, safe=False) return JsonResponse(app_domain_assessment, safe=False)
context = get_base_context(request) context = get_base_context(request)
@ -940,14 +935,8 @@ def package_model(request, package_uuid, model_uuid):
else: else:
return HttpResponseBadRequest() return HttpResponseBadRequest()
else: else:
# TODO: Move cleaning to property updater name = request.POST.get("model-name", "").strip()
name = request.POST.get("model-name") description = request.POST.get("model-description", "").strip()
if name is not None:
name = nh3.clean(name, tags=s.ALLOWED_HTML_TAGS).strip()
description = request.POST.get("model-description")
if description is not None:
description = nh3.clean(description, tags=s.ALLOWED_HTML_TAGS).strip()
if any([name, description]): if any([name, description]):
if name: if name:
@ -1049,16 +1038,8 @@ def package(request, package_uuid):
else: else:
return HttpResponseBadRequest() return HttpResponseBadRequest()
# TODO: Move cleaning to property updater
new_package_name = request.POST.get("package-name") new_package_name = request.POST.get("package-name")
if new_package_name is not None:
new_package_name = nh3.clean(new_package_name, tags=s.ALLOWED_HTML_TAGS).strip()
new_package_description = request.POST.get("package-description") new_package_description = request.POST.get("package-description")
if new_package_description is not None:
new_package_description = nh3.clean(
new_package_description, tags=s.ALLOWED_HTML_TAGS
).strip()
grantee_url = request.POST.get("grantee") grantee_url = request.POST.get("grantee")
read = request.POST.get("read") == "on" read = request.POST.get("read") == "on"
@ -1167,7 +1148,7 @@ def package_compounds(request, package_uuid):
elif request.method == "POST": elif request.method == "POST":
compound_name = request.POST.get("compound-name") compound_name = request.POST.get("compound-name")
compound_smiles = request.POST.get("compound-smiles").strip() compound_smiles = request.POST.get("compound-smiles")
compound_description = request.POST.get("compound-description") compound_description = request.POST.get("compound-description")
c = Compound.create(current_package, compound_smiles, compound_name, compound_description) c = Compound.create(current_package, compound_smiles, compound_name, compound_description)
@ -1220,16 +1201,8 @@ def package_compound(request, package_uuid, compound_uuid):
return JsonResponse({"success": current_compound.url}) return JsonResponse({"success": current_compound.url})
# TODO: Move cleaning to property updater new_compound_name = request.POST.get("compound-name", "").strip()
new_compound_name = request.POST.get("compound-name") new_compound_description = request.POST.get("compound-description", "").strip()
if new_compound_name is not None:
new_compound_name = nh3.clean(new_compound_name, tags=s.ALLOWED_HTML_TAGS).strip()
new_compound_description = request.POST.get("compound-description")
if new_compound_description is not None:
new_compound_description = nh3.clean(
new_compound_description, tags=s.ALLOWED_HTML_TAGS
).strip()
if new_compound_name: if new_compound_name:
current_compound.name = new_compound_name current_compound.name = new_compound_name
@ -1294,7 +1267,7 @@ def package_compound_structures(request, package_uuid, compound_uuid):
elif request.method == "POST": elif request.method == "POST":
structure_name = request.POST.get("structure-name") structure_name = request.POST.get("structure-name")
structure_smiles = request.POST.get("structure-smiles").strip() structure_smiles = request.POST.get("structure-smiles")
structure_description = request.POST.get("structure-description") structure_description = request.POST.get("structure-description")
try: try:
@ -1365,16 +1338,8 @@ def package_compound_structure(request, package_uuid, compound_uuid, structure_u
else: else:
return HttpResponseBadRequest() return HttpResponseBadRequest()
# TODO: Move cleaning to property updater new_structure_name = request.POST.get("compound-structure-name", "").strip()
new_structure_name = request.POST.get("compound-structure-name") new_structure_description = request.POST.get("compound-structure-description", "").strip()
if new_structure_name is not None:
new_structure_name = nh3.clean(new_structure_name, tags=s.ALLOWED_HTML_TAGS).strip()
new_structure_description = request.POST.get("compound-structure-description")
if new_structure_description is not None:
new_structure_description = nh3.clean(
new_structure_description, tags=s.ALLOWED_HTML_TAGS
).strip()
if new_structure_name: if new_structure_name:
current_structure.name = new_structure_name current_structure.name = new_structure_name
@ -1476,11 +1441,11 @@ def package_rules(request, package_uuid):
# Obtain parameters as required by rule type # Obtain parameters as required by rule type
if rule_type == "SimpleAmbitRule": if rule_type == "SimpleAmbitRule":
params["smirks"] = request.POST.get("rule-smirks").strip() params["smirks"] = request.POST.get("rule-smirks")
params["reactant_filter_smarts"] = request.POST.get("rule-reactant-smarts") params["reactant_filter_smarts"] = request.POST.get("rule-reactant-smarts")
params["product_filter_smarts"] = request.POST.get("rule-product-smarts") params["product_filter_smarts"] = request.POST.get("rule-product-smarts")
elif rule_type == "SimpleRDKitRule": elif rule_type == "SimpleRDKitRule":
params["reaction_smarts"] = request.POST.get("rule-reaction-smarts").strip() params["reaction_smarts"] = request.POST.get("rule-reaction-smarts")
elif rule_type == "ParallelRule": elif rule_type == "ParallelRule":
pass pass
elif rule_type == "SequentialRule": elif rule_type == "SequentialRule":
@ -1581,14 +1546,8 @@ def package_rule(request, package_uuid, rule_uuid):
return JsonResponse({"success": current_rule.url}) return JsonResponse({"success": current_rule.url})
# TODO: Move cleaning to property updater rule_name = request.POST.get("rule-name", "").strip()
rule_name = request.POST.get("rule-name") rule_description = request.POST.get("rule-description", "").strip()
if rule_name is not None:
rule_name = nh3.clean(rule_name, tags=s.ALLOWED_HTML_TAGS).strip()
rule_description = request.POST.get("rule-description")
if rule_description is not None:
rule_description = nh3.clean(rule_description, tags=s.ALLOWED_HTML_TAGS).strip()
if rule_name: if rule_name:
current_rule.name = rule_name current_rule.name = rule_name
@ -1677,8 +1636,8 @@ def package_reactions(request, package_uuid):
elif request.method == "POST": elif request.method == "POST":
reaction_name = request.POST.get("reaction-name") reaction_name = request.POST.get("reaction-name")
reaction_description = request.POST.get("reaction-description") reaction_description = request.POST.get("reaction-description")
reactions_smirks = request.POST.get("reaction-smirks")
reactions_smirks = request.POST.get("reaction-smirks").strip()
educts = reactions_smirks.split(">>")[0].split(".") educts = reactions_smirks.split(">>")[0].split(".")
products = reactions_smirks.split(">>")[1].split(".") products = reactions_smirks.split(">>")[1].split(".")
@ -1739,16 +1698,8 @@ def package_reaction(request, package_uuid, reaction_uuid):
return JsonResponse({"success": current_reaction.url}) return JsonResponse({"success": current_reaction.url})
# TODO: Move cleaning to property updater new_reaction_name = request.POST.get("reaction-name", "").strip()
new_reaction_name = request.POST.get("reaction-name") new_reaction_description = request.POST.get("reaction-description", "").strip()
if new_reaction_name is not None:
new_reaction_name = nh3.clean(new_reaction_name, tags=s.ALLOWED_HTML_TAGS).strip()
new_reaction_description = request.POST.get("reaction-description")
if new_reaction_description is not None:
new_reaction_description = nh3.clean(
new_reaction_description, tags=s.ALLOWED_HTML_TAGS
).strip()
if new_reaction_name: if new_reaction_name:
current_reaction.name = new_reaction_name current_reaction.name = new_reaction_name
@ -1825,9 +1776,8 @@ def package_pathways(request, package_uuid):
name = request.POST.get("name") name = request.POST.get("name")
description = request.POST.get("description") description = request.POST.get("description")
smiles = request.POST.get("smiles", "").strip()
pw_mode = request.POST.get("predict", "predict").strip() pw_mode = request.POST.get("predict", "predict").strip()
smiles = request.POST.get("smiles", "").strip()
if "smiles" in request.POST and smiles == "": if "smiles" in request.POST and smiles == "":
return error( return error(
@ -1836,6 +1786,8 @@ def package_pathways(request, package_uuid):
"Pathway prediction failed due to missing or empty SMILES", "Pathway prediction failed due to missing or empty SMILES",
) )
smiles = smiles.strip()
try: try:
stand_smiles = FormatConverter.standardize(smiles) stand_smiles = FormatConverter.standardize(smiles)
except ValueError: except ValueError:
@ -1994,14 +1946,8 @@ def package_pathway(request, package_uuid, pathway_uuid):
return JsonResponse({"success": current_pathway.url}) return JsonResponse({"success": current_pathway.url})
# TODO: Move cleaning to property updater
pathway_name = request.POST.get("pathway-name") pathway_name = request.POST.get("pathway-name")
if pathway_name is not None:
pathway_name = nh3.clean(pathway_name, tags=s.ALLOWED_HTML_TAGS).strip()
pathway_description = request.POST.get("pathway-description") pathway_description = request.POST.get("pathway-description")
if pathway_description is not None:
pathway_description = nh3.clean(pathway_description, tags=s.ALLOWED_HTML_TAGS).strip()
if any([pathway_name, pathway_description]): if any([pathway_name, pathway_description]):
if pathway_name is not None and pathway_name.strip() != "": if pathway_name is not None and pathway_name.strip() != "":
@ -2089,8 +2035,8 @@ def package_pathway_nodes(request, package_uuid, pathway_uuid):
elif request.method == "POST": elif request.method == "POST":
node_name = request.POST.get("node-name") node_name = request.POST.get("node-name")
node_description = request.POST.get("node-description") node_description = request.POST.get("node-description")
node_smiles = request.POST.get("node-smiles")
node_smiles = request.POST.get("node-smiles").strip()
current_pathway.add_node(node_smiles, name=node_name, description=node_description) current_pathway.add_node(node_smiles, name=node_name, description=node_description)
return redirect(current_pathway.url) return redirect(current_pathway.url)
@ -2252,9 +2198,9 @@ def package_pathway_edges(request, package_uuid, pathway_uuid):
elif request.method == "POST": elif request.method == "POST":
log_post_params(request) log_post_params(request)
edge_name = request.POST.get("edge-name") edge_name = request.POST.get("edge-name")
edge_description = request.POST.get("edge-description") edge_description = request.POST.get("edge-description")
edge_substrates = request.POST.getlist("edge-substrates") edge_substrates = request.POST.getlist("edge-substrates")
edge_products = request.POST.getlist("edge-products") edge_products = request.POST.getlist("edge-products")
@ -2341,7 +2287,7 @@ def package_scenarios(request, package_uuid):
"all", False "all", False
): ):
scens = Scenario.objects.filter(package=current_package).order_by("name") scens = Scenario.objects.filter(package=current_package).order_by("name")
res = [{"name": s_.name, "url": s_.url, "uuid": s_.uuid} for s_ in scens] res = [{"name": s.name, "url": s.url, "uuid": s.uuid} for s in scens]
return JsonResponse(res, safe=False) return JsonResponse(res, safe=False)
context = get_base_context(request) context = get_base_context(request)
@ -2389,21 +2335,21 @@ def package_scenarios(request, package_uuid):
"name": "soil", "name": "soil",
"widgets": [ "widgets": [
HTMLGenerator.generate_html(ai, prefix=f"soil_{0}") HTMLGenerator.generate_html(ai, prefix=f"soil_{0}")
for ai in [x for sv in SOIL_ADDITIONAL_INFORMATION.values() for x in sv] for ai in [x for s in SOIL_ADDITIONAL_INFORMATION.values() for x in s]
], ],
}, },
"Sludge Data": { "Sludge Data": {
"name": "sludge", "name": "sludge",
"widgets": [ "widgets": [
HTMLGenerator.generate_html(ai, prefix=f"sludge_{0}") HTMLGenerator.generate_html(ai, prefix=f"sludge_{0}")
for ai in [x for sv in SLUDGE_ADDITIONAL_INFORMATION.values() for x in sv] for ai in [x for s in SLUDGE_ADDITIONAL_INFORMATION.values() for x in s]
], ],
}, },
"Water-Sediment System Data": { "Water-Sediment System Data": {
"name": "sediment", "name": "sediment",
"widgets": [ "widgets": [
HTMLGenerator.generate_html(ai, prefix=f"sediment_{0}") HTMLGenerator.generate_html(ai, prefix=f"sediment_{0}")
for ai in [x for sv in SEDIMENT_ADDITIONAL_INFORMATION.values() for x in sv] for ai in [x for s in SEDIMENT_ADDITIONAL_INFORMATION.values() for x in s]
], ],
}, },
} }
@ -2418,7 +2364,6 @@ def package_scenarios(request, package_uuid):
scenario_name = request.POST.get("scenario-name") scenario_name = request.POST.get("scenario-name")
scenario_description = request.POST.get("scenario-description") scenario_description = request.POST.get("scenario-description")
scenario_date_year = request.POST.get("scenario-date-year") scenario_date_year = request.POST.get("scenario-date-year")
scenario_date_month = request.POST.get("scenario-date-month") scenario_date_month = request.POST.get("scenario-date-month")
scenario_date_day = request.POST.get("scenario-date-day") scenario_date_day = request.POST.get("scenario-date-day")
@ -2432,9 +2377,9 @@ def package_scenarios(request, package_uuid):
scenario_type = request.POST.get("scenario-type") scenario_type = request.POST.get("scenario-type")
additional_information = HTMLGenerator.build_models(request.POST.dict()) additional_information = HTMLGenerator.build_models(request.POST.dict())
additional_information = [x for sv in additional_information.values() for x in sv] additional_information = [x for s in additional_information.values() for x in s]
new_scen = Scenario.create( s = Scenario.create(
current_package, current_package,
name=scenario_name, name=scenario_name,
description=scenario_description, description=scenario_description,
@ -2443,7 +2388,7 @@ def package_scenarios(request, package_uuid):
additional_information=additional_information, additional_information=additional_information,
) )
return redirect(new_scen.url) return redirect(s.url)
else: else:
return HttpResponseNotAllowed( return HttpResponseNotAllowed(
[ [
@ -2743,7 +2688,6 @@ def settings(request):
name = request.POST.get("prediction-setting-name") name = request.POST.get("prediction-setting-name")
description = request.POST.get("prediction-setting-description") description = request.POST.get("prediction-setting-description")
new_default = request.POST.get("prediction-setting-new-default", "off") == "on" new_default = request.POST.get("prediction-setting-new-default", "off") == "on"
max_nodes = min( max_nodes = min(

4
pnpm-lock.yaml generated
View File

@ -1,4 +0,0 @@
lockfileVersion: 6.0
specifiers: {}
dependencies: {}
packages: {}

View File

@ -27,12 +27,10 @@ dependencies = [
"scikit-learn>=1.6.1", "scikit-learn>=1.6.1",
"sentry-sdk[django]>=2.32.0", "sentry-sdk[django]>=2.32.0",
"setuptools>=80.8.0", "setuptools>=80.8.0",
"nh3==0.3.2",
"polars==1.35.1",
] ]
[tool.uv.sources] [tool.uv.sources]
enviformer = { git = "ssh://git@git.envipath.com/enviPath/enviformer.git", rev = "v0.1.4" } enviformer = { git = "ssh://git@git.envipath.com/enviPath/enviformer.git", rev = "v0.1.2" }
envipy-plugins = { git = "ssh://git@git.envipath.com/enviPath/enviPy-plugins.git", rev = "v0.1.0" } envipy-plugins = { git = "ssh://git@git.envipath.com/enviPath/enviPy-plugins.git", rev = "v0.1.0" }
envipy-additional-information = { git = "ssh://git@git.envipath.com/enviPath/enviPy-additional-information.git", rev = "v0.1.7"} envipy-additional-information = { git = "ssh://git@git.envipath.com/enviPath/enviPy-additional-information.git", rev = "v0.1.7"}
envipy-ambit = { git = "ssh://git@git.envipath.com/enviPath/enviPy-ambit.git" } envipy-ambit = { git = "ssh://git@git.envipath.com/enviPath/enviPy-ambit.git" }

View File

@ -1,5 +1,6 @@
{% extends "framework.html" %} {% extends "framework.html" %}
{% load static %} {% load static %}
{% load envipytags %}
{% block content %} {% block content %}
<div class="panel-group" id="reviewListAccordion"> <div class="panel-group" id="reviewListAccordion">

View File

@ -192,7 +192,7 @@
<div class="panel-body list-group-item" id="ReviewedContent"> <div class="panel-body list-group-item" id="ReviewedContent">
{% if object_type == 'package' %} {% if object_type == 'package' %}
{% for obj in reviewed_objects %} {% for obj in reviewed_objects %}
<a class="list-group-item" href="{{ obj.url }}">{{ obj.name|safe }} <a class="list-group-item" href="{{ obj.url }}">{{ obj.name }}
<span class="glyphicon glyphicon-star" aria-hidden="true" <span class="glyphicon glyphicon-star" aria-hidden="true"
style="float:right" data-toggle="tooltip" style="float:right" data-toggle="tooltip"
data-placement="top" title="" data-original-title="Reviewed"> data-placement="top" title="" data-original-title="Reviewed">
@ -201,7 +201,7 @@
{% endfor %} {% endfor %}
{% else %} {% else %}
{% for obj in reviewed_objects|slice:":50" %} {% for obj in reviewed_objects|slice:":50" %}
<a class="list-group-item" href="{{ obj.url }}">{{ obj.name|safe }}{# <i>({{ obj.package.name }})</i> #} <a class="list-group-item" href="{{ obj.url }}">{{ obj.name }}{# <i>({{ obj.package.name }})</i> #}
<span class="glyphicon glyphicon-star" aria-hidden="true" <span class="glyphicon glyphicon-star" aria-hidden="true"
style="float:right" data-toggle="tooltip" style="float:right" data-toggle="tooltip"
data-placement="top" title="" data-original-title="Reviewed"> data-placement="top" title="" data-original-title="Reviewed">
@ -221,11 +221,11 @@
<div class="panel-body list-group-item" id="UnreviewedContent"> <div class="panel-body list-group-item" id="UnreviewedContent">
{% if object_type == 'package' %} {% if object_type == 'package' %}
{% for obj in unreviewed_objects %} {% for obj in unreviewed_objects %}
<a class="list-group-item" href="{{ obj.url }}">{{ obj.name|safe }}</a> <a class="list-group-item" href="{{ obj.url }}">{{ obj.name }}</a>
{% endfor %} {% endfor %}
{% else %} {% else %}
{% for obj in unreviewed_objects|slice:":50" %} {% for obj in unreviewed_objects|slice:":50" %}
<a class="list-group-item" href="{{ obj.url }}">{{ obj.name|safe }}</a> <a class="list-group-item" href="{{ obj.url }}">{{ obj.name }}</a>
{% endfor %} {% endfor %}
{% endif %} {% endif %}
</div> </div>
@ -236,9 +236,9 @@
<ul class='list-group'> <ul class='list-group'>
{% for obj in objects %} {% for obj in objects %}
{% if object_type == 'user' %} {% if object_type == 'user' %}
<a class="list-group-item" href="{{ obj.url }}">{{ obj.username|safe }}</a> <a class="list-group-item" href="{{ obj.url }}">{{ obj.username }}</a>
{% else %} {% else %}
<a class="list-group-item" href="{{ obj.url }}">{{ obj.name|safe }}</a> <a class="list-group-item" href="{{ obj.url }}">{{ obj.name }}</a>
{% endif %} {% endif %}
{% endfor %} {% endfor %}
</ul> </ul>

View File

@ -56,7 +56,7 @@
(function () { (function () {
var u = "//matomo.envipath.com/"; var u = "//matomo.envipath.com/";
_paq.push(['setTrackerUrl', u + 'matomo.php']); _paq.push(['setTrackerUrl', u + 'matomo.php']);
_paq.push(['setSiteId', '{{ meta.site_id }}']); _paq.push(['setSiteId', '10']);
var d = document, g = d.createElement('script'), s = d.getElementsByTagName('script')[0]; var d = document, g = d.createElement('script'), s = d.getElementsByTagName('script')[0];
g.async = true; g.async = true;
g.src = u + 'matomo.js'; g.src = u + 'matomo.js';

View File

@ -26,12 +26,12 @@
{% endif %} {% endif %}
<h4 class="panel-title"> <h4 class="panel-title">
<a id="{{ obj.id }}-link" data-toggle="collapse" data-parent="#migration-detail" <a id="{{ obj.id }}-link" data-toggle="collapse" data-parent="#migration-detail"
href="#{{ obj.id }}">{{ obj.name|safe }}</a> href="#{{ obj.id }}">{{ obj.name }}</a>
</h4> </h4>
</div> </div>
<div id="{{ obj.id }}" class="panel-collapse collapse {% if not obj.status %}in{% endif %}"> <div id="{{ obj.id }}" class="panel-collapse collapse {% if not obj.status %}in{% endif %}">
<div class="panel-body list-group-item"> <div class="panel-body list-group-item">
<a class="list-group-item" href="{{ obj.detail_url }}">{{ obj.name|safe }} Migration Detail Page</a> <a class="list-group-item" href="{{ obj.detail_url }}">{{ obj.name }} Migration Detail Page</a>
</div> </div>
</div> </div>
{% endfor %} {% endfor %}

View File

@ -27,7 +27,7 @@
{% endif %} {% endif %}
<h4 class="panel-title"> <h4 class="panel-title">
<a id="{{ obj.id }}-link" data-toggle="collapse" data-parent="#migration-detail" <a id="{{ obj.id }}-link" data-toggle="collapse" data-parent="#migration-detail"
href="#{{ obj.id }}">{{ obj.name|safe }}</a> href="#{{ obj.id }}">{{ obj.name }}</a>
</h4> </h4>
</div> </div>
<div id="{{ obj.id }}" class="panel-collapse collapse {% if not obj.status %}in{% endif %}"> <div id="{{ obj.id }}" class="panel-collapse collapse {% if not obj.status %}in{% endif %}">

View File

@ -1,4 +1,3 @@
<div class="modal fade" tabindex="-1" id="new_model_modal" role="dialog" aria-labelledby="new_model_modal" <div class="modal fade" tabindex="-1" id="new_model_modal" role="dialog" aria-labelledby="new_model_modal"
aria-hidden="true"> aria-hidden="true">
<div class="modal-dialog modal-lg"> <div class="modal-dialog modal-lg">
@ -48,14 +47,14 @@
<option disabled>Reviewed Packages</option> <option disabled>Reviewed Packages</option>
{% for obj in meta.readable_packages %} {% for obj in meta.readable_packages %}
{% if obj.reviewed %} {% if obj.reviewed %}
<option value="{{ obj.url }}">{{ obj.name|safe }}</option> <option value="{{ obj.url }}">{{ obj.name }}</option>
{% endif %} {% endif %}
{% endfor %} {% endfor %}
<option disabled>Unreviewed Packages</option> <option disabled>Unreviewed Packages</option>
{% for obj in meta.readable_packages %} {% for obj in meta.readable_packages %}
{% if not obj.reviewed %} {% if not obj.reviewed %}
<option value="{{ obj.url }}">{{ obj.name|safe }}</option> <option value="{{ obj.url }}">{{ obj.name }}</option>
{% endif %} {% endif %}
{% endfor %} {% endfor %}
</select> </select>
@ -69,14 +68,14 @@
<option disabled>Reviewed Packages</option> <option disabled>Reviewed Packages</option>
{% for obj in meta.readable_packages %} {% for obj in meta.readable_packages %}
{% if obj.reviewed %} {% if obj.reviewed %}
<option value="{{ obj.url }}">{{ obj.name|safe }}</option> <option value="{{ obj.url }}">{{ obj.name }}</option>
{% endif %} {% endif %}
{% endfor %} {% endfor %}
<option disabled>Unreviewed Packages</option> <option disabled>Unreviewed Packages</option>
{% for obj in meta.readable_packages %} {% for obj in meta.readable_packages %}
{% if not obj.reviewed %} {% if not obj.reviewed %}
<option value="{{ obj.url }}">{{ obj.name|safe }}</option> <option value="{{ obj.url }}">{{ obj.name }}</option>
{% endif %} {% endif %}
{% endfor %} {% endfor %}
</select> </select>

View File

@ -1,4 +1,3 @@
{% load static %} {% load static %}
<div class="modal fade" tabindex="-1" id="new_pathway_modal" role="dialog" aria-labelledby="new_pathway_modal" <div class="modal fade" tabindex="-1" id="new_pathway_modal" role="dialog" aria-labelledby="new_pathway_modal"
aria-hidden="true" style="overflow-y: auto;"> aria-hidden="true" style="overflow-y: auto;">
@ -112,7 +111,7 @@
<select id="settingSelect" name="settingSelect" class="form-control"> <select id="settingSelect" name="settingSelect" class="form-control">
{% for setting in available_settings %} {% for setting in available_settings %}
<option value="{{ setting.id }}">{{ setting.name|safe }}</option> <option value="{{ setting.id }}">{{ setting.name }}</option>
{% endfor %} {% endfor %}
</select> </select>
<p></p> <p></p>

View File

@ -1,4 +1,3 @@
{% load static %} {% load static %}
<div id="new_prediction_setting_modal" class="modal" tabindex="-1"> <div id="new_prediction_setting_modal" class="modal" tabindex="-1">
@ -41,14 +40,14 @@
<option disabled>Reviewed Packages</option> <option disabled>Reviewed Packages</option>
{% for obj in meta.readable_packages %} {% for obj in meta.readable_packages %}
{% if obj.reviewed %} {% if obj.reviewed %}
<option value="{{ obj.url }}">{{ obj.name|safe }}</option> <option value="{{ obj.url }}">{{ obj.name }}</option>
{% endif %} {% endif %}
{% endfor %} {% endfor %}
<option disabled>Unreviewed Packages</option> <option disabled>Unreviewed Packages</option>
{% for obj in meta.readable_packages %} {% for obj in meta.readable_packages %}
{% if not obj.reviewed %} {% if not obj.reviewed %}
<option value="{{ obj.url }}">{{ obj.name|safe }}</option> <option value="{{ obj.url }}">{{ obj.name }}</option>
{% endif %} {% endif %}
{% endfor %} {% endfor %}
</select> </select>
@ -58,7 +57,7 @@
<select id="model-based-prediction-setting-model" name="model-based-prediction-setting-model" class="form-control" data-width='100%'> <select id="model-based-prediction-setting-model" name="model-based-prediction-setting-model" class="form-control" data-width='100%'>
<option disabled selected>Select the model</option> <option disabled selected>Select the model</option>
{% for m in models %} {% for m in models %}
<option value="{{ m.url }}">{{ m.name|safe }}</option> <option value="{{ m.url }}">{{ m.name }}</option>
{% endfor %} {% endfor %}
</select> </select>
<label for="model-based-prediction-setting-threshold">Threshold</label> <label for="model-based-prediction-setting-threshold">Threshold</label>

View File

@ -1,4 +1,3 @@
{% load static %} {% load static %}
<div class="modal fade bs-modal-lg" id="add_pathway_edge_modal" tabindex="-1" aria-labelledby="add_pathway_edge_modal" <div class="modal fade bs-modal-lg" id="add_pathway_edge_modal" tabindex="-1" aria-labelledby="add_pathway_edge_modal"
aria-modal="true" aria-modal="true"
@ -37,7 +36,7 @@
<select id="add_pathway_edge_substrates" name="edge-substrates" <select id="add_pathway_edge_substrates" name="edge-substrates"
data-actions-box='true' class="form-control" multiple data-width='100%'> data-actions-box='true' class="form-control" multiple data-width='100%'>
{% for n in pathway.nodes %} {% for n in pathway.nodes %}
<option data-smiles="{{ n.default_node_label.smiles }}" value="{{ n.url }}">{{ n.default_node_label.name|safe }}</option> <option data-smiles="{{ n.default_node_label.smiles }}" value="{{ n.url }}">{{ n.default_node_label.name }}</option>
{% endfor %} {% endfor %}
</select> </select>
</div> </div>
@ -48,7 +47,7 @@
<select id="add_pathway_edge_products" name="edge-products" <select id="add_pathway_edge_products" name="edge-products"
data-actions-box='true' class="form-control" multiple data-width='100%'> data-actions-box='true' class="form-control" multiple data-width='100%'>
{% for n in pathway.nodes %} {% for n in pathway.nodes %}
<option data-smiles="{{ n.default_node_label.smiles }}" value="{{ n.url }}">{{ n.default_node_label.name|safe }}</option> <option data-smiles="{{ n.default_node_label.smiles }}" value="{{ n.url }}">{{ n.default_node_label.name }}</option>
{% endfor %} {% endfor %}
</select> </select>
</div> </div>

View File

@ -1,4 +1,3 @@
{% load static %} {% load static %}
<!-- Delete Edge --> <!-- Delete Edge -->
<div id="delete_pathway_edge_modal" class="modal" tabindex="-1"> <div id="delete_pathway_edge_modal" class="modal" tabindex="-1">
@ -20,7 +19,7 @@
data-actions-box='true' class="form-control" data-width='100%'> data-actions-box='true' class="form-control" data-width='100%'>
<option value="" disabled selected>Select Reaction to delete</option> <option value="" disabled selected>Select Reaction to delete</option>
{% for e in pathway.edges %} {% for e in pathway.edges %}
<option value="{{ e.url }}">{{ e.edge_label.name|safe }}</option> <option value="{{ e.url }}">{{ e.edge_label.name }}</option>
{% endfor %} {% endfor %}
</select> </select>
<input type="hidden" id="hidden" name="hidden" value="delete"/> <input type="hidden" id="hidden" name="hidden" value="delete"/>

View File

@ -1,5 +1,4 @@
{% load static %} {% load static %}
<!-- Delete Node --> <!-- Delete Node -->
<div id="delete_pathway_node_modal" class="modal" tabindex="-1"> <div id="delete_pathway_node_modal" class="modal" tabindex="-1">
<div class="modal-dialog"> <div class="modal-dialog">
@ -20,7 +19,7 @@
data-actions-box='true' class="form-control" data-width='100%'> data-actions-box='true' class="form-control" data-width='100%'>
<option value="" disabled selected>Select Compound to delete</option> <option value="" disabled selected>Select Compound to delete</option>
{% for n in pathway.nodes %} {% for n in pathway.nodes %}
<option value="{{ n.url }}">{{ n.default_node_label.name|safe }}</option> <option value="{{ n.url }}">{{ n.default_node_label.name }}</option>
{% endfor %} {% endfor %}
</select> </select>
<input type="hidden" id="hidden" name="hidden" value="delete"/> <input type="hidden" id="hidden" name="hidden" value="delete"/>

View File

@ -1,4 +1,3 @@
{% load static %} {% load static %}
<!-- Edit Compound --> <!-- Edit Compound -->
<div id="edit_compound_modal" class="modal" tabindex="-1"> <div id="edit_compound_modal" class="modal" tabindex="-1">
@ -16,12 +15,12 @@
{% csrf_token %} {% csrf_token %}
<p> <p>
<label for="compound-name">Name</label> <label for="compound-name">Name</label>
<input id="compound-name" class="form-control" name="compound-name" value="{{ compound.name|safe}}"> <input id="compound-name" class="form-control" name="compound-name" value="{{ compound.name}}">
</p> </p>
<p> <p>
<label for="compound-description">Description</label> <label for="compound-description">Description</label>
<input id="compound-description" type="text" class="form-control" <input id="compound-description" type="text" class="form-control"
value="{{ compound.description|safe }}" value="{{ compound.description }}"
name="compound-description"> name="compound-description">
</p> </p>
</form> </form>

View File

@ -1,4 +1,3 @@
{% load static %} {% load static %}
<!-- Edit Compound --> <!-- Edit Compound -->
<div id="edit_compound_structure_modal" class="modal" tabindex="-1"> <div id="edit_compound_structure_modal" class="modal" tabindex="-1">
@ -16,12 +15,12 @@
{% csrf_token %} {% csrf_token %}
<p> <p>
<label for="compound-structure-name">Name</label> <label for="compound-structure-name">Name</label>
<input id="compound-structure-name" class="form-control" name="compound-structure-name" value="{{ compound_structure.name|safe }}"> <input id="compound-structure-name" class="form-control" name="compound-structure-name" value="{{ compound_structure.name }}">
</p> </p>
<p> <p>
<label for="compound-structure-description">Description</label> <label for="compound-structure-description">Description</label>
<input id="compound-structure-description" type="text" class="form-control" <input id="compound-structure-description" type="text" class="form-control"
value="{{ compound_structure.description|safe }}" name="compound-structure-description"> value="{{ compound_structure.description }}" name="compound-structure-description">
</p> </p>
</form> </form>
</div> </div>

View File

@ -1,4 +1,3 @@
{% load static %} {% load static %}
<!-- Edit Package Permission --> <!-- Edit Package Permission -->
<div id="edit_group_member_modal" class="modal" tabindex="-1"> <div id="edit_group_member_modal" class="modal" tabindex="-1">
@ -40,7 +39,7 @@
{% endfor %} {% endfor %}
<option disabled>Groups</option> <option disabled>Groups</option>
{% for g in groups %} {% for g in groups %}
<option value="{{ g.url }}">{{ g.name|safe }}</option> <option value="{{ g.url }}">{{ g.name }}</option>
{% endfor %} {% endfor %}
</select> </select>
<input type="hidden" name="action" value="add"> <input type="hidden" name="action" value="add">
@ -82,7 +81,7 @@
accept-charset="UTF-8" action="" data-remote="true" method="post"> accept-charset="UTF-8" action="" data-remote="true" method="post">
{% csrf_token %} {% csrf_token %}
<div class="col-xs-8"> <div class="col-xs-8">
{{ g.name|safe }} {{ g.name }}
<input type="hidden" name="member" value="{{ g.url }}"/> <input type="hidden" name="member" value="{{ g.url }}"/>
<input type="hidden" name="action" value="remove"> <input type="hidden" name="action" value="remove">
</div> </div>

View File

@ -1,4 +1,3 @@
{% load static %} {% load static %}
<!-- Edit Model --> <!-- Edit Model -->
<div id="edit_model_modal" class="modal" tabindex="-1"> <div id="edit_model_modal" class="modal" tabindex="-1">
@ -17,12 +16,12 @@
<p> <p>
<label for="model-name">Name</label> <label for="model-name">Name</label>
<input id="model-name" type="text" class="form-control" name="model-name" <input id="model-name" type="text" class="form-control" name="model-name"
value="{{ model.name|safe }}"> value="{{ model.name }}">
</p> </p>
<p> <p>
<label for="model-description">Description</label> <label for="model-description">Description</label>
<input id="model-description" type="text" class="form-control" name="model-description" <input id="model-description" type="text" class="form-control" name="model-description"
value="{{ model.description|safe }}"> value="{{ model.description }}">
</p> </p>
</form> </form>
</div> </div>

View File

@ -1,4 +1,3 @@
{% load static %} {% load static %}
<!-- Edit Node --> <!-- Edit Node -->
<div id="edit_node_modal" class="modal" tabindex="-1"> <div id="edit_node_modal" class="modal" tabindex="-1">
@ -16,12 +15,12 @@
{% csrf_token %} {% csrf_token %}
<p> <p>
<label for="node-name">Name</label> <label for="node-name">Name</label>
<input id="node-name" class="form-control" name="node-name" value="{{ node.name|safe}}"> <input id="node-name" class="form-control" name="node-name" value="{{ node.name}}">
</p> </p>
<p> <p>
<label for="node-description">Description</label> <label for="node-description">Description</label>
<input id="node-description" type="text" class="form-control" <input id="node-description" type="text" class="form-control"
value="{{ node.description|safe }}" value="{{ node.description }}"
name="node-description"> name="node-description">
</p> </p>
</form> </form>

View File

@ -1,4 +1,3 @@
{% load static %} {% load static %}
<!-- Edit Package --> <!-- Edit Package -->
<div id="edit_package_modal" class="modal" tabindex="-1"> <div id="edit_package_modal" class="modal" tabindex="-1">
@ -16,12 +15,12 @@
{% csrf_token %} {% csrf_token %}
<p> <p>
<label for="package-name">Name</label> <label for="package-name">Name</label>
<input id="package-name" class="form-control" name="package-name" value="{{ package.name|safe}}"> <input id="package-name" class="form-control" name="package-name" value="{{ package.name}}">
</p> </p>
<p> <p>
<label for="package-description">Description</label> <label for="package-description">Description</label>
<input id="package-description" type="text" class="form-control" <input id="package-description" type="text" class="form-control"
value="{{ package.description|safe }}" value="{{ package.description }}"
name="package-description"> name="package-description">
</p> </p>
</form> </form>

View File

@ -1,4 +1,3 @@
{% load static %} {% load static %}
<!-- Edit Package Permission --> <!-- Edit Package Permission -->
<div id="edit_package_permissions_modal" class="modal" tabindex="-1"> <div id="edit_package_permissions_modal" class="modal" tabindex="-1">
@ -47,7 +46,7 @@
{% endfor %} {% endfor %}
<option disabled>Groups</option> <option disabled>Groups</option>
{% for g in groups %} {% for g in groups %}
<option value="{{ g.url }}">{{ g.name|safe }}</option> <option value="{{ g.url }}">{{ g.name }}</option>
{% endfor %} {% endfor %}
</select> </select>
</div> </div>
@ -101,7 +100,7 @@
accept-charset="UTF-8" action="" data-remote="true" method="post"> accept-charset="UTF-8" action="" data-remote="true" method="post">
{% csrf_token %} {% csrf_token %}
<div class="col-xs-4"> <div class="col-xs-4">
{{ gp.group.name|safe }} {{ gp.group.name }}
<input type="hidden" name="grantee" value="{{ gp.group.url }}"/> <input type="hidden" name="grantee" value="{{ gp.group.url }}"/>
</div> </div>
<div class="col-xs-2"> <div class="col-xs-2">

View File

@ -1,4 +1,3 @@
{% load static %} {% load static %}
<!-- Edit Pathway --> <!-- Edit Pathway -->
<div id="edit_pathway_modal" class="modal" tabindex="-1"> <div id="edit_pathway_modal" class="modal" tabindex="-1">
@ -16,12 +15,12 @@
{% csrf_token %} {% csrf_token %}
<p> <p>
<label for="pathway-name">Name</label> <label for="pathway-name">Name</label>
<input id="pathway-name" class="form-control" name="pathway-name" value="{{ pathway.name|safe }}"> <input id="pathway-name" class="form-control" name="pathway-name" value="{{ pathway.name }}">
</p> </p>
<p> <p>
<label for="pathway-description">Description</label> <label for="pathway-description">Description</label>
<textarea id="pathway-description" type="text" class="form-control" name="pathway-description" <textarea id="pathway-description" type="text" class="form-control" name="pathway-description"
rows="10">{{ pathway.description|safe }}</textarea> rows="10">{{ pathway.description }}</textarea>
</p> </p>
</form> </form>
</div> </div>

View File

@ -32,7 +32,7 @@
<td colspan="2"> <td colspan="2">
<select id="model" name="model" class="form-control" data-width='100%'> <select id="model" name="model" class="form-control" data-width='100%'>
{% for m in models %} {% for m in models %}
<option value="{{ m.id }}" {% if user.prediction_settings.model.url == m.url %}selected{% endif %}>{{ m.name|safe }}</option> <option value="{{ m.id }}" {% if user.prediction_settings.model.url == m.url %}selected{% endif %}>{{ m.name }}</option>
{% endfor %} {% endfor %}
</select> </select>
</td> </td>

View File

@ -1,4 +1,3 @@
{% load static %} {% load static %}
<!-- Edit Reaction --> <!-- Edit Reaction -->
<div id="edit_reaction_modal" class="modal" tabindex="-1"> <div id="edit_reaction_modal" class="modal" tabindex="-1">
@ -15,12 +14,12 @@
{% csrf_token %} {% csrf_token %}
<p> <p>
<label for="reaction-name">Name</label> <label for="reaction-name">Name</label>
<input id="reaction-name" class="form-control" name="reaction-name" value="{{ reaction.name|safe }}"> <input id="reaction-name" class="form-control" name="reaction-name" value="{{ reaction.name }}">
</p> </p>
<p> <p>
<label for="reaction-description">Description</label> <label for="reaction-description">Description</label>
<input id="reaction-description" type="text" class="form-control" <input id="reaction-description" type="text" class="form-control"
value="{{ reaction.description|safe }}" name="reaction-description"> value="{{ reaction.description }}" name="reaction-description">
</p> </p>
</form> </form>
</div> </div>

View File

@ -1,4 +1,3 @@
{% load static %} {% load static %}
<!-- Edit Rule --> <!-- Edit Rule -->
<div id="edit_rule_modal" class="modal" tabindex="-1"> <div id="edit_rule_modal" class="modal" tabindex="-1">
@ -15,12 +14,12 @@
{% csrf_token %} {% csrf_token %}
<p> <p>
<label for="rule-name">Name</label> <label for="rule-name">Name</label>
<input id="rule-name" class="form-control" name="rule-name" value="{{ rule.name|safe }}"> <input id="rule-name" class="form-control" name="rule-name" value="{{ rule.name }}">
</p> </p>
<p> <p>
<label for="rule-description">Description</label> <label for="rule-description">Description</label>
<input id="rule-description" type="text" class="form-control" <input id="rule-description" type="text" class="form-control"
value="{{ rule.description|safe }}" name="rule-description"> value="{{ rule.description }}" name="rule-description">
</p> </p>
</form> </form>
</div> </div>

View File

@ -1,4 +1,3 @@
{% load static %} {% load static %}
<!-- Edit User --> <!-- Edit User -->
<div id="edit_user_modal" class="modal" tabindex="-1"> <div id="edit_user_modal" class="modal" tabindex="-1">
@ -19,7 +18,7 @@
<select id="default-package" name="default-package" class="form-control" data-width='100%'> <select id="default-package" name="default-package" class="form-control" data-width='100%'>
<option disabled>Select a Package</option> <option disabled>Select a Package</option>
{% for p in meta.writeable_packages %} {% for p in meta.writeable_packages %}
<option value="{{ p.url }}" {% if p.id == meta.user.default_package.id %}selected{% endif %}>{{ p.name|safe }}</option> <option value="{{ p.url }}" {% if p.id == meta.user.default_package.id %}selected{% endif %}>{{ p.name }}</option>
{% endfor %} {% endfor %}
</select> </select>
</p> </p>
@ -28,7 +27,7 @@
<select id="default-group" name="default-group" class="form-control" data-width='100%'> <select id="default-group" name="default-group" class="form-control" data-width='100%'>
<option disabled>Select a Group</option> <option disabled>Select a Group</option>
{% for g in meta.available_groups %} {% for g in meta.available_groups %}
<option value="{{ g.url }}" {% if g.id == meta.user.default_group.id %}selected{% endif %}>{{ g.name|safe }}</option> <option value="{{ g.url }}" {% if g.id == meta.user.default_group.id %}selected{% endif %}>{{ g.name }}</option>
{% endfor %} {% endfor %}
</select> </select>
</p> </p>
@ -37,7 +36,7 @@
<select id="default-prediction-setting" name="default-prediction-setting" class="form-control" data-width='100%'> <select id="default-prediction-setting" name="default-prediction-setting" class="form-control" data-width='100%'>
<option disabled>Select a Setting</option> <option disabled>Select a Setting</option>
{% for s in meta.available_settings %} {% for s in meta.available_settings %}
<option value="{{ s.url }}" {% if s.id == meta.user.default_setting.id %}selected{% endif %}>{{ s.name|safe }}</option> <option value="{{ s.url }}" {% if s.id == meta.user.default_setting.id %}selected{% endif %}>{{ s.name }}</option>
{% endfor %} {% endfor %}
</select> </select>
</p> </p>

View File

@ -1,4 +1,3 @@
<div class="modal fade" tabindex="-1" id="evaluate_model_modal" role="dialog" aria-labelledby="evaluate_model_modal" <div class="modal fade" tabindex="-1" id="evaluate_model_modal" role="dialog" aria-labelledby="evaluate_model_modal"
aria-hidden="true"> aria-hidden="true">
<div class="modal-dialog modal-lg"> <div class="modal-dialog modal-lg">
@ -25,14 +24,14 @@
<option disabled>Reviewed Packages</option> <option disabled>Reviewed Packages</option>
{% for obj in meta.readable_packages %} {% for obj in meta.readable_packages %}
{% if obj.reviewed %} {% if obj.reviewed %}
<option value="{{ obj.url }}">{{ obj.name|safe }}</option> <option value="{{ obj.url }}">{{ obj.name }}</option>
{% endif %} {% endif %}
{% endfor %} {% endfor %}
<option disabled>Unreviewed Packages</option> <option disabled>Unreviewed Packages</option>
{% for obj in meta.readable_packages %} {% for obj in meta.readable_packages %}
{% if not obj.reviewed %} {% if not obj.reviewed %}
<option value="{{ obj.url }}">{{ obj.name|safe }}</option> <option value="{{ obj.url }}">{{ obj.name }}</option>
{% endif %} {% endif %}
{% endfor %} {% endfor %}
</select> </select>

View File

@ -1,4 +1,3 @@
{% load static %} {% load static %}
<!-- Copy Object --> <!-- Copy Object -->
<div id="generic_copy_object_modal" class="modal" tabindex="-1"> <div id="generic_copy_object_modal" class="modal" tabindex="-1">
@ -19,7 +18,7 @@
data-width='100%'> data-width='100%'>
<option disabled selected>Select Target Package</option> <option disabled selected>Select Target Package</option>
{% for p in meta.writeable_packages %} {% for p in meta.writeable_packages %}
<option value="{{ p.url }}">{{ p.name|safe }}</option>` <option value="{{ p.url }}">{{ p.name }}</option>`
{% endfor %} {% endfor %}
</select> </select>
<input type="hidden" name="hidden" value="copy"> <input type="hidden" name="hidden" value="copy">

View File

@ -1,4 +1,3 @@
{% load static %} {% load static %}
<style> <style>
@ -56,7 +55,7 @@
<button type="button" class="close" data-dismiss="modal" aria-label="Close"> <button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">×</span> <span aria-hidden="true">×</span>
</button> </button>
<h4 class="modal-title">Set Aliases for {{ current_object.name|safe }}</h4> <h4 class="modal-title">Set Aliases for {{ current_object.name }}</h4>
</div> </div>
<div class="modal-body"> <div class="modal-body">
<form id="set_aliases_modal_form" accept-charset="UTF-8" action="{{ current_object.url }}" <form id="set_aliases_modal_form" accept-charset="UTF-8" action="{{ current_object.url }}"

View File

@ -1,4 +1,3 @@
{% load static %} {% load static %}
<!-- Delete Object --> <!-- Delete Object -->
<div id="generic_set_external_reference_modal" class="modal" tabindex="-1"> <div id="generic_set_external_reference_modal" class="modal" tabindex="-1">
@ -24,7 +23,7 @@
{% if entity == object_type %} {% if entity == object_type %}
{% for db in databases %} {% for db in databases %}
<option id="db-select-{{ db.database.pk }}" data-input-placeholder="{{ db.placeholder }}" <option id="db-select-{{ db.database.pk }}" data-input-placeholder="{{ db.placeholder }}"
value="{{ db.database.id }}">{{ db.database.name|safe }}</option>` value="{{ db.database.id }}">{{ db.database.name }}</option>`
{% endfor %} {% endfor %}
{% endif %} {% endif %}
{% endfor %} {% endfor %}

View File

@ -1,4 +1,3 @@
{% load static %} {% load static %}
<div class="modal fade bs-modal-lg" id="set_scenario_modal" tabindex="-1" aria-labelledby="set_scenario_modal" <div class="modal fade bs-modal-lg" id="set_scenario_modal" tabindex="-1" aria-labelledby="set_scenario_modal"
aria-modal="true" role="dialog"> aria-modal="true" role="dialog">
@ -8,7 +7,7 @@
<button type="button" class="close" data-dismiss="modal" aria-label="Close"> <button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">×</span> <span aria-hidden="true">×</span>
</button> </button>
<h4 class="modal-title">Set Scenarios for {{ current_object.name|safe }}</h4> <h4 class="modal-title">Set Scenarios for {{ current_object.name }}</h4>
</div> </div>
<div class="modal-body"> <div class="modal-body">
<div id="loading_scenario_div" class="text-center"></div> <div id="loading_scenario_div" class="text-center"></div>

View File

@ -1,4 +1,3 @@
<div class="modal fade" <div class="modal fade"
tabindex="-1" tabindex="-1"
id="manage_api_token_modal" id="manage_api_token_modal"
@ -42,7 +41,7 @@
<div class="input-group"> <div class="input-group">
<input type="hidden" name="hidden" value="delete"> <input type="hidden" name="hidden" value="delete">
<input type="hidden" name="token-id" value="{{ t.pk }}"> <input type="hidden" name="token-id" value="{{ t.pk }}">
<input type="text" class="form-control" value="{{ t.name|safe }}" disabled> <input type="text" class="form-control" value="{{ t.name }}" disabled>
<span class="input-group-btn"> <span class="input-group-btn">
<button type="submit" class="btn btn-danger">Delete</button> <button type="submit" class="btn btn-danger">Delete</button>
</span> </span>

View File

@ -56,7 +56,7 @@
<option disabled>Select a Setting</option> <option disabled>Select a Setting</option>
{% for s in meta.available_settings %} {% for s in meta.available_settings %}
<option value="{{ s.url }}"{% if s.id == meta.user.default_setting.id %}selected{% endif %}> <option value="{{ s.url }}"{% if s.id == meta.user.default_setting.id %}selected{% endif %}>
{{ s.name|safe }}{% if s.id == meta.user.default_setting.id %} <i>(User default)</i>{% endif %} {{ s.name }}{% if s.id == meta.user.default_setting.id %} <i>(User default)</i>{% endif %}
</option> </option>
{% endfor %} {% endfor %}
</select> </select>

View File

@ -13,7 +13,7 @@
<div class="panel-group" id="rule-detail"> <div class="panel-group" id="rule-detail">
<div class="panel panel-default"> <div class="panel panel-default">
<div class="panel-heading" id="headingPanel" style="font-size:2rem;height: 46px"> <div class="panel-heading" id="headingPanel" style="font-size:2rem;height: 46px">
{{ rule.name|safe }} {{ rule.name }}
<div id="actionsButton" <div id="actionsButton"
style="float: right;font-weight: normal;font-size: medium;position: relative; top: 50%; transform: translateY(-50%);z-index:100;display: none;" style="float: right;font-weight: normal;font-size: medium;position: relative; top: 50%; transform: translateY(-50%);z-index:100;display: none;"
class="dropdown"><a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" class="dropdown"><a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button"
@ -60,7 +60,7 @@
<div id="rule-reaction-patterns" class="panel-collapse collapse in"> <div id="rule-reaction-patterns" class="panel-collapse collapse in">
<div class="panel-body list-group-item"> <div class="panel-body list-group-item">
{% for r in rule.srs %} {% for r in rule.srs %}
<a class="list-group-item" href="{{ r.url }}">{{ r.name|safe }}</a> <a class="list-group-item" href="{{ r.url }}">{{ r.name }}</a>
<div align="center"> <div align="center">
<p> <p>
{{r.as_svg|safe}} {{r.as_svg|safe}}
@ -81,7 +81,7 @@
<div id="rule-scenario" class="panel-collapse collapse in"> <div id="rule-scenario" class="panel-collapse collapse in">
<div class="panel-body list-group-item"> <div class="panel-body list-group-item">
{% for s in rule.scenarios.all %} {% for s in rule.scenarios.all %}
<a class="list-group-item" href="{{ s.url }}">{{ s.name|safe }} <i>({{ s.package.name|safe }})</i></a> <a class="list-group-item" href="{{ s.url }}">{{ s.name }} <i>({{ s.package.name }})</i></a>
{% endfor %} {% endfor %}
</div> </div>
</div> </div>

View File

@ -15,7 +15,7 @@
<div class="panel-group" id="compound-detail"> <div class="panel-group" id="compound-detail">
<div class="panel panel-default"> <div class="panel panel-default">
<div class="panel-heading" id="headingPanel" style="font-size:2rem;height: 46px"> <div class="panel-heading" id="headingPanel" style="font-size:2rem;height: 46px">
{{ compound.name|safe }} {{ compound.name }}
<div id="actionsButton" <div id="actionsButton"
style="float: right;font-weight: normal;font-size: medium;position: relative; top: 50%; transform: translateY(-50%);z-index:100;display: none;" style="float: right;font-weight: normal;font-size: medium;position: relative; top: 50%; transform: translateY(-50%);z-index:100;display: none;"
class="dropdown"><a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" class="dropdown"><a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button"
@ -64,7 +64,7 @@
</div> </div>
<div id="compound-desc" class="panel-collapse collapse in"> <div id="compound-desc" class="panel-collapse collapse in">
<div class="panel-body list-group-item"> <div class="panel-body list-group-item">
{{ compound.description|safe }} {{ compound.description }}
</div> </div>
</div> </div>
@ -133,7 +133,7 @@
<div id="compound-reaction" class="panel-collapse collapse in"> <div id="compound-reaction" class="panel-collapse collapse in">
<div class="panel-body list-group-item"> <div class="panel-body list-group-item">
{% for r in compound.related_reactions %} {% for r in compound.related_reactions %}
<a class="list-group-item" href="{{ r.url }}">{{ r.name|safe }} <i>({{ r.package.name|safe }})</i></a> <a class="list-group-item" href="{{ r.url }}">{{ r.name }} <i>({{ r.package.name }})</i></a>
{% endfor %} {% endfor %}
</div> </div>
</div> </div>
@ -150,7 +150,7 @@
<div id="compound-pathway" class="panel-collapse collapse in"> <div id="compound-pathway" class="panel-collapse collapse in">
<div class="panel-body list-group-item"> <div class="panel-body list-group-item">
{% for r in compound.related_pathways %} {% for r in compound.related_pathways %}
<a class="list-group-item" href="{{ r.url }}">{{ r.name|safe }} <i>({{ r.package.name|safe }})</i></a> <a class="list-group-item" href="{{ r.url }}">{{ r.name }} <i>({{ r.package.name }})</i></a>
{% endfor %} {% endfor %}
</div> </div>
</div> </div>
@ -167,7 +167,7 @@
<div id="compound-scenario" class="panel-collapse collapse in"> <div id="compound-scenario" class="panel-collapse collapse in">
<div class="panel-body list-group-item"> <div class="panel-body list-group-item">
{% for s in compound.scenarios.all %} {% for s in compound.scenarios.all %}
<a class="list-group-item" href="{{ s.url }}">{{ s.name|safe }} <i>({{ s.package.name|safe }})</i></a> <a class="list-group-item" href="{{ s.url }}">{{ s.name }} <i>({{ s.package.name }})</i></a>
{% endfor %} {% endfor %}
</div> </div>
</div> </div>

View File

@ -13,7 +13,7 @@
<div class="panel-group" id="compound-structure-detail"> <div class="panel-group" id="compound-structure-detail">
<div class="panel panel-default"> <div class="panel panel-default">
<div class="panel-heading" id="headingPanel" style="font-size:2rem;height: 46px"> <div class="panel-heading" id="headingPanel" style="font-size:2rem;height: 46px">
{{ compound_structure.name|safe }} {{ compound_structure.name }}
<div id="actionsButton" <div id="actionsButton"
style="float: right;font-weight: normal;font-size: medium;position: relative; top: 50%; transform: translateY(-50%);z-index:100;display: none;" style="float: right;font-weight: normal;font-size: medium;position: relative; top: 50%; transform: translateY(-50%);z-index:100;display: none;"
class="dropdown"><a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" class="dropdown"><a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button"
@ -28,7 +28,7 @@
</div> </div>
</div> </div>
<div class="panel-body"> <div class="panel-body">
<p> {{ compound_structure.description|safe }} </p> <p> {{ compound_structure.description }} </p>
</div> </div>
<!-- Image --> <!-- Image -->
@ -86,7 +86,7 @@
<div id="compound-structure-scenario" class="panel-collapse collapse in"> <div id="compound-structure-scenario" class="panel-collapse collapse in">
<div class="panel-body list-group-item"> <div class="panel-body list-group-item">
{% for s in compound_structure.scenarios.all %} {% for s in compound_structure.scenarios.all %}
<a class="list-group-item" href="{{ s.url }}">{{ s.name|safe }} <i>({{ s.package.name|safe }})</i></a> <a class="list-group-item" href="{{ s.url }}">{{ s.name }} <i>({{ s.package.name }})</i></a>
{% endfor %} {% endfor %}
</div> </div>
</div> </div>

View File

@ -12,7 +12,7 @@
<div class="panel-group" id="edge-detail"> <div class="panel-group" id="edge-detail">
<div class="panel panel-default"> <div class="panel panel-default">
<div class="panel-heading" id="headingPanel" style="font-size:2rem;height: 46px"> <div class="panel-heading" id="headingPanel" style="font-size:2rem;height: 46px">
{{ edge.edge_label.name|safe }} {{ edge.edge_label.name }}
<div id="actionsButton" <div id="actionsButton"
style="float: right;font-weight: normal;font-size: medium;position: relative; top: 50%; transform: translateY(-50%);z-index:100;display: none;" style="float: right;font-weight: normal;font-size: medium;position: relative; top: 50%; transform: translateY(-50%);z-index:100;display: none;"
class="dropdown"><a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" class="dropdown"><a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button"
@ -36,7 +36,7 @@
</div> </div>
<div id="edge-desc" class="panel-collapse collapse in"> <div id="edge-desc" class="panel-collapse collapse in">
<div class="panel-body list-group-item"> <div class="panel-body list-group-item">
{{ edge.description|safe }} {{ edge.description }}
</div> </div>
</div> </div>
@ -82,12 +82,12 @@
<div id="edge-description-smiles" class="panel-collapse collapse in"> <div id="edge-description-smiles" class="panel-collapse collapse in">
<div class="panel-body list-group-item"> <div class="panel-body list-group-item">
{% for educt in edge.start_nodes.all %} {% for educt in edge.start_nodes.all %}
<a class="btn btn-default" href="{{ educt.url }}">{{ educt.name|safe }}</a> <a class="btn btn-default" href="{{ educt.url }}">{{ educt.name }}</a>
{% endfor %} {% endfor %}
<span class="glyphicon glyphicon-arrow-right" style="margin-left:5em;margin-right:5em;" <span class="glyphicon glyphicon-arrow-right" style="margin-left:5em;margin-right:5em;"
aria-hidden="true"></span> aria-hidden="true"></span>
{% for product in edge.end_nodes.all %} {% for product in edge.end_nodes.all %}
<a class="btn btn-default" href="{{ product.url }}">{{ product.name|safe }}</a> <a class="btn btn-default" href="{{ product.url }}">{{ product.name }}</a>
{% endfor %} {% endfor %}
</div> </div>
</div> </div>
@ -116,7 +116,7 @@
<div id="edge-rules" class="panel-collapse collapse in"> <div id="edge-rules" class="panel-collapse collapse in">
<div class="panel-body list-group-item"> <div class="panel-body list-group-item">
{% for r in edge.edge_label.rules.all %} {% for r in edge.edge_label.rules.all %}
<a class="list-group-item" href="{{ r.url }}">{{ r.name|safe }}</a> <a class="list-group-item" href="{{ r.url }}">{{ r.name }}</a>
{% endfor %} {% endfor %}
</div> </div>
</div> </div>
@ -132,7 +132,7 @@
<div id="edge-scenario" class="panel-collapse collapse in"> <div id="edge-scenario" class="panel-collapse collapse in">
<div class="panel-body list-group-item"> <div class="panel-body list-group-item">
{% for s in edge.scenarios.all %} {% for s in edge.scenarios.all %}
<a class="list-group-item" href="{{ s.url }}">{{ s.name|safe }} <i>({{ s.package.name|safe }})</i></a> <a class="list-group-item" href="{{ s.url }}">{{ s.name }} <i>({{ s.package.name }})</i></a>
{% endfor %} {% endfor %}
</div> </div>
</div> </div>

View File

@ -11,7 +11,7 @@
<div class="panel-group" id="package-detail"> <div class="panel-group" id="package-detail">
<div class="panel panel-default"> <div class="panel panel-default">
<div class="panel-heading" id="headingPanel" style="font-size:2rem;height: 46px"> <div class="panel-heading" id="headingPanel" style="font-size:2rem;height: 46px">
{{ group.name|safe }} {{ group.name }}
<div id="actionsButton" <div id="actionsButton"
style="float: right;font-weight: normal;font-size: medium;position: relative; top: 50%; transform: translateY(-50%);z-index:100;display: none;" style="float: right;font-weight: normal;font-size: medium;position: relative; top: 50%; transform: translateY(-50%);z-index:100;display: none;"
class="dropdown"><a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" class="dropdown"><a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button"
@ -26,7 +26,7 @@
</div> </div>
</div> </div>
<div class="panel-body"> <div class="panel-body">
<p> {{ group.description|safe }} </p> <p> {{ group.description }} </p>
</div> </div>
</div> </div>
@ -39,10 +39,10 @@
</div> </div>
<ul class="list-group"> <ul class="list-group">
{% for um in group.user_member.all %} {% for um in group.user_member.all %}
<a class="list-group-item" href="{{ um.url }}">{{ um.username|safe }}</a> <a class="list-group-item" href="{{ um.url }}">{{ um.username }}</a>
{% endfor %} {% endfor %}
{% for gm in group.group_member.all %} {% for gm in group.group_member.all %}
<a class="list-group-item" href="{{ gm.url }}">{{ gm.name|safe }}</a> <a class="list-group-item" href="{{ gm.url }}">{{ gm.name }}</a>
{% endfor %} {% endfor %}
</ul> </ul>
</div> </div>
@ -56,7 +56,7 @@
</div> </div>
<ul class="list-group"> <ul class="list-group">
{% for p in packages %} {% for p in packages %}
<a class="list-group-item" href="{{ p.url }}">{{ p.name|safe }}</a> <a class="list-group-item" href="{{ p.url }}">{{ p.name }}</a>
{% endfor %} {% endfor %}
</ul> </ul>
</div> </div>

View File

@ -1,5 +1,6 @@
{% extends "framework.html" %} {% extends "framework.html" %}
{% load static %} {% load static %}
{% load envipytags %}
{% block content %} {% block content %}
{% block action_modals %} {% block action_modals %}
@ -17,7 +18,7 @@
<div class="panel-group" id="model-detail"> <div class="panel-group" id="model-detail">
<div class="panel panel-default"> <div class="panel panel-default">
<div class="panel-heading" id="headingPanel" style="font-size:2rem;height: 46px"> <div class="panel-heading" id="headingPanel" style="font-size:2rem;height: 46px">
{{ model.name|safe }} {{ model.name }}
<div id="actionsButton" <div id="actionsButton"
style="float: right;font-weight: normal;font-size: medium;position: relative; top: 50%; transform: translateY(-50%);z-index:100;display: none;" style="float: right;font-weight: normal;font-size: medium;position: relative; top: 50%; transform: translateY(-50%);z-index:100;display: none;"
class="dropdown"><a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" class="dropdown"><a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button"
@ -32,7 +33,7 @@
</div> </div>
</div> </div>
<div class="panel-body"> <div class="panel-body">
<p> {{ model.description|safe }} </p> <p> {{ model.description }} </p>
</div> </div>
{% if model|classname == 'MLRelativeReasoning' or model|classname == 'RuleBasedRelativeReasoning'%} {% if model|classname == 'MLRelativeReasoning' or model|classname == 'RuleBasedRelativeReasoning'%}
<!-- Rule Packages --> <!-- Rule Packages -->
@ -45,7 +46,7 @@
<div id="rule-package" class="panel-collapse collapse in"> <div id="rule-package" class="panel-collapse collapse in">
<div class="panel-body list-group-item"> <div class="panel-body list-group-item">
{% for p in model.rule_packages.all %} {% for p in model.rule_packages.all %}
<a class="list-group-item" href="{{ p.url }}">{{ p.name|safe }}</a> <a class="list-group-item" href="{{ p.url }}">{{ p.name }}</a>
{% endfor %} {% endfor %}
</div> </div>
</div> </div>
@ -59,7 +60,7 @@
<div id="reaction-package" class="panel-collapse collapse in"> <div id="reaction-package" class="panel-collapse collapse in">
<div class="panel-body list-group-item"> <div class="panel-body list-group-item">
{% for p in model.data_packages.all %} {% for p in model.data_packages.all %}
<a class="list-group-item" href="{{ p.url }}">{{ p.name|safe }}</a> <a class="list-group-item" href="{{ p.url }}">{{ p.name }}</a>
{% endfor %} {% endfor %}
</div> </div>
</div> </div>
@ -74,7 +75,7 @@
<div id="eval-package" class="panel-collapse collapse in"> <div id="eval-package" class="panel-collapse collapse in">
<div class="panel-body list-group-item"> <div class="panel-body list-group-item">
{% for p in model.eval_packages.all %} {% for p in model.eval_packages.all %}
<a class="list-group-item" href="{{ p.url }}">{{ p.name|safe }}</a> <a class="list-group-item" href="{{ p.url }}">{{ p.name }}</a>
{% endfor %} {% endfor %}
</div> </div>
</div> </div>

View File

@ -12,7 +12,7 @@
<div class="panel-group" id="node-detail"> <div class="panel-group" id="node-detail">
<div class="panel panel-default"> <div class="panel panel-default">
<div class="panel-heading" id="headingPanel" style="font-size:2rem;height: 46px"> <div class="panel-heading" id="headingPanel" style="font-size:2rem;height: 46px">
{{ node.name|safe }} {{ node.name }}
<div id="actionsButton" <div id="actionsButton"
style="float: right;font-weight: normal;font-size: medium;position: relative; top: 50%; transform: translateY(-50%);z-index:100;display: none;" style="float: right;font-weight: normal;font-size: medium;position: relative; top: 50%; transform: translateY(-50%);z-index:100;display: none;"
class="dropdown"><a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" class="dropdown"><a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button"
@ -39,7 +39,7 @@
</div> </div>
<div id="node-desc" class="panel-collapse collapse in"> <div id="node-desc" class="panel-collapse collapse in">
<div class="panel-body list-group-item"> <div class="panel-body list-group-item">
{{ node.description|safe }} {{ node.description }}
</div> </div>
</div> </div>
@ -98,7 +98,7 @@
<div id="node-scenario" class="panel-collapse collapse in"> <div id="node-scenario" class="panel-collapse collapse in">
<div class="panel-body list-group-item"> <div class="panel-body list-group-item">
{% for s in node.scenarios.all %} {% for s in node.scenarios.all %}
<a class="list-group-item" href="{{ s.url }}">{{ s.name|safe }} <i>({{ s.package.name|safe }})</i></a> <a class="list-group-item" href="{{ s.url }}">{{ s.name }} <i>({{ s.package.name }})</i></a>
{% endfor %} {% endfor %}
</div> </div>
</div> </div>

View File

@ -14,7 +14,7 @@
<div class="panel-group" id="package-detail"> <div class="panel-group" id="package-detail">
<div class="panel panel-default"> <div class="panel panel-default">
<div class="panel-heading" id="headingPanel" style="font-size:2rem;height: 46px"> <div class="panel-heading" id="headingPanel" style="font-size:2rem;height: 46px">
{{ package.name|safe }} {{ package.name }}
<div id="actionsButton" <div id="actionsButton"
style="float: right;font-weight: normal;font-size: medium;position: relative; top: 50%; transform: translateY(-50%);z-index:100;display: none;" style="float: right;font-weight: normal;font-size: medium;position: relative; top: 50%; transform: translateY(-50%);z-index:100;display: none;"
class="dropdown"><a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" class="dropdown"><a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button"

View File

@ -98,7 +98,7 @@
<div class="panel-group" id="pwAccordion"> <div class="panel-group" id="pwAccordion">
<div class="panel panel-default"> <div class="panel panel-default">
<div class="panel-heading" id="headingPanel" style="font-size:2rem;height: 46px"> <div class="panel-heading" id="headingPanel" style="font-size:2rem;height: 46px">
{{ pathway.name|safe }} {{ pathway.name }}
</div> </div>
</div> </div>
<div class="panel panel-default panel-heading list-group-item" style="background-color:silver"> <div class="panel panel-default panel-heading list-group-item" style="background-color:silver">
@ -236,7 +236,7 @@
<div id="pathway-scenario" class="panel-collapse collapse in"> <div id="pathway-scenario" class="panel-collapse collapse in">
<div class="panel-body list-group-item"> <div class="panel-body list-group-item">
{% for s in pathway.scenarios.all %} {% for s in pathway.scenarios.all %}
<a class="list-group-item" href="{{ s.url }}">{{ s.name|safe }} <i>({{ s.package.name|safe }})</i></a> <a class="list-group-item" href="{{ s.url }}">{{ s.name }} <i>({{ s.package.name }})</i></a>
{% endfor %} {% endfor %}
</div> </div>
</div> </div>
@ -266,7 +266,7 @@
<td colspan="2"> <td colspan="2">
<li class="list-group-item"> <li class="list-group-item">
<a href="{{ pathway.setting.model.url }}"> <a href="{{ pathway.setting.model.url }}">
{{ pathway.setting.model.name|safe }} {{ pathway.setting.model.name }}
</a> </a>
</li> </li>
</td> </td>
@ -299,7 +299,7 @@
{% for p in pathway.setting.rule_packages.all %} {% for p in pathway.setting.rule_packages.all %}
<li class="list-group-item"> <li class="list-group-item">
<a href="{{ p.url }}"> <a href="{{ p.url }}">
{{ p.name|safe }} {{ p.name }}
</a> </a>
</li> </li>
{% endfor %} {% endfor %}

View File

@ -14,7 +14,7 @@
<div class="panel-group" id="reaction-detail"> <div class="panel-group" id="reaction-detail">
<div class="panel panel-default"> <div class="panel panel-default">
<div class="panel-heading" id="headingPanel" style="font-size:2rem;height: 46px"> <div class="panel-heading" id="headingPanel" style="font-size:2rem;height: 46px">
{{ reaction.name|safe }} {{ reaction.name }}
<div id="actionsButton" <div id="actionsButton"
style="float: right;font-weight: normal;font-size: medium;position: relative; top: 50%; transform: translateY(-50%);z-index:100;display: none;" style="float: right;font-weight: normal;font-size: medium;position: relative; top: 50%; transform: translateY(-50%);z-index:100;display: none;"
class="dropdown"><a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" class="dropdown"><a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button"
@ -38,7 +38,7 @@
</div> </div>
<div id="reaction-desc" class="panel-collapse collapse in"> <div id="reaction-desc" class="panel-collapse collapse in">
<div class="panel-body list-group-item"> <div class="panel-body list-group-item">
{{ reaction.description|safe }} {{ reaction.description }}
</div> </div>
</div> </div>
@ -84,12 +84,12 @@
<div id="reaction-description-smiles" class="panel-collapse collapse in"> <div id="reaction-description-smiles" class="panel-collapse collapse in">
<div class="panel-body list-group-item"> <div class="panel-body list-group-item">
{% for educt in reaction.educts.all %} {% for educt in reaction.educts.all %}
<a class="btn btn-default" href="{{ educt.url }}">{{ educt.name|safe }}</a> <a class="btn btn-default" href="{{ educt.url }}">{{ educt.name }}</a>
{% endfor %} {% endfor %}
<span class="glyphicon glyphicon-arrow-right" style="margin-left:5em;margin-right:5em;" <span class="glyphicon glyphicon-arrow-right" style="margin-left:5em;margin-right:5em;"
aria-hidden="true"></span> aria-hidden="true"></span>
{% for product in reaction.products.all %} {% for product in reaction.products.all %}
<a class="btn btn-default" href="{{ product.url }}">{{ product.name|safe }}</a> <a class="btn btn-default" href="{{ product.url }}">{{ product.name }}</a>
{% endfor %} {% endfor %}
</div> </div>
</div> </div>
@ -118,7 +118,7 @@
<div id="reaction-rules" class="panel-collapse collapse in"> <div id="reaction-rules" class="panel-collapse collapse in">
<div class="panel-body list-group-item"> <div class="panel-body list-group-item">
{% for r in reaction.rules.all %} {% for r in reaction.rules.all %}
<a class="list-group-item" href="{{ r.url }}">{{ r.name|safe }}</a> <a class="list-group-item" href="{{ r.url }}">{{ r.name }}</a>
{% endfor %} {% endfor %}
</div> </div>
</div> </div>
@ -152,7 +152,7 @@
<div id="reaction-pathway" class="panel-collapse collapse in"> <div id="reaction-pathway" class="panel-collapse collapse in">
<div class="panel-body list-group-item"> <div class="panel-body list-group-item">
{% for r in reaction.related_pathways %} {% for r in reaction.related_pathways %}
<a class="list-group-item" href="{{ r.url }}">{{ r.name|safe }}</a> <a class="list-group-item" href="{{ r.url }}">{{ r.name }}</a>
{% endfor %} {% endfor %}
</div> </div>
</div> </div>
@ -168,7 +168,7 @@
<div id="reaction-scenario" class="panel-collapse collapse in"> <div id="reaction-scenario" class="panel-collapse collapse in">
<div class="panel-body list-group-item"> <div class="panel-body list-group-item">
{% for s in reaction.scenarios.all %} {% for s in reaction.scenarios.all %}
<a class="list-group-item" href="{{ s.url }}">{{ s.name|safe }} <i>({{ s.package.name|safe }})</i></a> <a class="list-group-item" href="{{ s.url }}">{{ s.name }} <i>({{ s.package.name }})</i></a>
{% endfor %} {% endfor %}
</div> </div>
</div> </div>

View File

@ -10,7 +10,7 @@
<div class="panel-group" id="scenario-detail"> <div class="panel-group" id="scenario-detail">
<div class="panel panel-default"> <div class="panel panel-default">
<div class="panel-heading" id="headingPanel" style="font-size:2rem;height: 46px"> <div class="panel-heading" id="headingPanel" style="font-size:2rem;height: 46px">
{{ scenario.name|safe }} {{ scenario.name }}
<div id="actionsButton" <div id="actionsButton"
style="float: right;font-weight: normal;font-size: medium;position: relative; top: 50%; transform: translateY(-50%);z-index:100;display: none;" style="float: right;font-weight: normal;font-size: medium;position: relative; top: 50%; transform: translateY(-50%);z-index:100;display: none;"
class="dropdown"><a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" class="dropdown"><a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button"
@ -30,7 +30,7 @@
<div class="panel panel-default"> <div class="panel panel-default">
<div class="panel-heading">Description</div> <div class="panel-heading">Description</div>
<div class="panel-body"> <div class="panel-body">
{{ scenario.description|safe }} {{ scenario.description }}
<br> <br>
{{ scenario.scenario_type }} {{ scenario.scenario_type }}
<br> <br>

View File

@ -13,7 +13,7 @@
<div class="panel-group" id="rule-detail"> <div class="panel-group" id="rule-detail">
<div class="panel panel-default"> <div class="panel panel-default">
<div class="panel-heading" id="headingPanel" style="font-size:2rem;height: 46px"> <div class="panel-heading" id="headingPanel" style="font-size:2rem;height: 46px">
{{ rule.name|safe }} {{ rule.name }}
<div id="actionsButton" <div id="actionsButton"
style="float: right;font-weight: normal;font-size: medium;position: relative; top: 50%; transform: translateY(-50%);z-index:100;display: none;" style="float: right;font-weight: normal;font-size: medium;position: relative; top: 50%; transform: translateY(-50%);z-index:100;display: none;"
class="dropdown"><a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" class="dropdown"><a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button"
@ -29,7 +29,7 @@
</div> </div>
<div class="panel-body"> <div class="panel-body">
<p> <p>
{{ rule.description|safe }} {{ rule.description }}
</p> </p>
</div> </div>
@ -145,7 +145,7 @@
<div id="rule-composite-rule" class="panel-collapse collapse in"> <div id="rule-composite-rule" class="panel-collapse collapse in">
<div class="panel-body list-group-item"> <div class="panel-body list-group-item">
{% for cr in rule.parallelrule_set.all %} {% for cr in rule.parallelrule_set.all %}
<a class="list-group-item" href="{{ cr.url }}">{{ cr.name|safe }}</a> <a class="list-group-item" href="{{ cr.url }}">{{ cr.name }}</a>
{% endfor %} {% endfor %}
</div> </div>
</div> </div>
@ -162,7 +162,7 @@
<div id="rule-scenario" class="panel-collapse collapse in"> <div id="rule-scenario" class="panel-collapse collapse in">
<div class="panel-body list-group-item"> <div class="panel-body list-group-item">
{% for s in rule.scenarios.all %} {% for s in rule.scenarios.all %}
<a class="list-group-item" href="{{ s.url }}">{{ s.name|safe }}</a> <a class="list-group-item" href="{{ s.url }}">{{ s.name }}</a>
{% endfor %} {% endfor %}
</div> </div>
</div> </div>
@ -179,7 +179,7 @@
<div id="rule-reaction" class="panel-collapse collapse"> <div id="rule-reaction" class="panel-collapse collapse">
<div class="panel-body list-group-item"> <div class="panel-body list-group-item">
{% for r in rule.related_reactions %} {% for r in rule.related_reactions %}
<a class="list-group-item" href="{{ r.url }}">{{ r.name|safe }}</a> <a class="list-group-item" href="{{ r.url }}">{{ r.name }}</a>
{% endfor %} {% endfor %}
</div> </div>
</div> </div>
@ -196,7 +196,7 @@
<div id="rule-pathway" class="panel-collapse collapse"> <div id="rule-pathway" class="panel-collapse collapse">
<div class="panel-body list-group-item"> <div class="panel-body list-group-item">
{% for r in rule.related_pathways %} {% for r in rule.related_pathways %}
<a class="list-group-item" href="{{ r.url }}">{{ r.name|safe }}</a> <a class="list-group-item" href="{{ r.url }}">{{ r.name }}</a>
{% endfor %} {% endfor %}
</div> </div>
</div> </div>

View File

@ -41,7 +41,7 @@
<div id="default-package" class="panel-collapse collapse in"> <div id="default-package" class="panel-collapse collapse in">
<div class="panel-body list-group-item"> <div class="panel-body list-group-item">
<li class="list-group-item"> <li class="list-group-item">
<a href="{{ user.default_package.url }}"> {{ user.default_package.name|safe }}</a> <a href="{{ user.default_package.url }}"> {{ user.default_package.name }}</a>
</li> </li>
</div> </div>
</div> </div>
@ -58,7 +58,7 @@
<div class="panel-body list-group-item"> <div class="panel-body list-group-item">
{% for g in meta.available_groups %} {% for g in meta.available_groups %}
<li class="list-group-item"> <li class="list-group-item">
<a href="{{ g.url }}"> {{ g.name|safe }}</a> <a href="{{ g.url }}"> {{ g.name }}</a>
</li> </li>
{% endfor %} {% endfor %}
</div> </div>
@ -90,7 +90,7 @@
<td colspan="2"> <td colspan="2">
<li class="list-group-item"> <li class="list-group-item">
<a href="{{user.default_setting.model.url}}"> <a href="{{user.default_setting.model.url}}">
{{ user.default_setting.model.name|safe }} {{ user.default_setting.model.name }}
</a> </a>
</li> </li>
</td> </td>
@ -123,7 +123,7 @@
{% for p in user.default_setting.rule_packages.all %} {% for p in user.default_setting.rule_packages.all %}
<li class="list-group-item"> <li class="list-group-item">
<a href="{{p.url}}"> <a href="{{p.url}}">
{{ p.name|safe }} {{ p.name }}
</a> </a>
</li> </li>
{% endfor %} {% endfor %}

View File

@ -18,7 +18,7 @@
</head> </head>
<body> <body>
<p></p> <p></p>
{{ pathway.name|safe }} {{ pathway.name }}
<div id="viz"> <div id="viz">
<svg width="2000" height="2000"> <!-- Sehr großes SVG für Zoom --> <svg width="2000" height="2000"> <!-- Sehr großes SVG für Zoom -->
<defs> <defs>

View File

@ -11,13 +11,13 @@
<option disabled>Reviewed Packages</option> <option disabled>Reviewed Packages</option>
{% endif %} {% endif %}
{% for obj in reviewed_objects %} {% for obj in reviewed_objects %}
<option value="{{ obj.url }}" selected>{{ obj.name|safe }}</option> <option value="{{ obj.url }}" selected>{{ obj.name }}</option>
{% endfor %} {% endfor %}
{% if unreviewed_objects %} {% if unreviewed_objects %}
<option disabled>Unreviewed Packages</option> <option disabled>Unreviewed Packages</option>
{% endif %} {% endif %}
{% for obj in unreviewed_objects %} {% for obj in unreviewed_objects %}
<option value="{{ obj.url }}">{{ obj.name|safe }}</option> <option value="{{ obj.url }}">{{ obj.name }}</option>
{% endfor %} {% endfor %}
</select> </select>
</div> </div>

View File

@ -1,10 +1,8 @@
import os.path
from tempfile import TemporaryDirectory
from django.test import TestCase from django.test import TestCase
from epdb.logic import PackageManager from epdb.logic import PackageManager
from epdb.models import Reaction, Compound, User, Rule, Package from epdb.models import Reaction, Compound, User, Rule
from utilities.chem import FormatConverter from utilities.ml import Dataset
from utilities.ml import RuleBasedDataset, EnviFormerDataset
class DatasetTest(TestCase): class DatasetTest(TestCase):
@ -43,108 +41,12 @@ class DatasetTest(TestCase):
super(DatasetTest, cls).setUpClass() super(DatasetTest, cls).setUpClass()
cls.user = User.objects.get(username="anonymous") cls.user = User.objects.get(username="anonymous")
cls.package = PackageManager.create_package(cls.user, "Anon Test Package", "No Desc") cls.package = PackageManager.create_package(cls.user, "Anon Test Package", "No Desc")
cls.BBD_SUBSET = Package.objects.get(name="Fixtures")
def test_generate_dataset(self): def test_smoke(self):
"""Test generating dataset does not crash"""
self.generate_rule_dataset()
def test_indexing(self):
"""Test indexing a few different ways to check for crashes"""
ds, reactions, rules = self.generate_rule_dataset()
print(ds[5])
print(ds[2, 5])
print(ds[3:6, 2:8])
print(ds[:2, "structure_id"])
def test_add_rows(self):
"""Test adding one row and adding multiple rows"""
ds, reactions, rules = self.generate_rule_dataset()
ds.add_row(list(ds.df.row(1)))
ds.add_rows([list(ds.df.row(i)) for i in range(5)])
def test_times_triggered(self):
"""Check getting times triggered for a rule id"""
ds, reactions, rules = self.generate_rule_dataset()
print(ds.times_triggered(rules[0].uuid))
def test_block_indices(self):
"""Test the usages of _block_indices"""
ds, reactions, rules = self.generate_rule_dataset()
print(ds.struct_features())
print(ds.triggered())
print(ds.observed())
def test_structure_id(self):
"""Check getting a structure id from row index"""
ds, reactions, rules = self.generate_rule_dataset()
print(ds.structure_id(0))
def test_x(self):
"""Test getting X portion of the dataframe"""
ds, reactions, rules = self.generate_rule_dataset()
print(ds.X().df.head())
def test_trig(self):
"""Test getting the triggered portion of the dataframe"""
ds, reactions, rules = self.generate_rule_dataset()
print(ds.trig().df.head())
def test_y(self):
"""Test getting the Y portion of the dataframe"""
ds, reactions, rules = self.generate_rule_dataset()
print(ds.y().df.head())
def test_classification_dataset(self):
"""Test making the classification dataset"""
ds, reactions, rules = self.generate_rule_dataset()
compounds = [c.default_structure for c in Compound.objects.filter(package=self.BBD_SUBSET)]
class_ds, products = ds.classification_dataset(compounds, rules)
print(class_ds.df.head(5))
print(products[:5])
def test_extra_features(self):
reactions = [r for r in Reaction.objects.filter(package=self.BBD_SUBSET)]
applicable_rules = [r for r in Rule.objects.filter(package=self.BBD_SUBSET)]
ds = RuleBasedDataset.generate_dataset(reactions, applicable_rules, feat_funcs=[FormatConverter.maccs, FormatConverter.morgan])
print(ds.shape)
def test_to_arff(self):
"""Test exporting the arff version of the dataset"""
ds, reactions, rules = self.generate_rule_dataset()
ds.to_arff("dataset_arff_test.arff")
def test_save_load(self):
"""Test saving and loading dataset"""
with TemporaryDirectory() as tmpdir:
ds, reactions, rules = self.generate_rule_dataset()
ds.save(os.path.join(tmpdir, "save_dataset.pkl"))
ds_loaded = RuleBasedDataset.load(os.path.join(tmpdir, "save_dataset.pkl"))
self.assertTrue(ds.df.equals(ds_loaded.df))
def test_dataset_example(self):
"""Test with a concrete example checking dataset size"""
reactions = [r for r in Reaction.objects.filter(package=self.package)] reactions = [r for r in Reaction.objects.filter(package=self.package)]
applicable_rules = [self.rule1] applicable_rules = [self.rule1]
ds = RuleBasedDataset.generate_dataset(reactions, applicable_rules) ds = Dataset.generate_dataset(reactions, applicable_rules)
self.assertEqual(len(ds.y()), 1) self.assertEqual(len(ds.y()), 1)
self.assertEqual(ds.y().df.item(), 1) self.assertEqual(sum(ds.y()[0]), 1)
def test_enviformer_dataset(self):
ds, reactions = self.generate_enviformer_dataset()
print(ds.X().head())
print(ds.y().head())
def generate_rule_dataset(self):
"""Generate a RuleBasedDataset from test package data"""
reactions = [r for r in Reaction.objects.filter(package=self.BBD_SUBSET)]
applicable_rules = [r for r in Rule.objects.filter(package=self.BBD_SUBSET)]
ds = RuleBasedDataset.generate_dataset(reactions, applicable_rules)
return ds, reactions, applicable_rules
def generate_enviformer_dataset(self):
reactions = [r for r in Reaction.objects.filter(package=self.BBD_SUBSET)]
ds = EnviFormerDataset.generate_dataset(reactions)
return ds, reactions

View File

@ -42,11 +42,13 @@ class EnviFormerTest(TestCase):
threshold = float(0.5) threshold = float(0.5)
data_package_objs = [self.BBD_SUBSET] data_package_objs = [self.BBD_SUBSET]
eval_packages_objs = [self.BBD_SUBSET] eval_packages_objs = [self.BBD_SUBSET]
mod = EnviFormer.create(self.package, data_package_objs, threshold=threshold) mod = EnviFormer.create(
self.package, data_package_objs, eval_packages_objs, threshold=threshold
)
mod.build_dataset() mod.build_dataset()
mod.build_model() mod.build_model()
mod.evaluate_model(True, eval_packages_objs, n_splits=2) mod.evaluate_model(True, eval_packages_objs)
mod.predict("CCN(CC)C(=O)C1=CC(=CC=C1)C") mod.predict("CCN(CC)C(=O)C1=CC(=CC=C1)C")
@ -55,9 +57,12 @@ class EnviFormerTest(TestCase):
with self.settings(MODEL_DIR=tmpdir): with self.settings(MODEL_DIR=tmpdir):
threshold = float(0.5) threshold = float(0.5)
data_package_objs = [self.BBD_SUBSET] data_package_objs = [self.BBD_SUBSET]
eval_packages_objs = [self.BBD_SUBSET]
mods = [] mods = []
for _ in range(4): for _ in range(4):
mod = EnviFormer.create(self.package, data_package_objs, threshold=threshold) mod = EnviFormer.create(
self.package, data_package_objs, eval_packages_objs, threshold=threshold
)
mod.build_dataset() mod.build_dataset()
mod.build_model() mod.build_model()
mods.append(mod) mods.append(mod)
@ -68,11 +73,15 @@ class EnviFormerTest(TestCase):
# Test pathway prediction # Test pathway prediction
times = [measure_predict(mods[1], self.BBD_SUBSET.pathways[0].pk) for _ in range(5)] times = [measure_predict(mods[1], self.BBD_SUBSET.pathways[0].pk) for _ in range(5)]
print(f"First pathway prediction took {times[0]} seconds, subsequent ones took {times[1:]}") print(
f"First pathway prediction took {times[0]} seconds, subsequent ones took {times[1:]}"
)
# Test eviction by performing three prediction with every model, twice. # Test eviction by performing three prediction with every model, twice.
times = defaultdict(list) times = defaultdict(list)
for _ in range(2): # Eviction should cause the second iteration here to have to reload the models for _ in range(
2
): # Eviction should cause the second iteration here to have to reload the models
for mod in mods: for mod in mods:
for _ in range(3): for _ in range(3):
times[mod.pk].append(measure_predict(mod)) times[mod.pk].append(measure_predict(mod))

View File

@ -4,7 +4,7 @@ import numpy as np
from django.test import TestCase from django.test import TestCase
from epdb.logic import PackageManager from epdb.logic import PackageManager
from epdb.models import User, MLRelativeReasoning, Package, RuleBasedRelativeReasoning from epdb.models import User, MLRelativeReasoning, Package
class ModelTest(TestCase): class ModelTest(TestCase):
@ -17,7 +17,7 @@ class ModelTest(TestCase):
cls.package = PackageManager.create_package(cls.user, "Anon Test Package", "No Desc") cls.package = PackageManager.create_package(cls.user, "Anon Test Package", "No Desc")
cls.BBD_SUBSET = Package.objects.get(name="Fixtures") cls.BBD_SUBSET = Package.objects.get(name="Fixtures")
def test_mlrr(self): def test_smoke(self):
with TemporaryDirectory() as tmpdir: with TemporaryDirectory() as tmpdir:
with self.settings(MODEL_DIR=tmpdir): with self.settings(MODEL_DIR=tmpdir):
threshold = float(0.5) threshold = float(0.5)
@ -35,9 +35,21 @@ class ModelTest(TestCase):
description="Created MLRelativeReasoning in Testcase", description="Created MLRelativeReasoning in Testcase",
) )
# mod = RuleBasedRelativeReasoning.create(
# self.package,
# rule_package_objs,
# data_package_objs,
# eval_packages_objs,
# threshold=threshold,
# min_count=5,
# max_count=0,
# name='ECC - BBD - 0.5',
# description='Created MLRelativeReasoning in Testcase',
# )
mod.build_dataset() mod.build_dataset()
mod.build_model() mod.build_model()
mod.evaluate_model(True, eval_packages_objs, n_splits=2) mod.evaluate_model(True, eval_packages_objs)
results = mod.predict("CCN(CC)C(=O)C1=CC(=CC=C1)C") results = mod.predict("CCN(CC)C(=O)C1=CC(=CC=C1)C")
@ -58,57 +70,3 @@ class ModelTest(TestCase):
# from pprint import pprint # from pprint import pprint
# pprint(mod.eval_results) # pprint(mod.eval_results)
def test_applicability(self):
with TemporaryDirectory() as tmpdir:
with self.settings(MODEL_DIR=tmpdir):
threshold = float(0.5)
rule_package_objs = [self.BBD_SUBSET]
data_package_objs = [self.BBD_SUBSET]
eval_packages_objs = [self.BBD_SUBSET]
mod = MLRelativeReasoning.create(
self.package,
rule_package_objs,
data_package_objs,
threshold=threshold,
name="ECC - BBD - 0.5",
description="Created MLRelativeReasoning in Testcase",
build_app_domain=True, # To test the applicability domain this must be True
app_domain_num_neighbours=5,
app_domain_local_compatibility_threshold=0.5,
app_domain_reliability_threshold=0.5,
)
mod.build_dataset()
mod.build_model()
mod.evaluate_model(True, eval_packages_objs, n_splits=2)
results = mod.predict("CCN(CC)C(=O)C1=CC(=CC=C1)C")
def test_rbrr(self):
with TemporaryDirectory() as tmpdir:
with self.settings(MODEL_DIR=tmpdir):
threshold = float(0.5)
rule_package_objs = [self.BBD_SUBSET]
data_package_objs = [self.BBD_SUBSET]
eval_packages_objs = [self.BBD_SUBSET]
mod = RuleBasedRelativeReasoning.create(
self.package,
rule_package_objs,
data_package_objs,
threshold=threshold,
min_count=5,
max_count=0,
name='ECC - BBD - 0.5',
description='Created MLRelativeReasoning in Testcase',
)
mod.build_dataset()
mod.build_model()
mod.evaluate_model(True, eval_packages_objs, n_splits=2)
results = mod.predict("CCN(CC)C(=O)C1=CC(=CC=C1)C")

View File

@ -29,7 +29,7 @@ class RuleTest(TestCase):
self.assertEqual(r.name, "bt0022-2833") self.assertEqual(r.name, "bt0022-2833")
self.assertEqual( self.assertEqual(
r.description, r.description,
"Dihalomethyl derivative + Halomethyl derivative &gt; 1-Halo-1-methylalcohol derivative + 1-Methylalcohol derivative", "Dihalomethyl derivative + Halomethyl derivative > 1-Halo-1-methylalcohol derivative + 1-Methylalcohol derivative",
) )
def test_smirks_are_trimmed(self): def test_smirks_are_trimmed(self):

View File

@ -2,12 +2,13 @@ import logging
import re import re
from abc import ABC from abc import ABC
from collections import defaultdict from collections import defaultdict
from typing import List, Optional, Dict, TYPE_CHECKING from typing import List, Optional, Dict, TYPE_CHECKING, Union
from indigo import Indigo, IndigoException, IndigoObject from indigo import Indigo, IndigoException, IndigoObject
from indigo.renderer import IndigoRenderer from indigo.renderer import IndigoRenderer
from rdkit import Chem, rdBase from rdkit import Chem, rdBase
from rdkit.Chem import MACCSkeys, Descriptors, rdFingerprintGenerator from rdkit.Chem import MACCSkeys, Descriptors
from rdkit.Chem import rdchem
from rdkit.Chem import rdChemReactions from rdkit.Chem import rdChemReactions
from rdkit.Chem.Draw import rdMolDraw2D from rdkit.Chem.Draw import rdMolDraw2D
from rdkit.Chem.MolStandardize import rdMolStandardize from rdkit.Chem.MolStandardize import rdMolStandardize
@ -90,8 +91,15 @@ class FormatConverter(object):
return Chem.MolToSmiles(mol, canonical=canonical) return Chem.MolToSmiles(mol, canonical=canonical)
@staticmethod @staticmethod
def InChIKey(smiles): def InChIKey(mol_or_smiles: Union[rdchem.Mol | str]):
return Chem.MolToInchiKey(FormatConverter.from_smiles(smiles)) if isinstance(mol_or_smiles, str):
mol_or_smiles = mol_or_smiles.replace("~", "")
mol_or_smiles = FormatConverter.from_smiles(mol_or_smiles)
if mol_or_smiles is None:
return None
return Chem.MolToInchiKey(mol_or_smiles)
@staticmethod @staticmethod
def InChI(smiles): def InChI(smiles):
@ -107,13 +115,6 @@ class FormatConverter(object):
bitvec = MACCSkeys.GenMACCSKeys(mol) bitvec = MACCSkeys.GenMACCSKeys(mol)
return bitvec.ToList() return bitvec.ToList()
@staticmethod
def morgan(smiles, radius=3, fpSize=2048):
finger_gen = rdFingerprintGenerator.GetMorganGenerator(radius=radius, fpSize=fpSize)
mol = Chem.MolFromSmiles(smiles)
fp = finger_gen.GetFingerprint(mol)
return fp.ToList()
@staticmethod @staticmethod
def get_functional_groups(smiles: str) -> List[str]: def get_functional_groups(smiles: str) -> List[str]:
res = list() res = list()
@ -255,30 +256,6 @@ class FormatConverter(object):
except Exception: except Exception:
return False return False
@staticmethod
def is_valid_smarts(smarts: str) -> bool:
"""
Checks whether a given string is a valid SMARTS pattern.
Parameters
----------
smarts : str
The SMARTS string to validate.
Returns
-------
bool
True if the SMARTS string is valid, False otherwise.
"""
if not isinstance(smarts, str) or not smarts.strip():
return False
try:
mol = Chem.MolFromSmarts(smarts)
return mol is not None
except Exception:
return False
@staticmethod @staticmethod
def apply( def apply(
smiles: str, smiles: str,
@ -319,7 +296,8 @@ class FormatConverter(object):
product = GetMolFrags(product, asMols=True) product = GetMolFrags(product, asMols=True)
for p in product: for p in product:
p = FormatConverter.standardize( p = FormatConverter.standardize(
Chem.MolToSmiles(p), remove_stereo=remove_stereo Chem.MolToSmiles(p).replace("~", ""),
remove_stereo=remove_stereo,
) )
prods.append(p) prods.append(p)

View File

@ -35,6 +35,7 @@ from epdb.models import (
RuleBasedRelativeReasoning, RuleBasedRelativeReasoning,
Scenario, Scenario,
SequentialRule, SequentialRule,
Setting,
SimpleAmbitRule, SimpleAmbitRule,
SimpleRDKitRule, SimpleRDKitRule,
SimpleRule, SimpleRule,
@ -1258,3 +1259,46 @@ class PathwayUtils:
res[edge.url] = rule_chain res[edge.url] = rule_chain
return res return res
def _find_intermediates(self, data_pathway, pred_pathway):
pass
def engineer(self, setting: "Setting"):
from epdb.logic import SPathway
# get a fresh copy
pw = Pathway.objects.get(id=self.pathway.pk)
root_nodes = [n.default_node_label.smiles for n in pw.root_nodes]
if len(root_nodes) != 1:
logger.warning(f"Pathway {pw.name} has {len(root_nodes)} root nodes")
return
spw = SPathway(root_nodes[0], None, setting)
level = 0
while not spw.done:
spw.predict_step(from_depth=level)
level += 1
# Generate Node / SMILES mapping
node_mapping = {}
from utilities.chem import FormatConverter
for node in pw.nodes:
for snode in spw.smiles_to_node.values():
data_smiles = node.default_node_label.smiles
pred_smiles = snode.smiles
data_key = FormatConverter.InChIKey(data_smiles.replace("~", ""))
pred_key = FormatConverter.InChIKey(pred_smiles.replace("~", ""))
if data_key == pred_key:
node_mapping[snode] = node
print(node_mapping)
return spw
pass

View File

@ -5,14 +5,11 @@ import logging
from collections import defaultdict from collections import defaultdict
from datetime import datetime from datetime import datetime
from pathlib import Path from pathlib import Path
from typing import List, Dict, Set, Tuple, TYPE_CHECKING, Callable from typing import List, Dict, Set, Tuple, TYPE_CHECKING
from abc import ABC, abstractmethod
import networkx as nx import networkx as nx
import numpy as np import numpy as np
from envipy_plugins import Descriptor
from numpy.random import default_rng from numpy.random import default_rng
import polars as pl
from sklearn.base import BaseEstimator, ClassifierMixin from sklearn.base import BaseEstimator, ClassifierMixin
from sklearn.decomposition import PCA from sklearn.decomposition import PCA
from sklearn.dummy import DummyClassifier from sklearn.dummy import DummyClassifier
@ -29,281 +26,70 @@ if TYPE_CHECKING:
from epdb.models import Rule, CompoundStructure, Reaction from epdb.models import Rule, CompoundStructure, Reaction
class Dataset(ABC): class Dataset:
def __init__(self, columns: List[str] = None, data: List[List[str | int | float]] | pl.DataFrame = None): def __init__(
if isinstance(data, pl.DataFrame): # Allows for re-creation of self in cases like indexing with __getitem__ self, columns: List[str], num_labels: int, data: List[List[str | int | float]] = None
self.df = data ):
self.columns: List[str] = columns
self.num_labels: int = num_labels
if data is None:
self.data: List[List[str | int | float]] = list()
else: else:
# Build either an empty dataframe with columns or fill it with list of list data self.data = data
if data is not None and len(columns) != len(data[0]):
raise ValueError(f"Header and Data are not aligned {len(columns)} columns vs. {len(data[0])} columns")
if columns is None:
raise ValueError("Columns can't be None if data is not already a DataFrame")
self.df = pl.DataFrame(data=data, schema=columns, orient="row", infer_schema_length=None)
def add_rows(self, rows: List[List[str | int | float]]): self.num_features: int = len(columns) - self.num_labels
"""Add rows to the dataset. Extends the polars dataframe stored in self""" self._struct_features: Tuple[int, int] = self._block_indices("feature_")
if len(self.columns) != len(rows[0]): self._triggered: Tuple[int, int] = self._block_indices("trig_")
raise ValueError(f"Header and Data are not aligned {len(self.columns)} columns vs. {len(rows[0])} columns") self._observed: Tuple[int, int] = self._block_indices("obs_")
new_rows = pl.DataFrame(data=rows, schema=self.columns, orient="row", infer_schema_length=None)
self.df.extend(new_rows)
def add_row(self, row: List[str | int | float]): def _block_indices(self, prefix) -> Tuple[int, int]:
"""See add_rows"""
self.add_rows([row])
def block_indices(self, prefix) -> List[int]:
"""Find the indexes in column labels that has the prefix"""
indices: List[int] = [] indices: List[int] = []
for i, feature in enumerate(self.columns): for i, feature in enumerate(self.columns):
if feature.startswith(prefix): if feature.startswith(prefix):
indices.append(i) indices.append(i)
return indices
@property return min(indices), max(indices)
def columns(self) -> List[str]:
"""Use the polars dataframe columns"""
return self.df.columns
@property def structure_id(self):
def shape(self): return self.data[0][0]
return self.df.shape
@abstractmethod def add_row(self, row: List[str | int | float]):
def X(self, **kwargs): if len(self.columns) != len(row):
pass raise ValueError(f"Header and Data are not aligned {len(self.columns)} vs. {len(row)}")
self.data.append(row)
@abstractmethod
def y(self, **kwargs):
pass
@staticmethod
@abstractmethod
def generate_dataset(reactions, *args, **kwargs):
pass
def __iter__(self):
"""Use polars iter_rows for iterating over the dataset"""
return self.df.iter_rows()
def __getitem__(self, item):
"""Item is passed to polars allowing for advanced indexing.
See https://docs.pola.rs/api/python/stable/reference/dataframe/api/polars.DataFrame.__getitem__.html#polars.DataFrame.__getitem__"""
res = self.df[item]
if isinstance(res, pl.DataFrame): # If we get a dataframe back from indexing make new self with res dataframe
return self.__class__(data=res)
else: # If we don't get a dataframe back (likely base type, int, str, float etc.) return the item
return res
def save(self, path: "Path | str"):
import pickle
with open(path, "wb") as fh:
pickle.dump(self, fh)
@staticmethod
def load(path: "str | Path") -> "Dataset":
import pickle
return pickle.load(open(path, "rb"))
def to_numpy(self):
return self.df.to_numpy()
def __repr__(self):
return (
f"<{self.__class__.__name__} #rows={len(self.df)} #cols={len(self.columns)}>"
)
def __len__(self):
return len(self.df)
def iter_rows(self, named=False):
return self.df.iter_rows(named=named)
def filter(self, *predicates, **constraints):
return self.__class__(data=self.df.filter(*predicates, **constraints))
def select(self, *exprs, **named_exprs):
return self.__class__(data=self.df.select(*exprs, **named_exprs))
def with_columns(self, *exprs, **name_exprs):
return self.__class__(data=self.df.with_columns(*exprs, **name_exprs))
def sort(self, by, *more_by, descending=False, nulls_last=False, multithreaded=True, maintain_order=False):
return self.__class__(data=self.df.sort(by, *more_by, descending=descending, nulls_last=nulls_last,
multithreaded=multithreaded, maintain_order=maintain_order))
def item(self, row=None, column=None):
return self.df.item(row, column)
def fill_nan(self, value):
return self.__class__(data=self.df.fill_nan(value))
@property
def height(self):
return self.df.height
class RuleBasedDataset(Dataset):
def __init__(self, num_labels=None, columns=None, data=None):
super().__init__(columns, data)
# Calculating num_labels allows functions like getitem to be in the base Dataset as it unifies the init.
self.num_labels: int = num_labels if num_labels else sum([1 for c in self.columns if "obs_" in c])
# Pre-calculate the ids of columns for features/labels, useful later in X and y
self._struct_features: List[int] = self.block_indices("feature_")
self._triggered: List[int] = self.block_indices("trig_")
self._observed: List[int] = self.block_indices("obs_")
self.feature_cols: List[int] = self._struct_features + self._triggered
self.num_features: int = len(self.feature_cols)
self.has_probs = False
def times_triggered(self, rule_uuid) -> int: def times_triggered(self, rule_uuid) -> int:
"""Count how many times a rule is triggered by the number of rows with one in the rules trig column""" idx = self.columns.index(f"trig_{rule_uuid}")
return self.df.filter(pl.col(f"trig_{rule_uuid}") == 1).height
def struct_features(self) -> List[int]: times_triggered = 0
for row in self.data:
if row[idx] == 1:
times_triggered += 1
return times_triggered
def struct_features(self) -> Tuple[int, int]:
return self._struct_features return self._struct_features
def triggered(self) -> List[int]: def triggered(self) -> Tuple[int, int]:
return self._triggered return self._triggered
def observed(self) -> List[int]: def observed(self) -> Tuple[int, int]:
return self._observed return self._observed
def structure_id(self, index: int): def at(self, position: int) -> Dataset:
"""Get the UUID of a compound""" return Dataset(self.columns, self.num_labels, [self.data[position]])
return self.item(index, "structure_id")
def X(self, exclude_id_col=True, na_replacement=0): def limit(self, limit: int) -> Dataset:
"""Get all the feature and trig columns""" return Dataset(self.columns, self.num_labels, self.data[:limit])
_col_ids = self.feature_cols
if not exclude_id_col:
_col_ids = [0] + _col_ids
res = self[:, _col_ids]
if na_replacement is not None:
res.df = res.df.fill_null(na_replacement)
return res
def trig(self, na_replacement=0): def __iter__(self):
"""Get all the trig columns""" return (self.at(i) for i, _ in enumerate(self.data))
res = self[:, self._triggered]
if na_replacement is not None:
res.df = res.df.fill_null(na_replacement)
return res
def y(self, na_replacement=0):
"""Get all the obs columns"""
res = self[:, self._observed]
if na_replacement is not None:
res.df = res.df.fill_null(na_replacement)
return res
@staticmethod
def generate_dataset(reactions, applicable_rules, educts_only=True, feat_funcs: List["Callable | Descriptor"]=None):
if feat_funcs is None:
feat_funcs = [FormatConverter.maccs]
_structures = set() # Get all the structures
for r in reactions:
_structures.update(r.educts.all())
if not educts_only:
_structures.update(r.products.all())
compounds = sorted(_structures, key=lambda x: x.url)
triggered: Dict[str, Set[str]] = defaultdict(set)
observed: Set[str] = set()
# Apply rules on collected compounds and store tps
for i, comp in enumerate(compounds):
logger.debug(f"{i + 1}/{len(compounds)}...")
for rule in applicable_rules:
product_sets = rule.apply(comp.smiles)
if len(product_sets) == 0:
continue
key = f"{rule.uuid} + {comp.uuid}"
if key in triggered:
logger.info(f"{key} already present. Duplicate reaction?")
for prod_set in product_sets:
for smi in prod_set:
try:
smi = FormatConverter.standardize(smi, remove_stereo=True)
except Exception:
logger.debug(f"Standardizing SMILES failed for {smi}")
triggered[key].add(smi)
for i, r in enumerate(reactions):
logger.debug(f"{i + 1}/{len(reactions)}...")
if len(r.educts.all()) != 1:
logger.debug(f"Skipping {r.url} as it has {len(r.educts.all())} substrates!")
continue
for comp in r.educts.all():
for rule in applicable_rules:
key = f"{rule.uuid} + {comp.uuid}"
if key not in triggered:
continue
# standardize products from reactions for comparison
standardized_products = []
for cs in r.products.all():
smi = cs.smiles
try:
smi = FormatConverter.standardize(smi, remove_stereo=True)
except Exception as e:
logger.debug(f"Standardizing SMILES failed for {smi}")
standardized_products.append(smi)
if len(set(standardized_products).difference(triggered[key])) == 0:
observed.add(key)
feat_columns = []
for feat_func in feat_funcs:
if isinstance(feat_func, Descriptor):
feats = feat_func.get_molecule_descriptors(compounds[0].smiles)
else:
feats = feat_func(compounds[0].smiles)
start_i = len(feat_columns)
feat_columns.extend([f"feature_{start_i + i}" for i, _ in enumerate(feats)])
ds_columns = (["structure_id"] +
feat_columns +
[f"trig_{r.uuid}" for r in applicable_rules] +
[f"obs_{r.uuid}" for r in applicable_rules])
rows = []
for i, comp in enumerate(compounds):
# Features
feats = []
for feat_func in feat_funcs:
if isinstance(feat_func, Descriptor):
feat = feat_func.get_molecule_descriptors(comp.smiles)
else:
feat = feat_func(comp.smiles)
feats.extend(feat)
trig = []
obs = []
for rule in applicable_rules:
key = f"{rule.uuid} + {comp.uuid}"
# Check triggered
if key in triggered:
trig.append(1)
else:
trig.append(0)
# Check obs
if key in observed:
obs.append(1)
elif key not in triggered:
obs.append(None)
else:
obs.append(0)
rows.append([str(comp.uuid)] + feats + trig + obs)
ds = RuleBasedDataset(len(applicable_rules), ds_columns, data=rows)
return ds
def classification_dataset( def classification_dataset(
self, structures: List[str | "CompoundStructure"], applicable_rules: List["Rule"] self, structures: List[str | "CompoundStructure"], applicable_rules: List["Rule"]
) -> Tuple[RuleBasedDataset, List[List[PredictionResult]]]: ) -> Tuple[Dataset, List[List[PredictionResult]]]:
classify_data = [] classify_data = []
classify_products = [] classify_products = []
for struct in structures: for struct in structures:
@ -327,18 +113,186 @@ class RuleBasedDataset(Dataset):
else: else:
trig.append(0) trig.append(0)
prods.append([]) prods.append([])
new_row = [struct_id] + features + trig + ([-1] * len(trig))
if self.has_probs:
new_row += [-1] * len(trig)
classify_data.append(new_row)
classify_products.append(prods)
ds = RuleBasedDataset(len(applicable_rules), self.columns, data=classify_data)
return ds, classify_products
def add_probs(self, probs): classify_data.append([struct_id] + features + trig + ([-1] * len(trig)))
col_names = [f"prob_{self.columns[r_id].split('_')[-1]}" for r_id in self._observed] classify_products.append(prods)
self.df = self.df.with_columns(*[pl.Series(name, probs[:, j]) for j, name in enumerate(col_names)])
self.has_probs = True return Dataset(
columns=self.columns, num_labels=self.num_labels, data=classify_data
), classify_products
@staticmethod
def generate_dataset(
reactions: List["Reaction"], applicable_rules: List["Rule"], educts_only: bool = True
) -> Dataset:
_structures = set()
for r in reactions:
for e in r.educts.all():
_structures.add(e)
if not educts_only:
for e in r.products:
_structures.add(e)
compounds = sorted(_structures, key=lambda x: x.url)
triggered: Dict[str, Set[str]] = defaultdict(set)
observed: Set[str] = set()
# Apply rules on collected compounds and store tps
for i, comp in enumerate(compounds):
logger.debug(f"{i + 1}/{len(compounds)}...")
for rule in applicable_rules:
product_sets = rule.apply(comp.smiles)
if len(product_sets) == 0:
continue
key = f"{rule.uuid} + {comp.uuid}"
if key in triggered:
logger.info(f"{key} already present. Duplicate reaction?")
for prod_set in product_sets:
for smi in prod_set:
try:
smi = FormatConverter.standardize(smi, remove_stereo=True)
except Exception:
# :shrug:
logger.debug(f"Standardizing SMILES failed for {smi}")
pass
triggered[key].add(smi)
for i, r in enumerate(reactions):
logger.debug(f"{i + 1}/{len(reactions)}...")
if len(r.educts.all()) != 1:
logger.debug(f"Skipping {r.url} as it has {len(r.educts.all())} substrates!")
continue
for comp in r.educts.all():
for rule in applicable_rules:
key = f"{rule.uuid} + {comp.uuid}"
if key not in triggered:
continue
# standardize products from reactions for comparison
standardized_products = []
for cs in r.products.all():
smi = cs.smiles
try:
smi = FormatConverter.standardize(smi, remove_stereo=True)
except Exception as e:
# :shrug:
logger.debug(f"Standardizing SMILES failed for {smi}")
pass
standardized_products.append(smi)
if len(set(standardized_products).difference(triggered[key])) == 0:
observed.add(key)
else:
pass
ds = None
for i, comp in enumerate(compounds):
# Features
feat = FormatConverter.maccs(comp.smiles)
trig = []
obs = []
for rule in applicable_rules:
key = f"{rule.uuid} + {comp.uuid}"
# Check triggered
if key in triggered:
trig.append(1)
else:
trig.append(0)
# Check obs
if key in observed:
obs.append(1)
elif key not in triggered:
obs.append(None)
else:
obs.append(0)
if ds is None:
header = (
["structure_id"]
+ [f"feature_{i}" for i, _ in enumerate(feat)]
+ [f"trig_{r.uuid}" for r in applicable_rules]
+ [f"obs_{r.uuid}" for r in applicable_rules]
)
ds = Dataset(header, len(applicable_rules))
ds.add_row([str(comp.uuid)] + feat + trig + obs)
return ds
def X(self, exclude_id_col=True, na_replacement=0):
res = self.__getitem__(
(slice(None), slice(1 if exclude_id_col else 0, len(self.columns) - self.num_labels))
)
if na_replacement is not None:
res = [[x if x is not None else na_replacement for x in row] for row in res]
return res
def trig(self, na_replacement=0):
res = self.__getitem__((slice(None), slice(self._triggered[0], self._triggered[1])))
if na_replacement is not None:
res = [[x if x is not None else na_replacement for x in row] for row in res]
return res
def y(self, na_replacement=0):
res = self.__getitem__((slice(None), slice(len(self.columns) - self.num_labels, None)))
if na_replacement is not None:
res = [[x if x is not None else na_replacement for x in row] for row in res]
return res
def __getitem__(self, key):
if not isinstance(key, tuple):
raise TypeError("Dataset must be indexed with dataset[rows, columns]")
row_key, col_key = key
# Normalize rows
if isinstance(row_key, int):
rows = [self.data[row_key]]
else:
rows = self.data[row_key]
# Normalize columns
if isinstance(col_key, int):
res = [row[col_key] for row in rows]
else:
res = [
[row[i] for i in range(*col_key.indices(len(row)))]
if isinstance(col_key, slice)
else [row[i] for i in col_key]
for row in rows
]
return res
def save(self, path: "Path"):
import pickle
with open(path, "wb") as fh:
pickle.dump(self, fh)
@staticmethod
def load(path: "Path") -> "Dataset":
import pickle
return pickle.load(open(path, "rb"))
def to_arff(self, path: "Path"): def to_arff(self, path: "Path"):
arff = f"@relation 'enviPy-dataset: -C {self.num_labels}'\n" arff = f"@relation 'enviPy-dataset: -C {self.num_labels}'\n"
@ -350,7 +304,7 @@ class RuleBasedDataset(Dataset):
arff += f"@attribute {c} {{0,1}}\n" arff += f"@attribute {c} {{0,1}}\n"
arff += "\n@data\n" arff += "\n@data\n"
for d in self: for d in self.data:
ys = ",".join([str(v if v is not None else "?") for v in d[-self.num_labels :]]) ys = ",".join([str(v if v is not None else "?") for v in d[-self.num_labels :]])
xs = ",".join([str(v if v is not None else "?") for v in d[: self.num_features]]) xs = ",".join([str(v if v is not None else "?") for v in d[: self.num_features]])
arff += f"{ys},{xs}\n" arff += f"{ys},{xs}\n"
@ -359,40 +313,10 @@ class RuleBasedDataset(Dataset):
fh.write(arff) fh.write(arff)
fh.flush() fh.flush()
def __repr__(self):
class EnviFormerDataset(Dataset): return (
def __init__(self, columns=None, data=None): f"<Dataset #rows={len(self.data)} #cols={len(self.columns)} #labels={self.num_labels}>"
super().__init__(columns, data) )
def X(self):
"""Return the educts"""
return self["educts"]
def y(self):
"""Return the products"""
return self["products"]
@staticmethod
def generate_dataset(reactions, *args, **kwargs):
# Standardise reactions for the training data
stereo = kwargs.get("stereo", False)
rows = []
for reaction in reactions:
e = ".".join(
[
FormatConverter.standardize(smile.smiles, remove_stereo=not stereo)
for smile in reaction.educts.all()
]
)
p = ".".join(
[
FormatConverter.standardize(smile.smiles, remove_stereo=not stereo)
for smile in reaction.products.all()
]
)
rows.append([e, p])
ds = EnviFormerDataset(["educts", "products"], rows)
return ds
class SparseLabelECC(BaseEstimator, ClassifierMixin): class SparseLabelECC(BaseEstimator, ClassifierMixin):
@ -574,7 +498,7 @@ class EnsembleClassifierChain:
self.classifiers = [] self.classifiers = []
if self.num_labels is None: if self.num_labels is None:
self.num_labels = Y.shape[1] self.num_labels = len(Y[0])
for p in range(self.num_chains): for p in range(self.num_chains):
logger.debug(f"{datetime.now()} fitting {p + 1}/{self.num_chains}") logger.debug(f"{datetime.now()} fitting {p + 1}/{self.num_chains}")
@ -605,7 +529,7 @@ class RelativeReasoning:
def fit(self, X, Y): def fit(self, X, Y):
n_instances = len(Y) n_instances = len(Y)
n_attributes = Y.shape[1] n_attributes = len(Y[0])
for i in range(n_attributes): for i in range(n_attributes):
for j in range(n_attributes): for j in range(n_attributes):
@ -617,8 +541,8 @@ class RelativeReasoning:
countboth = 0 countboth = 0
for k in range(n_instances): for k in range(n_instances):
vi = Y[k, i] vi = Y[k][i]
vj = Y[k, j] vj = Y[k][j]
if vi is None or vj is None: if vi is None or vj is None:
continue continue
@ -674,7 +598,7 @@ class ApplicabilityDomainPCA(PCA):
self.min_vals = None self.min_vals = None
self.max_vals = None self.max_vals = None
def build(self, train_dataset: "RuleBasedDataset"): def build(self, train_dataset: "Dataset"):
# transform # transform
X_scaled = self.scaler.fit_transform(train_dataset.X()) X_scaled = self.scaler.fit_transform(train_dataset.X())
# fit pca # fit pca
@ -688,7 +612,7 @@ class ApplicabilityDomainPCA(PCA):
instances_pca = self.transform(instances_scaled) instances_pca = self.transform(instances_scaled)
return instances_pca return instances_pca
def is_applicable(self, classify_instances: "RuleBasedDataset"): def is_applicable(self, classify_instances: "Dataset"):
instances_pca = self.__transform(classify_instances.X()) instances_pca = self.__transform(classify_instances.X())
is_applicable = [] is_applicable = []

213
uv.lock generated
View File

@ -1,10 +1,6 @@
version = 1 version = 1
revision = 2 revision = 3
requires-python = ">=3.12" requires-python = ">=3.12"
resolution-markers = [
"sys_platform == 'linux' or sys_platform == 'win32'",
"sys_platform != 'linux' and sys_platform != 'win32'",
]
[[package]] [[package]]
name = "aiohappyeyeballs" name = "aiohappyeyeballs"
@ -180,19 +176,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/c9/af/0dcccc7fdcdf170f9a1585e5e96b6fb0ba1749ef6be8c89a6202284759bd/celery-5.5.3-py3-none-any.whl", hash = "sha256:0b5761a07057acee94694464ca482416b959568904c9dfa41ce8413a7d65d525", size = 438775, upload-time = "2025-06-01T11:08:09.94Z" }, { url = "https://files.pythonhosted.org/packages/c9/af/0dcccc7fdcdf170f9a1585e5e96b6fb0ba1749ef6be8c89a6202284759bd/celery-5.5.3-py3-none-any.whl", hash = "sha256:0b5761a07057acee94694464ca482416b959568904c9dfa41ce8413a7d65d525", size = 438775, upload-time = "2025-06-01T11:08:09.94Z" },
] ]
[[package]]
name = "celery-stubs"
version = "0.1.3"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "mypy" },
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/98/14/b853ada8706a3a301396566b6dd405d1cbb24bff756236a12a01dbe766a4/celery-stubs-0.1.3.tar.gz", hash = "sha256:0fb5345820f8a2bd14e6ffcbef2d10181e12e40f8369f551d7acc99d8d514919", size = 46583, upload-time = "2023-02-10T02:20:11.837Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/1c/7a/4ab2347d13f1f59d10a7337feb9beb002664119f286036785284c6bec150/celery_stubs-0.1.3-py3-none-any.whl", hash = "sha256:dfb9ad27614a8af028b2055bb4a4ae99ca5e9a8d871428a506646d62153218d7", size = 89085, upload-time = "2023-02-10T02:20:09.409Z" },
]
[[package]] [[package]]
name = "certifi" name = "certifi"
version = "2025.10.5" version = "2025.10.5"
@ -542,14 +525,13 @@ wheels = [
[[package]] [[package]]
name = "enviformer" name = "enviformer"
version = "0.1.0" version = "0.1.0"
source = { git = "ssh://git@git.envipath.com/enviPath/enviformer.git?rev=v0.1.4#7094be5767748fd63d4a84a5d71f06cf02ba07f3" } source = { git = "ssh://git@git.envipath.com/enviPath/enviformer.git?rev=v0.1.2#3f28f60cfa1df814cf7559303b5130933efa40ae" }
dependencies = [ dependencies = [
{ name = "joblib" }, { name = "joblib" },
{ name = "lightning" }, { name = "lightning" },
{ name = "pytorch-lightning" }, { name = "pytorch-lightning" },
{ name = "scikit-learn" }, { name = "scikit-learn" },
{ name = "torch", version = "2.8.0", source = { registry = "https://pypi.org/simple" }, marker = "sys_platform != 'linux' and sys_platform != 'win32'" }, { name = "torch" },
{ name = "torch", version = "2.8.0+cu128", source = { registry = "https://download.pytorch.org/whl/cu128" }, marker = "sys_platform == 'linux' or sys_platform == 'win32'" },
] ]
[[package]] [[package]]
@ -564,6 +546,7 @@ dependencies = [
{ name = "django-ninja" }, { name = "django-ninja" },
{ name = "django-oauth-toolkit" }, { name = "django-oauth-toolkit" },
{ name = "django-polymorphic" }, { name = "django-polymorphic" },
{ name = "django-stubs" },
{ name = "enviformer" }, { name = "enviformer" },
{ name = "envipy-additional-information" }, { name = "envipy-additional-information" },
{ name = "envipy-ambit" }, { name = "envipy-ambit" },
@ -571,8 +554,6 @@ dependencies = [
{ name = "epam-indigo" }, { name = "epam-indigo" },
{ name = "gunicorn" }, { name = "gunicorn" },
{ name = "networkx" }, { name = "networkx" },
{ name = "nh3" },
{ name = "polars" },
{ name = "psycopg2-binary" }, { name = "psycopg2-binary" },
{ name = "python-dotenv" }, { name = "python-dotenv" },
{ name = "rdkit" }, { name = "rdkit" },
@ -585,8 +566,6 @@ dependencies = [
[package.optional-dependencies] [package.optional-dependencies]
dev = [ dev = [
{ name = "celery-stubs" },
{ name = "django-stubs" },
{ name = "poethepoet" }, { name = "poethepoet" },
{ name = "pre-commit" }, { name = "pre-commit" },
{ name = "ruff" }, { name = "ruff" },
@ -598,25 +577,22 @@ ms-login = [
[package.metadata] [package.metadata]
requires-dist = [ requires-dist = [
{ name = "celery", specifier = ">=5.5.2" }, { name = "celery", specifier = ">=5.5.2" },
{ name = "celery-stubs", marker = "extra == 'dev'", specifier = "==0.1.3" },
{ name = "django", specifier = ">=5.2.1" }, { name = "django", specifier = ">=5.2.1" },
{ name = "django-extensions", specifier = ">=4.1" }, { name = "django-extensions", specifier = ">=4.1" },
{ name = "django-model-utils", specifier = ">=5.0.0" }, { name = "django-model-utils", specifier = ">=5.0.0" },
{ name = "django-ninja", specifier = ">=1.4.1" }, { name = "django-ninja", specifier = ">=1.4.1" },
{ name = "django-oauth-toolkit", specifier = ">=3.0.1" }, { name = "django-oauth-toolkit", specifier = ">=3.0.1" },
{ name = "django-polymorphic", specifier = ">=4.1.0" }, { name = "django-polymorphic", specifier = ">=4.1.0" },
{ name = "django-stubs", marker = "extra == 'dev'", specifier = ">=5.2.4" }, { name = "django-stubs", specifier = ">=5.2.4" },
{ name = "enviformer", git = "ssh://git@git.envipath.com/enviPath/enviformer.git?rev=v0.1.4" }, { name = "enviformer", git = "ssh://git@git.envipath.com/enviPath/enviformer.git?rev=v0.1.2" },
{ name = "envipy-additional-information", git = "ssh://git@git.envipath.com/enviPath/enviPy-additional-information.git?rev=v0.1.7" }, { name = "envipy-additional-information", git = "ssh://git@git.envipath.com/enviPath/enviPy-additional-information.git?rev=v0.1.4" },
{ name = "envipy-ambit", git = "ssh://git@git.envipath.com/enviPath/enviPy-ambit.git" }, { name = "envipy-ambit", git = "ssh://git@git.envipath.com/enviPath/enviPy-ambit.git" },
{ name = "envipy-plugins", git = "ssh://git@git.envipath.com/enviPath/enviPy-plugins.git?rev=v0.1.0" }, { name = "envipy-plugins", git = "ssh://git@git.envipath.com/enviPath/enviPy-plugins.git?rev=v0.1.0" },
{ name = "epam-indigo", specifier = ">=1.30.1" }, { name = "epam-indigo", specifier = ">=1.30.1" },
{ name = "gunicorn", specifier = ">=23.0.0" }, { name = "gunicorn", specifier = ">=23.0.0" },
{ name = "msal", marker = "extra == 'ms-login'", specifier = ">=1.33.0" }, { name = "msal", marker = "extra == 'ms-login'", specifier = ">=1.33.0" },
{ name = "networkx", specifier = ">=3.4.2" }, { name = "networkx", specifier = ">=3.4.2" },
{ name = "nh3", specifier = "==0.3.2" },
{ name = "poethepoet", marker = "extra == 'dev'", specifier = ">=0.37.0" }, { name = "poethepoet", marker = "extra == 'dev'", specifier = ">=0.37.0" },
{ name = "polars", specifier = "==1.35.1" },
{ name = "pre-commit", marker = "extra == 'dev'", specifier = ">=4.3.0" }, { name = "pre-commit", marker = "extra == 'dev'", specifier = ">=4.3.0" },
{ name = "psycopg2-binary", specifier = ">=2.9.10" }, { name = "psycopg2-binary", specifier = ">=2.9.10" },
{ name = "python-dotenv", specifier = ">=1.1.0" }, { name = "python-dotenv", specifier = ">=1.1.0" },
@ -632,8 +608,8 @@ provides-extras = ["ms-login", "dev"]
[[package]] [[package]]
name = "envipy-additional-information" name = "envipy-additional-information"
version = "0.1.7" version = "0.1.0"
source = { git = "ssh://git@git.envipath.com/enviPath/enviPy-additional-information.git?rev=v0.1.7#d02a5d5e6a931e6565ea86127813acf7e4b33a30" } source = { git = "ssh://git@git.envipath.com/enviPath/enviPy-additional-information.git?rev=v0.1.4#4da604090bf7cf1f3f552d69485472dbc623030a" }
dependencies = [ dependencies = [
{ name = "pydantic" }, { name = "pydantic" },
] ]
@ -889,8 +865,7 @@ dependencies = [
{ name = "packaging" }, { name = "packaging" },
{ name = "pytorch-lightning" }, { name = "pytorch-lightning" },
{ name = "pyyaml" }, { name = "pyyaml" },
{ name = "torch", version = "2.8.0", source = { registry = "https://pypi.org/simple" }, marker = "sys_platform != 'linux' and sys_platform != 'win32'" }, { name = "torch" },
{ name = "torch", version = "2.8.0+cu128", source = { registry = "https://download.pytorch.org/whl/cu128" }, marker = "sys_platform == 'linux' or sys_platform == 'win32'" },
{ name = "torchmetrics" }, { name = "torchmetrics" },
{ name = "tqdm" }, { name = "tqdm" },
{ name = "typing-extensions" }, { name = "typing-extensions" },
@ -1099,47 +1074,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/b7/da/7d22601b625e241d4f23ef1ebff8acfc60da633c9e7e7922e24d10f592b3/multidict-6.7.0-py3-none-any.whl", hash = "sha256:394fc5c42a333c9ffc3e421a4c85e08580d990e08b99f6bf35b4132114c5dcb3", size = 12317, upload-time = "2025-10-06T14:52:29.272Z" }, { url = "https://files.pythonhosted.org/packages/b7/da/7d22601b625e241d4f23ef1ebff8acfc60da633c9e7e7922e24d10f592b3/multidict-6.7.0-py3-none-any.whl", hash = "sha256:394fc5c42a333c9ffc3e421a4c85e08580d990e08b99f6bf35b4132114c5dcb3", size = 12317, upload-time = "2025-10-06T14:52:29.272Z" },
] ]
[[package]]
name = "mypy"
version = "1.18.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "mypy-extensions" },
{ name = "pathspec" },
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/c0/77/8f0d0001ffad290cef2f7f216f96c814866248a0b92a722365ed54648e7e/mypy-1.18.2.tar.gz", hash = "sha256:06a398102a5f203d7477b2923dda3634c36727fa5c237d8f859ef90c42a9924b", size = 3448846, upload-time = "2025-09-19T00:11:10.519Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/07/06/dfdd2bc60c66611dd8335f463818514733bc763e4760dee289dcc33df709/mypy-1.18.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:33eca32dd124b29400c31d7cf784e795b050ace0e1f91b8dc035672725617e34", size = 12908273, upload-time = "2025-09-19T00:10:58.321Z" },
{ url = "https://files.pythonhosted.org/packages/81/14/6a9de6d13a122d5608e1a04130724caf9170333ac5a924e10f670687d3eb/mypy-1.18.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a3c47adf30d65e89b2dcd2fa32f3aeb5e94ca970d2c15fcb25e297871c8e4764", size = 11920910, upload-time = "2025-09-19T00:10:20.043Z" },
{ url = "https://files.pythonhosted.org/packages/5f/a9/b29de53e42f18e8cc547e38daa9dfa132ffdc64f7250e353f5c8cdd44bee/mypy-1.18.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5d6c838e831a062f5f29d11c9057c6009f60cb294fea33a98422688181fe2893", size = 12465585, upload-time = "2025-09-19T00:10:33.005Z" },
{ url = "https://files.pythonhosted.org/packages/77/ae/6c3d2c7c61ff21f2bee938c917616c92ebf852f015fb55917fd6e2811db2/mypy-1.18.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01199871b6110a2ce984bde85acd481232d17413868c9807e95c1b0739a58914", size = 13348562, upload-time = "2025-09-19T00:10:11.51Z" },
{ url = "https://files.pythonhosted.org/packages/4d/31/aec68ab3b4aebdf8f36d191b0685d99faa899ab990753ca0fee60fb99511/mypy-1.18.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a2afc0fa0b0e91b4599ddfe0f91e2c26c2b5a5ab263737e998d6817874c5f7c8", size = 13533296, upload-time = "2025-09-19T00:10:06.568Z" },
{ url = "https://files.pythonhosted.org/packages/9f/83/abcb3ad9478fca3ebeb6a5358bb0b22c95ea42b43b7789c7fb1297ca44f4/mypy-1.18.2-cp312-cp312-win_amd64.whl", hash = "sha256:d8068d0afe682c7c4897c0f7ce84ea77f6de953262b12d07038f4d296d547074", size = 9828828, upload-time = "2025-09-19T00:10:28.203Z" },
{ url = "https://files.pythonhosted.org/packages/5f/04/7f462e6fbba87a72bc8097b93f6842499c428a6ff0c81dd46948d175afe8/mypy-1.18.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:07b8b0f580ca6d289e69209ec9d3911b4a26e5abfde32228a288eb79df129fcc", size = 12898728, upload-time = "2025-09-19T00:10:01.33Z" },
{ url = "https://files.pythonhosted.org/packages/99/5b/61ed4efb64f1871b41fd0b82d29a64640f3516078f6c7905b68ab1ad8b13/mypy-1.18.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:ed4482847168439651d3feee5833ccedbf6657e964572706a2adb1f7fa4dfe2e", size = 11910758, upload-time = "2025-09-19T00:10:42.607Z" },
{ url = "https://files.pythonhosted.org/packages/3c/46/d297d4b683cc89a6e4108c4250a6a6b717f5fa96e1a30a7944a6da44da35/mypy-1.18.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c3ad2afadd1e9fea5cf99a45a822346971ede8685cc581ed9cd4d42eaf940986", size = 12475342, upload-time = "2025-09-19T00:11:00.371Z" },
{ url = "https://files.pythonhosted.org/packages/83/45/4798f4d00df13eae3bfdf726c9244bcb495ab5bd588c0eed93a2f2dd67f3/mypy-1.18.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a431a6f1ef14cf8c144c6b14793a23ec4eae3db28277c358136e79d7d062f62d", size = 13338709, upload-time = "2025-09-19T00:11:03.358Z" },
{ url = "https://files.pythonhosted.org/packages/d7/09/479f7358d9625172521a87a9271ddd2441e1dab16a09708f056e97007207/mypy-1.18.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7ab28cc197f1dd77a67e1c6f35cd1f8e8b73ed2217e4fc005f9e6a504e46e7ba", size = 13529806, upload-time = "2025-09-19T00:10:26.073Z" },
{ url = "https://files.pythonhosted.org/packages/71/cf/ac0f2c7e9d0ea3c75cd99dff7aec1c9df4a1376537cb90e4c882267ee7e9/mypy-1.18.2-cp313-cp313-win_amd64.whl", hash = "sha256:0e2785a84b34a72ba55fb5daf079a1003a34c05b22238da94fcae2bbe46f3544", size = 9833262, upload-time = "2025-09-19T00:10:40.035Z" },
{ url = "https://files.pythonhosted.org/packages/5a/0c/7d5300883da16f0063ae53996358758b2a2df2a09c72a5061fa79a1f5006/mypy-1.18.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:62f0e1e988ad41c2a110edde6c398383a889d95b36b3e60bcf155f5164c4fdce", size = 12893775, upload-time = "2025-09-19T00:10:03.814Z" },
{ url = "https://files.pythonhosted.org/packages/50/df/2cffbf25737bdb236f60c973edf62e3e7b4ee1c25b6878629e88e2cde967/mypy-1.18.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:8795a039bab805ff0c1dfdb8cd3344642c2b99b8e439d057aba30850b8d3423d", size = 11936852, upload-time = "2025-09-19T00:10:51.631Z" },
{ url = "https://files.pythonhosted.org/packages/be/50/34059de13dd269227fb4a03be1faee6e2a4b04a2051c82ac0a0b5a773c9a/mypy-1.18.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6ca1e64b24a700ab5ce10133f7ccd956a04715463d30498e64ea8715236f9c9c", size = 12480242, upload-time = "2025-09-19T00:11:07.955Z" },
{ url = "https://files.pythonhosted.org/packages/5b/11/040983fad5132d85914c874a2836252bbc57832065548885b5bb5b0d4359/mypy-1.18.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d924eef3795cc89fecf6bedc6ed32b33ac13e8321344f6ddbf8ee89f706c05cb", size = 13326683, upload-time = "2025-09-19T00:09:55.572Z" },
{ url = "https://files.pythonhosted.org/packages/e9/ba/89b2901dd77414dd7a8c8729985832a5735053be15b744c18e4586e506ef/mypy-1.18.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:20c02215a080e3a2be3aa50506c67242df1c151eaba0dcbc1e4e557922a26075", size = 13514749, upload-time = "2025-09-19T00:10:44.827Z" },
{ url = "https://files.pythonhosted.org/packages/25/bc/cc98767cffd6b2928ba680f3e5bc969c4152bf7c2d83f92f5a504b92b0eb/mypy-1.18.2-cp314-cp314-win_amd64.whl", hash = "sha256:749b5f83198f1ca64345603118a6f01a4e99ad4bf9d103ddc5a3200cc4614adf", size = 9982959, upload-time = "2025-09-19T00:10:37.344Z" },
{ url = "https://files.pythonhosted.org/packages/87/e3/be76d87158ebafa0309946c4a73831974d4d6ab4f4ef40c3b53a385a66fd/mypy-1.18.2-py3-none-any.whl", hash = "sha256:22a1748707dd62b58d2ae53562ffc4d7f8bcc727e8ac7cbc69c053ddc874d47e", size = 2352367, upload-time = "2025-09-19T00:10:15.489Z" },
]
[[package]]
name = "mypy-extensions"
version = "1.1.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343, upload-time = "2025-04-22T14:54:24.164Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" },
]
[[package]] [[package]]
name = "networkx" name = "networkx"
version = "3.5" version = "3.5"
@ -1149,39 +1083,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/eb/8d/776adee7bbf76365fdd7f2552710282c79a4ead5d2a46408c9043a2b70ba/networkx-3.5-py3-none-any.whl", hash = "sha256:0030d386a9a06dee3565298b4a734b68589749a544acbb6c412dc9e2489ec6ec", size = 2034406, upload-time = "2025-05-29T11:35:04.961Z" }, { url = "https://files.pythonhosted.org/packages/eb/8d/776adee7bbf76365fdd7f2552710282c79a4ead5d2a46408c9043a2b70ba/networkx-3.5-py3-none-any.whl", hash = "sha256:0030d386a9a06dee3565298b4a734b68589749a544acbb6c412dc9e2489ec6ec", size = 2034406, upload-time = "2025-05-29T11:35:04.961Z" },
] ]
[[package]]
name = "nh3"
version = "0.3.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/ca/a5/34c26015d3a434409f4d2a1cd8821a06c05238703f49283ffeb937bef093/nh3-0.3.2.tar.gz", hash = "sha256:f394759a06df8b685a4ebfb1874fb67a9cbfd58c64fc5ed587a663c0e63ec376", size = 19288, upload-time = "2025-10-30T11:17:45.948Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/5b/01/a1eda067c0ba823e5e2bb033864ae4854549e49fb6f3407d2da949106bfb/nh3-0.3.2-cp314-cp314t-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:d18957a90806d943d141cc5e4a0fefa1d77cf0d7a156878bf9a66eed52c9cc7d", size = 1419839, upload-time = "2025-10-30T11:17:09.956Z" },
{ url = "https://files.pythonhosted.org/packages/30/57/07826ff65d59e7e9cc789ef1dc405f660cabd7458a1864ab58aefa17411b/nh3-0.3.2-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:45c953e57028c31d473d6b648552d9cab1efe20a42ad139d78e11d8f42a36130", size = 791183, upload-time = "2025-10-30T11:17:11.99Z" },
{ url = "https://files.pythonhosted.org/packages/af/2f/e8a86f861ad83f3bb5455f596d5c802e34fcdb8c53a489083a70fd301333/nh3-0.3.2-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2c9850041b77a9147d6bbd6dbbf13eeec7009eb60b44e83f07fcb2910075bf9b", size = 829127, upload-time = "2025-10-30T11:17:13.192Z" },
{ url = "https://files.pythonhosted.org/packages/d8/97/77aef4daf0479754e8e90c7f8f48f3b7b8725a3b8c0df45f2258017a6895/nh3-0.3.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:403c11563e50b915d0efdb622866d1d9e4506bce590ef7da57789bf71dd148b5", size = 997131, upload-time = "2025-10-30T11:17:14.677Z" },
{ url = "https://files.pythonhosted.org/packages/41/ee/fd8140e4df9d52143e89951dd0d797f5546004c6043285289fbbe3112293/nh3-0.3.2-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:0dca4365db62b2d71ff1620ee4f800c4729849906c5dd504ee1a7b2389558e31", size = 1068783, upload-time = "2025-10-30T11:17:15.861Z" },
{ url = "https://files.pythonhosted.org/packages/87/64/bdd9631779e2d588b08391f7555828f352e7f6427889daf2fa424bfc90c9/nh3-0.3.2-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:0fe7ee035dd7b2290715baf29cb27167dddd2ff70ea7d052c958dbd80d323c99", size = 994732, upload-time = "2025-10-30T11:17:17.155Z" },
{ url = "https://files.pythonhosted.org/packages/79/66/90190033654f1f28ca98e3d76b8be1194505583f9426b0dcde782a3970a2/nh3-0.3.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a40202fd58e49129764f025bbaae77028e420f1d5b3c8e6f6fd3a6490d513868", size = 975997, upload-time = "2025-10-30T11:17:18.77Z" },
{ url = "https://files.pythonhosted.org/packages/34/30/ebf8e2e8d71fdb5a5d5d8836207177aed1682df819cbde7f42f16898946c/nh3-0.3.2-cp314-cp314t-win32.whl", hash = "sha256:1f9ba555a797dbdcd844b89523f29cdc90973d8bd2e836ea6b962cf567cadd93", size = 583364, upload-time = "2025-10-30T11:17:20.286Z" },
{ url = "https://files.pythonhosted.org/packages/94/ae/95c52b5a75da429f11ca8902c2128f64daafdc77758d370e4cc310ecda55/nh3-0.3.2-cp314-cp314t-win_amd64.whl", hash = "sha256:dce4248edc427c9b79261f3e6e2b3ecbdd9b88c267012168b4a7b3fc6fd41d13", size = 589982, upload-time = "2025-10-30T11:17:21.384Z" },
{ url = "https://files.pythonhosted.org/packages/b4/bd/c7d862a4381b95f2469704de32c0ad419def0f4a84b7a138a79532238114/nh3-0.3.2-cp314-cp314t-win_arm64.whl", hash = "sha256:019ecbd007536b67fdf76fab411b648fb64e2257ca3262ec80c3425c24028c80", size = 577126, upload-time = "2025-10-30T11:17:22.755Z" },
{ url = "https://files.pythonhosted.org/packages/b6/3e/f5a5cc2885c24be13e9b937441bd16a012ac34a657fe05e58927e8af8b7a/nh3-0.3.2-cp38-abi3-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:7064ccf5ace75825bd7bf57859daaaf16ed28660c1c6b306b649a9eda4b54b1e", size = 1431980, upload-time = "2025-10-30T11:17:25.457Z" },
{ url = "https://files.pythonhosted.org/packages/7f/f7/529a99324d7ef055de88b690858f4189379708abae92ace799365a797b7f/nh3-0.3.2-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c8745454cdd28bbbc90861b80a0111a195b0e3961b9fa2e672be89eb199fa5d8", size = 820805, upload-time = "2025-10-30T11:17:26.98Z" },
{ url = "https://files.pythonhosted.org/packages/3d/62/19b7c50ccd1fa7d0764822d2cea8f2a320f2fd77474c7a1805cb22cf69b0/nh3-0.3.2-cp38-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72d67c25a84579f4a432c065e8b4274e53b7cf1df8f792cf846abfe2c3090866", size = 803527, upload-time = "2025-10-30T11:17:28.284Z" },
{ url = "https://files.pythonhosted.org/packages/4a/ca/f022273bab5440abff6302731a49410c5ef66b1a9502ba3fbb2df998d9ff/nh3-0.3.2-cp38-abi3-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:13398e676a14d6233f372c75f52d5ae74f98210172991f7a3142a736bd92b131", size = 1051674, upload-time = "2025-10-30T11:17:29.909Z" },
{ url = "https://files.pythonhosted.org/packages/fa/f7/5728e3b32a11daf5bd21cf71d91c463f74305938bc3eb9e0ac1ce141646e/nh3-0.3.2-cp38-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:03d617e5c8aa7331bd2659c654e021caf9bba704b109e7b2b28b039a00949fe5", size = 1004737, upload-time = "2025-10-30T11:17:31.205Z" },
{ url = "https://files.pythonhosted.org/packages/53/7f/f17e0dba0a99cee29e6cee6d4d52340ef9cb1f8a06946d3a01eb7ec2fb01/nh3-0.3.2-cp38-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f2f55c4d2d5a207e74eefe4d828067bbb01300e06e2a7436142f915c5928de07", size = 911745, upload-time = "2025-10-30T11:17:32.945Z" },
{ url = "https://files.pythonhosted.org/packages/42/0f/c76bf3dba22c73c38e9b1113b017cf163f7696f50e003404ec5ecdb1e8a6/nh3-0.3.2-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7bb18403f02b655a1bbe4e3a4696c2ae1d6ae8f5991f7cacb684b1ae27e6c9f7", size = 797184, upload-time = "2025-10-30T11:17:34.226Z" },
{ url = "https://files.pythonhosted.org/packages/08/a1/73d8250f888fb0ddf1b119b139c382f8903d8bb0c5bd1f64afc7e38dad1d/nh3-0.3.2-cp38-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6d66f41672eb4060cf87c037f760bdbc6847852ca9ef8e9c5a5da18f090abf87", size = 838556, upload-time = "2025-10-30T11:17:35.875Z" },
{ url = "https://files.pythonhosted.org/packages/d1/09/deb57f1fb656a7a5192497f4a287b0ade5a2ff6b5d5de4736d13ef6d2c1f/nh3-0.3.2-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:f97f8b25cb2681d25e2338148159447e4d689aafdccfcf19e61ff7db3905768a", size = 1006695, upload-time = "2025-10-30T11:17:37.071Z" },
{ url = "https://files.pythonhosted.org/packages/b6/61/8f4d41c4ccdac30e4b1a4fa7be4b0f9914d8314a5058472f84c8e101a418/nh3-0.3.2-cp38-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:2ab70e8c6c7d2ce953d2a58102eefa90c2d0a5ed7aa40c7e29a487bc5e613131", size = 1075471, upload-time = "2025-10-30T11:17:38.225Z" },
{ url = "https://files.pythonhosted.org/packages/b0/c6/966aec0cb4705e69f6c3580422c239205d5d4d0e50fac380b21e87b6cf1b/nh3-0.3.2-cp38-abi3-musllinux_1_2_i686.whl", hash = "sha256:1710f3901cd6440ca92494ba2eb6dc260f829fa8d9196b659fa10de825610ce0", size = 1002439, upload-time = "2025-10-30T11:17:39.553Z" },
{ url = "https://files.pythonhosted.org/packages/e2/c8/97a2d5f7a314cce2c5c49f30c6f161b7f3617960ade4bfc2fd1ee092cb20/nh3-0.3.2-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:91e9b001101fb4500a2aafe3e7c92928d85242d38bf5ac0aba0b7480da0a4cd6", size = 987439, upload-time = "2025-10-30T11:17:40.81Z" },
{ url = "https://files.pythonhosted.org/packages/0d/95/2d6fc6461687d7a171f087995247dec33e8749a562bfadd85fb5dbf37a11/nh3-0.3.2-cp38-abi3-win32.whl", hash = "sha256:169db03df90da63286e0560ea0efa9b6f3b59844a9735514a1d47e6bb2c8c61b", size = 589826, upload-time = "2025-10-30T11:17:42.239Z" },
{ url = "https://files.pythonhosted.org/packages/64/9a/1a1c154f10a575d20dd634e5697805e589bbdb7673a0ad00e8da90044ba7/nh3-0.3.2-cp38-abi3-win_amd64.whl", hash = "sha256:562da3dca7a17f9077593214a9781a94b8d76de4f158f8c895e62f09573945fe", size = 596406, upload-time = "2025-10-30T11:17:43.773Z" },
{ url = "https://files.pythonhosted.org/packages/9e/7e/a96255f63b7aef032cbee8fc4d6e37def72e3aaedc1f72759235e8f13cb1/nh3-0.3.2-cp38-abi3-win_arm64.whl", hash = "sha256:cf5964d54edd405e68583114a7cba929468bcd7db5e676ae38ee954de1cfc104", size = 584162, upload-time = "2025-10-30T11:17:44.96Z" },
]
[[package]] [[package]]
name = "nodeenv" name = "nodeenv"
version = "1.9.1" version = "1.9.1"
@ -1291,7 +1192,7 @@ name = "nvidia-cudnn-cu12"
version = "9.10.2.21" version = "9.10.2.21"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "nvidia-cublas-cu12", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "nvidia-cublas-cu12" },
] ]
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/ba/51/e123d997aa098c61d029f76663dedbfb9bc8dcf8c60cbd6adbe42f76d049/nvidia_cudnn_cu12-9.10.2.21-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:949452be657fa16687d0930933f032835951ef0892b37d2d53824d1a84dc97a8", size = 706758467, upload-time = "2025-06-06T21:54:08.597Z" }, { url = "https://files.pythonhosted.org/packages/ba/51/e123d997aa098c61d029f76663dedbfb9bc8dcf8c60cbd6adbe42f76d049/nvidia_cudnn_cu12-9.10.2.21-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:949452be657fa16687d0930933f032835951ef0892b37d2d53824d1a84dc97a8", size = 706758467, upload-time = "2025-06-06T21:54:08.597Z" },
@ -1302,7 +1203,7 @@ name = "nvidia-cufft-cu12"
version = "11.3.3.83" version = "11.3.3.83"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "nvidia-nvjitlink-cu12", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "nvidia-nvjitlink-cu12" },
] ]
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/1f/13/ee4e00f30e676b66ae65b4f08cb5bcbb8392c03f54f2d5413ea99a5d1c80/nvidia_cufft_cu12-11.3.3.83-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4d2dd21ec0b88cf61b62e6b43564355e5222e4a3fb394cac0db101f2dd0d4f74", size = 193118695, upload-time = "2025-03-07T01:45:27.821Z" }, { url = "https://files.pythonhosted.org/packages/1f/13/ee4e00f30e676b66ae65b4f08cb5bcbb8392c03f54f2d5413ea99a5d1c80/nvidia_cufft_cu12-11.3.3.83-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4d2dd21ec0b88cf61b62e6b43564355e5222e4a3fb394cac0db101f2dd0d4f74", size = 193118695, upload-time = "2025-03-07T01:45:27.821Z" },
@ -1329,9 +1230,9 @@ name = "nvidia-cusolver-cu12"
version = "11.7.3.90" version = "11.7.3.90"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "nvidia-cublas-cu12", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "nvidia-cublas-cu12" },
{ name = "nvidia-cusparse-cu12", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "nvidia-cusparse-cu12" },
{ name = "nvidia-nvjitlink-cu12", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "nvidia-nvjitlink-cu12" },
] ]
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/85/48/9a13d2975803e8cf2777d5ed57b87a0b6ca2cc795f9a4f59796a910bfb80/nvidia_cusolver_cu12-11.7.3.90-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:4376c11ad263152bd50ea295c05370360776f8c3427b30991df774f9fb26c450", size = 267506905, upload-time = "2025-03-07T01:47:16.273Z" }, { url = "https://files.pythonhosted.org/packages/85/48/9a13d2975803e8cf2777d5ed57b87a0b6ca2cc795f9a4f59796a910bfb80/nvidia_cusolver_cu12-11.7.3.90-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:4376c11ad263152bd50ea295c05370360776f8c3427b30991df774f9fb26c450", size = 267506905, upload-time = "2025-03-07T01:47:16.273Z" },
@ -1342,7 +1243,7 @@ name = "nvidia-cusparse-cu12"
version = "12.5.8.93" version = "12.5.8.93"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "nvidia-nvjitlink-cu12", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "nvidia-nvjitlink-cu12" },
] ]
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/c2/f5/e1854cb2f2bcd4280c44736c93550cc300ff4b8c95ebe370d0aa7d2b473d/nvidia_cusparse_cu12-12.5.8.93-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1ec05d76bbbd8b61b06a80e1eaf8cf4959c3d4ce8e711b65ebd0443bb0ebb13b", size = 288216466, upload-time = "2025-03-07T01:48:13.779Z" }, { url = "https://files.pythonhosted.org/packages/c2/f5/e1854cb2f2bcd4280c44736c93550cc300ff4b8c95ebe370d0aa7d2b473d/nvidia_cusparse_cu12-12.5.8.93-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1ec05d76bbbd8b61b06a80e1eaf8cf4959c3d4ce8e711b65ebd0443bb0ebb13b", size = 288216466, upload-time = "2025-03-07T01:48:13.779Z" },
@ -1407,15 +1308,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/aa/18/a8444036c6dd65ba3624c63b734d3ba95ba63ace513078e1580590075d21/pastel-0.2.1-py2.py3-none-any.whl", hash = "sha256:4349225fcdf6c2bb34d483e523475de5bb04a5c10ef711263452cb37d7dd4364", size = 5955, upload-time = "2020-09-16T19:21:11.409Z" }, { url = "https://files.pythonhosted.org/packages/aa/18/a8444036c6dd65ba3624c63b734d3ba95ba63ace513078e1580590075d21/pastel-0.2.1-py2.py3-none-any.whl", hash = "sha256:4349225fcdf6c2bb34d483e523475de5bb04a5c10ef711263452cb37d7dd4364", size = 5955, upload-time = "2020-09-16T19:21:11.409Z" },
] ]
[[package]]
name = "pathspec"
version = "0.12.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/ca/bc/f35b8446f4531a7cb215605d100cd88b7ac6f44ab3fc94870c120ab3adbf/pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712", size = 51043, upload-time = "2023-12-10T22:30:45Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/cc/20/ff623b09d963f88bfde16306a54e12ee5ea43e9b597108672ff3a408aad6/pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", size = 31191, upload-time = "2023-12-10T22:30:43.14Z" },
]
[[package]] [[package]]
name = "pillow" name = "pillow"
version = "11.3.0" version = "11.3.0"
@ -1504,32 +1396,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/92/1b/5337af1a6a478d25a3e3c56b9b4b42b0a160314e02f4a0498d5322c8dac4/poethepoet-0.37.0-py3-none-any.whl", hash = "sha256:861790276315abcc8df1b4bd60e28c3d48a06db273edd3092f3c94e1a46e5e22", size = 90062, upload-time = "2025-08-11T18:00:27.595Z" }, { url = "https://files.pythonhosted.org/packages/92/1b/5337af1a6a478d25a3e3c56b9b4b42b0a160314e02f4a0498d5322c8dac4/poethepoet-0.37.0-py3-none-any.whl", hash = "sha256:861790276315abcc8df1b4bd60e28c3d48a06db273edd3092f3c94e1a46e5e22", size = 90062, upload-time = "2025-08-11T18:00:27.595Z" },
] ]
[[package]]
name = "polars"
version = "1.35.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "polars-runtime-32" },
]
sdist = { url = "https://files.pythonhosted.org/packages/9b/5b/3caad788d93304026cbf0ab4c37f8402058b64a2f153b9c62f8b30f5d2ee/polars-1.35.1.tar.gz", hash = "sha256:06548e6d554580151d6ca7452d74bceeec4640b5b9261836889b8e68cfd7a62e", size = 694881, upload-time = "2025-10-30T12:12:52.294Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/9f/4c/21a227b722534404241c2a76beceb7463469d50c775a227fc5c209eb8adc/polars-1.35.1-py3-none-any.whl", hash = "sha256:c29a933f28aa330d96a633adbd79aa5e6a6247a802a720eead9933f4613bdbf4", size = 783598, upload-time = "2025-10-30T12:11:54.668Z" },
]
[[package]]
name = "polars-runtime-32"
version = "1.35.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/df/3e/19c252e8eb4096300c1a36ec3e50a27e5fa9a1ccaf32d3927793c16abaee/polars_runtime_32-1.35.1.tar.gz", hash = "sha256:f6b4ec9cd58b31c87af1b8c110c9c986d82345f1d50d7f7595b5d447a19dc365", size = 2696218, upload-time = "2025-10-30T12:12:53.479Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/08/2c/da339459805a26105e9d9c2f07e43ca5b8baeee55acd5457e6881487a79a/polars_runtime_32-1.35.1-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:6f051a42f6ae2f26e3bc2cf1f170f2120602976e2a3ffb6cfba742eecc7cc620", size = 40525100, upload-time = "2025-10-30T12:11:58.098Z" },
{ url = "https://files.pythonhosted.org/packages/27/70/a0733568b3533481924d2ce68b279ab3d7334e5fa6ed259f671f650b7c5e/polars_runtime_32-1.35.1-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:c2232f9cf05ba59efc72d940b86c033d41fd2d70bf2742e8115ed7112a766aa9", size = 36701908, upload-time = "2025-10-30T12:12:02.166Z" },
{ url = "https://files.pythonhosted.org/packages/46/54/6c09137bef9da72fd891ba58c2962cc7c6c5cad4649c0e668d6b344a9d7b/polars_runtime_32-1.35.1-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:42f9837348557fd674477ea40a6ac8a7e839674f6dd0a199df24be91b026024c", size = 41317692, upload-time = "2025-10-30T12:12:04.928Z" },
{ url = "https://files.pythonhosted.org/packages/22/55/81c5b266a947c339edd7fbaa9e1d9614012d02418453f48b76cc177d3dd9/polars_runtime_32-1.35.1-cp39-abi3-manylinux_2_24_aarch64.whl", hash = "sha256:c873aeb36fed182d5ebc35ca17c7eb193fe83ae2ea551ee8523ec34776731390", size = 37853058, upload-time = "2025-10-30T12:12:08.342Z" },
{ url = "https://files.pythonhosted.org/packages/6c/58/be8b034d559eac515f52408fd6537be9bea095bc0388946a4e38910d3d50/polars_runtime_32-1.35.1-cp39-abi3-win_amd64.whl", hash = "sha256:35cde9453ca7032933f0e58e9ed4388f5a1e415dd0db2dd1e442c81d815e630c", size = 41289554, upload-time = "2025-10-30T12:12:11.104Z" },
{ url = "https://files.pythonhosted.org/packages/f4/7f/e0111b9e2a1169ea82cde3ded9c92683e93c26dfccd72aee727996a1ac5b/polars_runtime_32-1.35.1-cp39-abi3-win_arm64.whl", hash = "sha256:fd77757a6c9eb9865c4bfb7b07e22225207c6b7da382bd0b9bd47732f637105d", size = 36958878, upload-time = "2025-10-30T12:12:15.206Z" },
]
[[package]] [[package]]
name = "pre-commit" name = "pre-commit"
version = "4.3.0" version = "4.3.0"
@ -1804,8 +1670,7 @@ dependencies = [
{ name = "lightning-utilities" }, { name = "lightning-utilities" },
{ name = "packaging" }, { name = "packaging" },
{ name = "pyyaml" }, { name = "pyyaml" },
{ name = "torch", version = "2.8.0", source = { registry = "https://pypi.org/simple" }, marker = "sys_platform != 'linux' and sys_platform != 'win32'" }, { name = "torch" },
{ name = "torch", version = "2.8.0+cu128", source = { registry = "https://download.pytorch.org/whl/cu128" }, marker = "sys_platform == 'linux' or sys_platform == 'win32'" },
{ name = "torchmetrics" }, { name = "torchmetrics" },
{ name = "tqdm" }, { name = "tqdm" },
{ name = "typing-extensions" }, { name = "typing-extensions" },
@ -2098,40 +1963,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/32/d5/f9a850d79b0851d1d4ef6456097579a9005b31fea68726a4ae5f2d82ddd9/threadpoolctl-3.6.0-py3-none-any.whl", hash = "sha256:43a0b8fd5a2928500110039e43a5eed8480b918967083ea48dc3ab9f13c4a7fb", size = 18638, upload-time = "2025-03-13T13:49:21.846Z" }, { url = "https://files.pythonhosted.org/packages/32/d5/f9a850d79b0851d1d4ef6456097579a9005b31fea68726a4ae5f2d82ddd9/threadpoolctl-3.6.0-py3-none-any.whl", hash = "sha256:43a0b8fd5a2928500110039e43a5eed8480b918967083ea48dc3ab9f13c4a7fb", size = 18638, upload-time = "2025-03-13T13:49:21.846Z" },
] ]
[[package]]
name = "torch"
version = "2.8.0"
source = { registry = "https://pypi.org/simple" }
resolution-markers = [
"sys_platform != 'linux' and sys_platform != 'win32'",
]
dependencies = [
{ name = "filelock", marker = "sys_platform != 'linux' and sys_platform != 'win32'" },
{ name = "fsspec", marker = "sys_platform != 'linux' and sys_platform != 'win32'" },
{ name = "jinja2", marker = "sys_platform != 'linux' and sys_platform != 'win32'" },
{ name = "networkx", marker = "sys_platform != 'linux' and sys_platform != 'win32'" },
{ name = "setuptools", marker = "sys_platform != 'linux' and sys_platform != 'win32'" },
{ name = "sympy", marker = "sys_platform != 'linux' and sys_platform != 'win32'" },
{ name = "typing-extensions", marker = "sys_platform != 'linux' and sys_platform != 'win32'" },
]
wheels = [
{ url = "https://files.pythonhosted.org/packages/be/66/5c9a321b325aaecb92d4d1855421e3a055abd77903b7dab6575ca07796db/torch-2.8.0-cp312-none-macosx_11_0_arm64.whl", hash = "sha256:619c2869db3ada2c0105487ba21b5008defcc472d23f8b80ed91ac4a380283b0", size = 73630478, upload-time = "2025-08-06T14:53:57.144Z" },
{ url = "https://files.pythonhosted.org/packages/de/69/8b7b13bba430f5e21d77708b616f767683629fc4f8037564a177d20f90ed/torch-2.8.0-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:1a62a1ec4b0498930e2543535cf70b1bef8c777713de7ceb84cd79115f553767", size = 73915128, upload-time = "2025-08-06T14:54:34.769Z" },
{ url = "https://files.pythonhosted.org/packages/04/6e/650bb7f28f771af0cb791b02348db8b7f5f64f40f6829ee82aa6ce99aabe/torch-2.8.0-cp313-none-macosx_11_0_arm64.whl", hash = "sha256:7b677e17f5a3e69fdef7eb3b9da72622f8d322692930297e4ccb52fefc6c8211", size = 73632395, upload-time = "2025-08-06T14:55:28.645Z" },
]
[[package]] [[package]]
name = "torch" name = "torch"
version = "2.8.0+cu128" version = "2.8.0+cu128"
source = { registry = "https://download.pytorch.org/whl/cu128" } source = { registry = "https://download.pytorch.org/whl/cu128" }
resolution-markers = [
"sys_platform == 'linux' or sys_platform == 'win32'",
]
dependencies = [ dependencies = [
{ name = "filelock", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "filelock" },
{ name = "fsspec", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "fsspec" },
{ name = "jinja2", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "jinja2" },
{ name = "networkx", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "networkx" },
{ name = "nvidia-cublas-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, { name = "nvidia-cublas-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
{ name = "nvidia-cuda-cupti-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, { name = "nvidia-cuda-cupti-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
{ name = "nvidia-cuda-nvrtc-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, { name = "nvidia-cuda-nvrtc-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
@ -2146,10 +1986,10 @@ dependencies = [
{ name = "nvidia-nccl-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, { name = "nvidia-nccl-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
{ name = "nvidia-nvjitlink-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, { name = "nvidia-nvjitlink-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
{ name = "nvidia-nvtx-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, { name = "nvidia-nvtx-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
{ name = "setuptools", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "setuptools" },
{ name = "sympy", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "sympy" },
{ name = "triton", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, { name = "triton", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
{ name = "typing-extensions", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "typing-extensions" },
] ]
wheels = [ wheels = [
{ url = "https://download.pytorch.org/whl/cu128/torch-2.8.0%2Bcu128-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:4354fc05bb79b208d6995a04ca1ceef6a9547b1c4334435574353d381c55087c" }, { url = "https://download.pytorch.org/whl/cu128/torch-2.8.0%2Bcu128-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:4354fc05bb79b208d6995a04ca1ceef6a9547b1c4334435574353d381c55087c" },
@ -2168,8 +2008,7 @@ dependencies = [
{ name = "lightning-utilities" }, { name = "lightning-utilities" },
{ name = "numpy" }, { name = "numpy" },
{ name = "packaging" }, { name = "packaging" },
{ name = "torch", version = "2.8.0", source = { registry = "https://pypi.org/simple" }, marker = "sys_platform != 'linux' and sys_platform != 'win32'" }, { name = "torch" },
{ name = "torch", version = "2.8.0+cu128", source = { registry = "https://download.pytorch.org/whl/cu128" }, marker = "sys_platform == 'linux' or sys_platform == 'win32'" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/85/2e/48a887a59ecc4a10ce9e8b35b3e3c5cef29d902c4eac143378526e7485cb/torchmetrics-1.8.2.tar.gz", hash = "sha256:cf64a901036bf107f17a524009eea7781c9c5315d130713aeca5747a686fe7a5", size = 580679, upload-time = "2025-09-03T14:00:54.077Z" } sdist = { url = "https://files.pythonhosted.org/packages/85/2e/48a887a59ecc4a10ce9e8b35b3e3c5cef29d902c4eac143378526e7485cb/torchmetrics-1.8.2.tar.gz", hash = "sha256:cf64a901036bf107f17a524009eea7781c9c5315d130713aeca5747a686fe7a5", size = 580679, upload-time = "2025-09-03T14:00:54.077Z" }
wheels = [ wheels = [
@ -2193,7 +2032,7 @@ name = "triton"
version = "3.4.0" version = "3.4.0"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "setuptools", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "setuptools" },
] ]
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/d0/66/b1eb52839f563623d185f0927eb3530ee4d5ffe9d377cdaf5346b306689e/triton-3.4.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:31c1d84a5c0ec2c0f8e8a072d7fd150cab84a9c239eaddc6706c081bfae4eb04", size = 155560068, upload-time = "2025-07-30T19:58:37.081Z" }, { url = "https://files.pythonhosted.org/packages/d0/66/b1eb52839f563623d185f0927eb3530ee4d5ffe9d377cdaf5346b306689e/triton-3.4.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:31c1d84a5c0ec2c0f8e8a072d7fd150cab84a9c239eaddc6706c081bfae4eb04", size = 155560068, upload-time = "2025-07-30T19:58:37.081Z" },