xenosite-predict 0.2.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. xenosite_predict-0.2.0/.gitattributes +1 -0
  2. xenosite_predict-0.2.0/.github/workflows/publish.yml +57 -0
  3. xenosite_predict-0.2.0/.gitignore +55 -0
  4. xenosite_predict-0.2.0/.python-version +1 -0
  5. xenosite_predict-0.2.0/Makefile +126 -0
  6. xenosite_predict-0.2.0/PKG-INFO +197 -0
  7. xenosite_predict-0.2.0/README.md +182 -0
  8. xenosite_predict-0.2.0/pyproject.toml +79 -0
  9. xenosite_predict-0.2.0/src/xenosite/predict/__init__.py +65 -0
  10. xenosite_predict-0.2.0/src/xenosite/predict/__main__.py +51 -0
  11. xenosite_predict-0.2.0/src/xenosite/predict/_private.py +36 -0
  12. xenosite_predict-0.2.0/src/xenosite/predict/api.py +147 -0
  13. xenosite_predict-0.2.0/src/xenosite/predict/backends/__init__.py +133 -0
  14. xenosite_predict-0.2.0/src/xenosite/predict/backends/adapters.py +180 -0
  15. xenosite_predict-0.2.0/src/xenosite/predict/backends/http.py +60 -0
  16. xenosite_predict-0.2.0/src/xenosite/predict/backends/legacy.py +71 -0
  17. xenosite_predict-0.2.0/src/xenosite/predict/backends/onnx.py +111 -0
  18. xenosite_predict-0.2.0/src/xenosite/predict/compare.py +85 -0
  19. xenosite_predict-0.2.0/src/xenosite/predict/errors.py +33 -0
  20. xenosite_predict-0.2.0/src/xenosite/predict/features/__init__.py +13 -0
  21. xenosite_predict-0.2.0/src/xenosite/predict/forest.py +433 -0
  22. xenosite_predict-0.2.0/src/xenosite/predict/models/__init__.py +14 -0
  23. xenosite_predict-0.2.0/src/xenosite/predict/molecule.py +91 -0
  24. xenosite_predict-0.2.0/src/xenosite/predict/numbering.py +606 -0
  25. xenosite_predict-0.2.0/src/xenosite/predict/parallel.py +430 -0
  26. xenosite_predict-0.2.0/src/xenosite/predict/py.typed +1 -0
  27. xenosite_predict-0.2.0/src/xenosite/predict/registry.py +126 -0
  28. xenosite_predict-0.2.0/src/xenosite/predict/symmetry.py +24 -0
  29. xenosite_predict-0.2.0/src/xenosite/predict/types.py +119 -0
  30. xenosite_predict-0.2.0/src/xenosite/predict/v0_legacy/__init__.py +1 -0
  31. xenosite_predict-0.2.0/src/xenosite/predict/v0_legacy/features/__init__.py +38 -0
  32. xenosite_predict-0.2.0/src/xenosite/predict/v0_legacy/features/_ob.py +217 -0
  33. xenosite_predict-0.2.0/src/xenosite/predict/v0_legacy/features/atom.py +550 -0
  34. xenosite_predict-0.2.0/src/xenosite/predict/v0_legacy/features/bond.py +659 -0
  35. xenosite_predict-0.2.0/src/xenosite/predict/v0_legacy/features/bond_lonepair.py +774 -0
  36. xenosite_predict-0.2.0/src/xenosite/predict/v0_legacy/features/epoxidation_bond_names.json +383 -0
  37. xenosite_predict-0.2.0/src/xenosite/predict/v0_legacy/features/epoxidation_mol_names.json +39 -0
  38. xenosite_predict-0.2.0/src/xenosite/predict/v0_legacy/features/heuristic.py +79 -0
  39. xenosite_predict-0.2.0/src/xenosite/predict/v0_legacy/features/molgraph.py +208 -0
  40. xenosite_predict-0.2.0/src/xenosite/predict/v0_legacy/features/names.py +58 -0
  41. xenosite_predict-0.2.0/src/xenosite/predict/v0_legacy/features/ndealk_bond_names.json +390 -0
  42. xenosite_predict-0.2.0/src/xenosite/predict/v0_legacy/features/phase1_mol.py +70 -0
  43. xenosite_predict-0.2.0/src/xenosite/predict/v0_legacy/features/phase1_mol_names.json +43 -0
  44. xenosite_predict-0.2.0/src/xenosite/predict/v0_legacy/features/phase1_site_names.json +406 -0
  45. xenosite_predict-0.2.0/src/xenosite/predict/v0_legacy/features/quinone.py +116 -0
  46. xenosite_predict-0.2.0/src/xenosite/predict/v0_legacy/features/quinone_atom_names.json +394 -0
  47. xenosite_predict-0.2.0/src/xenosite/predict/v0_legacy/features/quinone_mol_names.json +25 -0
  48. xenosite_predict-0.2.0/src/xenosite/predict/v0_legacy/features/quinone_pair_names.json +8 -0
  49. xenosite_predict-0.2.0/src/xenosite/predict/v0_legacy/features/reactivity_atom_names.json +213 -0
  50. xenosite_predict-0.2.0/src/xenosite/predict/v0_legacy/features/reactivity_mol.py +69 -0
  51. xenosite_predict-0.2.0/src/xenosite/predict/v0_legacy/features/reactivity_mol_names.json +39 -0
  52. xenosite_predict-0.2.0/src/xenosite/predict/v0_legacy/features/two_stage.py +65 -0
  53. xenosite_predict-0.2.0/src/xenosite/predict/v0_legacy/features/ugt.py +342 -0
  54. xenosite_predict-0.2.0/src/xenosite/predict/v0_legacy/features/ugt_atom_names.json +86 -0
  55. xenosite_predict-0.2.0/src/xenosite/predict/v0_legacy/models/__init__.py +20 -0
  56. xenosite_predict-0.2.0/src/xenosite/predict/v0_legacy/models/_base.py +86 -0
  57. xenosite_predict-0.2.0/src/xenosite/predict/v0_legacy/models/bioactivation.py +74 -0
  58. xenosite_predict-0.2.0/src/xenosite/predict/v0_legacy/models/epoxidation.py +99 -0
  59. xenosite_predict-0.2.0/src/xenosite/predict/v0_legacy/models/ndealk.py +143 -0
  60. xenosite_predict-0.2.0/src/xenosite/predict/v0_legacy/models/phase1.py +193 -0
  61. xenosite_predict-0.2.0/src/xenosite/predict/v0_legacy/models/quinone.py +169 -0
  62. xenosite_predict-0.2.0/src/xenosite/predict/v0_legacy/models/reactivity.py +115 -0
  63. xenosite_predict-0.2.0/src/xenosite/predict/v0_legacy/models/ugt.py +50 -0
  64. xenosite_predict-0.2.0/src/xenosite/predict/v0_legacy/symmetry.py +218 -0
  65. xenosite_predict-0.2.0/src/xenosite/predict/v1/__init__.py +1 -0
  66. xenosite_predict-0.2.0/src/xenosite/predict/weights.py +339 -0
  67. xenosite_predict-0.2.0/tests/fixtures/stub_onnx/ugt/atom.onnx +0 -0
@@ -0,0 +1 @@
1
+ tests/fixtures/ob_dumps.json.gz filter=lfs diff=lfs merge=lfs -text
@@ -0,0 +1,57 @@
1
+ # Publish to PyPI via Trusted Publishing (OIDC). No API tokens in GitHub secrets.
2
+ #
3
+ # One-time setup (package not on PyPI yet → pending publisher):
4
+ # 1. https://pypi.org/manage/account/publishing/
5
+ # 2. Add pending publisher:
6
+ # PyPI project name: xenosite-predict
7
+ # Owner: swamidasslab
8
+ # Repository: xenosite-predict
9
+ # Workflow name: publish.yml
10
+ # Environment: pypi
11
+ # 3. GitHub → Settings → Environments → create `pypi`
12
+ # (optional but recommended: required reviewers)
13
+ # 4. Merge this workflow, then push a tag vX.Y.Z (or Run workflow → publish)
14
+ #
15
+ # Docs: https://docs.pypi.org/trusted-publishers/
16
+
17
+ name: Publish
18
+
19
+ on:
20
+ push:
21
+ tags:
22
+ - "v*"
23
+ workflow_dispatch:
24
+
25
+ permissions:
26
+ contents: read
27
+
28
+ jobs:
29
+ build:
30
+ runs-on: ubuntu-latest
31
+ steps:
32
+ - uses: actions/checkout@v4
33
+ - uses: astral-sh/setup-uv@v5
34
+ - name: Build sdist
35
+ run: uv build --sdist
36
+ - uses: actions/upload-artifact@v4
37
+ with:
38
+ name: dist
39
+ path: dist/
40
+
41
+ publish:
42
+ needs: build
43
+ runs-on: ubuntu-latest
44
+ environment:
45
+ name: pypi
46
+ url: https://pypi.org/p/xenosite-predict
47
+ permissions:
48
+ id-token: write # required for trusted publishing
49
+ steps:
50
+ - uses: actions/download-artifact@v4
51
+ with:
52
+ name: dist
53
+ path: dist/
54
+ - name: Publish to PyPI
55
+ uses: pypa/gh-action-pypi-publish@release/v1
56
+ with:
57
+ packages-dir: dist/
@@ -0,0 +1,55 @@
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+ *.so
6
+ .Python
7
+ .venv/
8
+ venv/
9
+ *.egg-info/
10
+ .eggs/
11
+ dist/
12
+ build/
13
+ .pytest_cache/
14
+ .hypothesis/
15
+ .mypy_cache/
16
+ .ruff_cache/
17
+ .coverage
18
+ htmlcov/
19
+
20
+ # OS
21
+ .DS_Store
22
+
23
+ # Local model artifacts — never commit weights or extracted legacy trees
24
+ weights/legacy/
25
+ weights/onnx/
26
+ weights/*.tgz
27
+ weights/**/*.onnx
28
+ weights/**/*.safetensors
29
+ weights/**/*.model
30
+ weights/**/*.pyp
31
+ weights/**/*.pkl
32
+ weights/**/*.pickle
33
+ *.onnx
34
+ *.safetensors
35
+ *.model
36
+ *.pyp
37
+ # Tiny stub graphs for parallel/async unit tests (not real weights).
38
+ !tests/fixtures/stub_onnx/**/*.onnx
39
+
40
+ # uv
41
+ uv.lock
42
+
43
+ # Regenerable uncompressed OpenBabel dumps from `make dump-ob`.
44
+ # The gzipped suite (ob_dumps.json.gz) is committed via Git LFS.
45
+ tests/fixtures/ob_dump_aspirin.json
46
+ tests/fixtures/ob_dumps.json
47
+ # Regenerable ONNX score cache from `make capture-suite-onnx`
48
+ tests/fixtures/suite_onnx_cache.json
49
+
50
+ # Local scratch / ad-hoc investigation scripts (see tools/archive/)
51
+ tools/archive/
52
+
53
+ # Editor / session artifacts
54
+ .cursor/
55
+ :memory:.ses
@@ -0,0 +1 @@
1
+ 3.11
@@ -0,0 +1,126 @@
1
+ # uv has no first-class task runner; this Makefile is the equivalent.
2
+ # Tools live under tools/ and are NOT installed with the wheel.
3
+
4
+ PYTHON ?= uv run python
5
+ PYTEST ?= uv run pytest
6
+ CONVERT ?= uv run --group convert python
7
+ IMAGE ?= dockerreg01.accounts.ad.wustl.edu/swamidass/xenosite-legacy:api
8
+ LEGACY_COMPOSE ?= tools/legacy-test-api/compose.yml
9
+ LEGACY_REPLICAS ?= 24
10
+ TARBALL ?= ../xenosite-legacy/data/xenosite_legacy_data_trimmed.tgz
11
+ ONNX_DIR ?= weights/onnx/v0
12
+ ONNX_TARBALL ?= weights/xenosite_onnx_v0.tgz
13
+
14
+ .PHONY: extract-weights convert-onnx convert-onnx-$(MODEL) pack-onnx extract-onnx download-onnx test test-golden test-live \
15
+ legacy-test-api legacy-test-api-down py2-dump-image dump-ob dump-ob dump-ob-features \
16
+ capture-suite-onnx gather-golden drift-report drift-descriptors help \
17
+ regather-ob-dumps regather-golden-onnx
18
+
19
+ help:
20
+ @echo "extract-weights copy pickles/TSV/source from $(IMAGE) into weights/legacy/"
21
+ @echo "convert-onnx pickle → safetensors → ONNX (all models)"
22
+ @echo "convert-onnx MODEL=epoxidation"
23
+ @echo "pack-onnx tarball of *.onnx + *.meta.json (no _dump) → $(ONNX_TARBALL)"
24
+ @echo "extract-onnx unpack $(ONNX_TARBALL) into $(ONNX_DIR)/"
25
+ @echo "download-onnx fetch $$XENOSITE_ONNX_URL into $(ONNX_DIR)/"
26
+ @echo "test unit tests, Docker-free (-n auto via pyproject.toml)"
27
+ @echo "test-golden golden_descriptor_suite ONNX parity (-n auto)"
28
+ @echo "test-live pytest -m live (skips if Docker/image/weights missing)"
29
+ @echo "py2-dump-image build python:2.7-slim dump image (numpy + OpenBabel 2.4 + RDKit)"
30
+ @echo "dump-ob fill descriptor suite incrementally (skip dumps already present)"
31
+ @echo "capture-suite-onnx cache ONNX scores (CAPTURE_WORKERS=24 default; CAPTURE_MODEL/SMILES to filter)"
32
+ @echo "gather-golden regather failing suite rows from legacy-test-api (GATHER_WORKERS=24)"
33
+ @echo "regather-ob-dumps refresh quinone rows in ob_dumps from py3 legacy OMP port"
34
+ @echo "regather-golden-onnx refresh golden scores from ONNX + GOLDEN_PARAMETER"
35
+ @echo "drift-report classify ONNX vs golden from cache (DRIFT_WORKERS=24 default)"
36
+ @echo "drift-descriptors cross-tab descriptor vs score drift for one model"
37
+ @echo "legacy-test-api nginx LB + cache, scale API with LEGACY_REPLICAS=24"
38
+ @echo "legacy-test-api-down"
39
+
40
+ extract-weights:
41
+ $(PYTHON) tools/extract_weights.py --image $(IMAGE) --tarball $(TARBALL) --out weights/legacy
42
+
43
+ convert-onnx:
44
+ $(CONVERT) tools/convert_onnx.py --src weights/legacy --out $(ONNX_DIR) $(if $(MODEL),--model $(MODEL),)
45
+
46
+ pack-onnx:
47
+ $(PYTHON) tools/pack_onnx.py --src $(ONNX_DIR) --out $(ONNX_TARBALL)
48
+
49
+ extract-onnx:
50
+ $(PYTHON) tools/pack_onnx.py --extract --src $(ONNX_DIR) --out $(ONNX_TARBALL)
51
+
52
+ download-onnx:
53
+ $(PYTHON) -m xenosite.predict download --dest $(ONNX_DIR)
54
+
55
+ test:
56
+ $(PYTEST) -m "not live"
57
+
58
+ test-golden:
59
+ $(PYTEST) tests/test_golden_suite.py
60
+
61
+ test-live:
62
+ $(PYTEST) -m live
63
+
64
+ legacy-test-api:
65
+ docker compose -f $(LEGACY_COMPOSE) up --build -d --scale legacy-test-api=$(LEGACY_REPLICAS)
66
+
67
+ legacy-test-api-down:
68
+ docker compose -f $(LEGACY_COMPOSE) down
69
+
70
+ py2-dump-image:
71
+ docker build --platform linux/amd64 -t xenosite-predict-py2:dump tools/py2-dump
72
+
73
+ SMILES ?= O=C(C)Oc1ccccc1C(=O)O
74
+ MODEL ?= epoxidation
75
+
76
+ # Optional filters for capture-suite-onnx (empty = full golden suite)
77
+ CAPTURE_MODEL ?=
78
+ CAPTURE_SMILES ?=
79
+ CAPTURE_WORKERS ?= 24
80
+ DRIFT_WORKERS ?= 24
81
+ GATHER_WORKERS ?= 24
82
+ GATHER_MODEL ?=
83
+
84
+ dump-ob-features:
85
+ $(PYTHON) tools/dump_ob.py --smiles '$(SMILES)' --model $(MODEL)
86
+
87
+ dump-ob:
88
+ $(PYTHON) tools/dump_ob.py --suite
89
+
90
+ capture-suite-onnx:
91
+ $(PYTHON) tools/capture_suite_onnx.py \
92
+ --workers $(CAPTURE_WORKERS) \
93
+ $(if $(CAPTURE_MODEL),--model $(CAPTURE_MODEL),) \
94
+ $(if $(CAPTURE_SMILES),--smiles '$(CAPTURE_SMILES)',) \
95
+ $(if $(FORCE),--force,)
96
+
97
+ gather-golden:
98
+ $(PYTHON) tools/gather_golden_suite.py \
99
+ --workers $(GATHER_WORKERS) \
100
+ --failing-only \
101
+ --force \
102
+ $(if $(GATHER_MODEL),--models $(GATHER_MODEL),)
103
+
104
+ drift-report:
105
+ -$(PYTHON) tools/report_suite_drift.py \
106
+ --workers $(DRIFT_WORKERS) \
107
+ $(if $(CAPTURE_MODEL),--model $(CAPTURE_MODEL),) \
108
+ $(if $(REFRESH),--refresh,)
109
+
110
+ DRIFT_MODEL ?= quinone
111
+
112
+ drift-descriptors:
113
+ $(PYTHON) tools/analyze_descriptor_score_drift.py \
114
+ --model $(DRIFT_MODEL) \
115
+ --workers $(DRIFT_WORKERS)
116
+
117
+ regather-ob-dumps:
118
+ $(PYTHON) tools/regather_internal_ob_dumps.py --models quinone
119
+
120
+ regather-golden-onnx:
121
+ $(PYTHON) tools/regather_golden_from_onnx.py \
122
+ --workers $(GATHER_WORKERS) \
123
+ --force \
124
+ --include-smoke \
125
+ --models epoxidation,quinone,reactivity,ugt,ndealk,isozyme \
126
+ $(if $(GATHER_MODEL),--models $(GATHER_MODEL),)
@@ -0,0 +1,197 @@
1
+ Metadata-Version: 2.5
2
+ Name: xenosite-predict
3
+ Version: 0.2.0
4
+ Summary: RDKit public API + internal OpenBabel 2.4 + ONNX predictors for XenoSite (xenosite.predict)
5
+ Author-email: "S. Joshua Swamidass" <swamidass@gmail.com>
6
+ Requires-Python: >=3.11
7
+ Requires-Dist: httpx>=0.24
8
+ Requires-Dist: numpy>=1.26
9
+ Requires-Dist: onnxruntime>=1.16
10
+ Requires-Dist: openbabel>=3.2.1
11
+ Requires-Dist: pydantic>=2.5
12
+ Requires-Dist: rdkit>=2023.9.2
13
+ Requires-Dist: xenosite-forest>=0.1.0
14
+ Description-Content-Type: text/markdown
15
+
16
+ # xenosite.predict
17
+
18
+ Python 3.11+ RDKit + ONNX predictors for XenoSite. Dist name **xenosite-predict**; import **`xenosite.predict`** (PEP 420 namespace). Checkout as a sibling of `xenosite-api` and `xenosite-legacy`; origin is [github.com/swamidasslab/xenosite-predict](https://github.com/swamidasslab/xenosite-predict).
19
+
20
+ Publish **sdist only** (no wheel): ONNX weights stay local (`weights/`, gitignored). OpenBabel comes from PyPI (`uv add openbabel`, currently 3.2.x wheels). `make test` is Docker-free. Feature tests compare the installed OpenBabel to the committed `tests/fixtures/ob_dumps.json.gz` (Git LFS). `make test-live` skips if Docker, the legacy image, or ONNX files are missing. Do not commit model weights, pickles, or extracted `libridass/` trees.
21
+
22
+ This package is **not** wired into `xenosite-api` yet.
23
+
24
+ ## User API
25
+
26
+ ```python
27
+ from xenosite.predict import predict, predict_many, apredict, apredict_many, list_models
28
+
29
+ mol = predict("O=C(C)Oc1ccccc1C(=O)O", model="epoxidation")
30
+ mol = predict("O=C(C)Oc1ccccc1C(=O)O", models=["epoxidation", ("ugt", "0")])
31
+ mol = predict(mol, models=["quinone"]) # append
32
+ list_models() # what this process can actually run (backend-aware)
33
+
34
+ # Many molecules (process pool for ONNX; sync API)
35
+ mols = predict_many(smiles_list, model="ugt", workers=4)
36
+
37
+ # Async (event-loop friendly; same workers under the hood)
38
+ mol = await apredict(smi, model="ugt")
39
+ mols = await apredict_many(smiles_list, model="ugt", workers=4)
40
+ mols = await asyncio.gather(*[apredict(s, model="ugt") for s in smiles_list])
41
+ ```
42
+
43
+ - **One molecule at a time** for ``predict`` / ``apredict`` (no multi-mol batch inside a single call).
44
+ - **Many molecules:** ``predict_many`` / ``apredict_many`` run each input independently in parallel.
45
+ - **Parse once** when several models run on one molecule. Canonical SMILES is **non-isomeric** (`isomericSmiles=False`).
46
+ - **`models=`** is a name (default version `"0"`) or `(name, version)` pairs. Do not pass one version string for a whole list.
47
+ - **Indices** are 0-based RDKit atom/bond indices. Scores are floats (`atol=1e-4` in tests).
48
+ - **Name lookup is omitted.** Pass SMILES, not drug names.
49
+ - Import does **not** open ONNX, HTTP, or OpenBabel. Load on first use of that `(model, version)`. Callers never import `openbabel` / `pybel`.
50
+ - First `predict()` downloads ONNX weights when `XENOSITE_ONNX_URL` is set and none are cached (an **INFO** line reports when they are found or downloaded). No separate `download_weights()` call is required.
51
+ - **Workers:** ONNX batch/async paths use a process pool (descriptor generation is CPU-bound; threads do not help). Set ``workers=`` or ``XENOSITE_WORKERS``. ``XENOSITE_ORT_INTRA_OP`` caps ORT threads per process under concurrency.
52
+ - **Legacy vs principled:** production defaults differ from golden-test-api parity in four internal `_parameter` flags (ndealk site keys, quinone OMP paths, bond symmetry, bond NRings). **Score impact summary:** [`docs/legacy-vs-principled.md`](docs/legacy-vs-principled.md#expected-score-impact-production-vs-legacy). Walkthrough: `tests/v0_legacy/test_legacy_vs_principled_guide.py`.
53
+
54
+ ### `predict_many` / `apredict` / `apredict_many`
55
+
56
+ | Helper | Meaning |
57
+ |---|---|
58
+ | `predict_many(inputs, …, workers=…)` | Sync batch: one molecule per input, process pool for ONNX |
59
+ | `apredict(inp, …)` | Async single molecule (offloads to the shared pool) |
60
+ | `apredict_many(inputs, …)` | Async batch (same workers as `predict_many`) |
61
+
62
+ `workers` defaults to CPU count (`XENOSITE_WORKERS` overrides). New models reuse the existing `predict` / runner path — no per-model async code.
63
+
64
+ ### `predict(inp, model=..., models=..., backend=..., backends=..., env=...)`
65
+
66
+ | Arg | Meaning |
67
+ |---|---|
68
+ | `inp` | SMILES or an existing `Molecule` (results append) |
69
+ | `model` | Single name; ignored if `models` is set |
70
+ | `models` | `str` or `(name, version)` iterable |
71
+ | `backend` | Pin the whole call: `"onnx"`, `"http"`, `"legacy"`, a URL, or a backend object |
72
+ | `backends` | Per-`(name, version)` override (ONNX epoxidation + HTTP bioactivation) |
73
+ | `env` | Picker mapping; `None` uses `os.environ`. Tests clear `XENOSITE_*` |
74
+
75
+ ### Return type (`Molecule`)
76
+
77
+ Ported from `xenosite-api` `types.py`: `smiles`, `atoms`, `bonds`, `results`. Result variants: `MolBondResult`, `MolAtomResult`, `MolAtomPairResult`, `AtomResult`, `BondResult`, `AtomBondResult`. Each result has `model` and `version`.
78
+
79
+ ### `list_models()`
80
+
81
+ Returns dicts `{name, version, available, backend, reason, heads, two_stage, pipeline}` for **this process**, not a fictional union of every backend.
82
+
83
+ ### ONNX weights
84
+
85
+ ONNX graphs are not in the sdist. Set `XENOSITE_ONNX_URL` to an https tarball
86
+ or a local `.tgz` path (the URL is not stored in this repo). The first
87
+ `predict()` (or `list_models()`) downloads into `$XDG_CACHE_HOME/xenosite/onnx/v0`
88
+ (or `~/.cache/xenosite/onnx/v0`, or `XENOSITE_MODELS_WEIGHTS` if set) and prints
89
+ an INFO line when weights are found or downloaded. Download logs and errors
90
+ never echo the URL (so a private weight location does not leak via stderr or
91
+ tracebacks). Tests that pass `env={}` never fetch. `python -m xenosite.predict download` and `make download-onnx`
92
+ are optional pre-fetch helpers.
93
+
94
+ ### Errors
95
+
96
+ `InvalidMolecule`, `UnknownModel`, `BackendNotConfigured`, `WeightsNotFound`, `WeightsDownloadError`, `ModelNotAvailable`, `OpenBabelNotAvailable`.
97
+
98
+ ## Backends
99
+
100
+ Picker (explicit env wins; first match):
101
+
102
+ 1. `XENOSITE_BACKEND` is an `http://` / `https://` URL → **HTTP** against that deployed **xenosite-api**. Optional `XENOSITE_API_KEY` as Bearer.
103
+ 2. Else `XENOSITE_MODELS_WEIGHTS` → local **ONNX** directory.
104
+ 3. Else auto-detect `./weights/onnx/v0` (or a flat `./weights/onnx` tree) → local ONNX.
105
+ 4. Else user cache (`$XDG_CACHE_HOME/xenosite/onnx/v0`) if `*.onnx` exist.
106
+ 5. Else, when `XENOSITE_ONNX_URL` is set in the process env, download that archive into the cache (INFO on found/download).
107
+ 6. Else raise `BackendNotConfigured`.
108
+
109
+ Live parity compares **ONNX vs the legacy test-API**, not vs production HTTP. Tests must pass `backend=` and must not inherit a developer shell (`XENOSITE_*` are cleared in `conftest.py`).
110
+
111
+ | Backend | Role |
112
+ |---|---|
113
+ | ONNX | Converted numpy-NN heads under `weights/onnx/v0/<model>/<head>.onnx` |
114
+ | HTTP | `GET {origin}/v0/<model>?smiles=` (xenosite-api) |
115
+ | Legacy | Derived Docker test API (`POST /predict/<model>`, `POST /nn/<model>/<head>`) |
116
+
117
+ Per-model override: `predict(..., backends={("bioactivation", "0"): "http"})`.
118
+
119
+ ## Built-in models (version `"0"`)
120
+
121
+ | Name | User results | Notes |
122
+ |---|---|---|
123
+ | `epoxidation` | `MolBondResult` | Two-stage: bond ONNX then mol ONNX (Top-N site scores). Averages two atom orderings. |
124
+ | `quinone` | `MolAtomPairResult` | Atom → pair → mol. Includes null-pair molecule `O=C(Br)C(F)(F)F`. |
125
+ | `reactivity` | four `MolAtomResult` (`reactivity.gsh` / `.protein` / `.cyanide` / `.dna`) | Two-stage atom then mol. |
126
+ | `ugt` | `AtomResult` | Internal OpenBabel topological + mol descriptors. No MOPAC/SmartCYP on the inference path. |
127
+ | `ndealk` | `BondResult` (HLM slice) | Same ONNX as isozyme. Check `CCCC1CCCNC1C=O` for off-by-1. |
128
+ | `isozyme` | ten `BondResult` (`isozyme.3a4`, … `isozyme.hlm`) | Production Flask uses **ndealk1** for `metabolism1`, not the MOPAC metabolism predictor. |
129
+ | `phase1` | five `AtomBondResult` | TF `molecularNN` → ONNX (`site` + `mol`). Bond_and_LonePair descriptors + topology-group pooling. |
130
+ | `bioactivation` | `MolAtomResult` + metabolites | **Pipeline last** (enumeration + other models), not a single ONNX. |
131
+
132
+ ## Makefile (tools are not in the sdist)
133
+
134
+ ```
135
+ make extract-weights # Docker image or fallback tarball → weights/legacy/
136
+ make convert-onnx # pickle → ONNX; MODEL=epoxidation for one model
137
+ make pack-onnx # weights/xenosite_onnx_v0.tgz (runtime graphs, no _dump)
138
+ make extract-onnx # unpack that tarball into weights/onnx/v0/
139
+ make download-onnx # fetch $XENOSITE_ONNX_URL into weights/onnx/v0/
140
+ make test # pytest -m "not live" (no Docker)
141
+ make test-live # pytest -m live; fixture skips if Docker/image missing
142
+ make py2-dump-image # python:2.7-slim + numpy + Debian OpenBabel 2.4
143
+ make dump-ob # OpenBabel feature dump via that image (no WashU)
144
+ make legacy-test-api # build/run derived test image
145
+ make legacy-test-api-down
146
+ ```
147
+
148
+ Convert deps: `uv run --group convert`. Installed runtime: rdkit, openbabel (PyPI 3.2.x), numpy, onnxruntime, httpx, pydantic. OpenBabel is **internal** (not part of the public API). No TensorFlow, pandas, or pickle at inference.
149
+
150
+ The dump image remains the OpenBabel **2.4.1** feature oracle. Host inference uses the PyPI **3.2.x** wheel; `tests/test_ob_features.py` reports 3.x vs 2.4 drift at atol `1e-4` / rtol `0`. Do not vendor OpenBabel sources (GPL).
151
+
152
+ Populate pickles from `dockerreg01.accounts.ad.wustl.edu/swamidass/xenosite-legacy:api` (needs registry login) or the sibling tarball `xenosite-legacy/data/xenosite_legacy_data_trimmed.tgz`. `make convert-onnx` unpickles in a public **python:2.7-slim** dump image (`tools/py2-dump/`), not the WashU API image.
153
+
154
+ The same dump image is the OpenBabel **feature oracle**: Debian Buster `python-openbabel` 2.4.1 and `python-rdkit` from archive.debian.org, running as `/usr/bin/python` (the image's `/usr/local` CPython cannot load the multiarch SWIG module). `make dump-ob` feeds an RDKit molblock so 1-based OB indices align with 0-based RDKit, and dumps BondTD/AtomTD/UGT/Heuristic/Bond_and_LonePair rows from sibling `xenosite-legacy/src`. It is **idempotent**: molecule/model pairs already in the suite are skipped, and the JSON is checkpointed after each chunk. The gzipped suite `tests/fixtures/ob_dumps.json.gz` is committed via Git LFS so dump tests run without Docker; uncompressed JSON stays gitignored. Clone with Git LFS (`git lfs pull`).
155
+
156
+ Public parse/canonicalize stays RDKit. Feature graphs call OpenBabel internally (PyPI 3.2.x). `tests/test_ob_features.py` compares host OpenBabel 3.2 rows to 2.4 dumps at atol `1e-4` / rtol `0`. Missing dumps fail. Hypothesis draws random finite matrices for ONNX heads (`test_onnx_random_matrix_finite`) and live `/nn` vs ONNX (`test_random_vector_nn`). The convert dump `tests/fixtures/random_vectors.json` is the Python-2 regression (ONNX == pickled numpy NN).
157
+
158
+ ## Layout
159
+
160
+ ```
161
+ src/xenosite/predict/ # user API (installed)
162
+ tools/ # extract, convert, legacy-test-api (not in the wheel)
163
+ weights/ # local only — README + .gitignore committed
164
+ tests/ # unit + @pytest.mark.live
165
+ docs/vendored-diffs.md # NN/feature hashes, MOPAC/SmartCYP gate
166
+ docs/legacy-vs-principled.md # production defaults vs golden legacy modes
167
+ ```
168
+
169
+ ## Development
170
+
171
+ ```
172
+ uv sync --group dev
173
+ make test
174
+ ```
175
+
176
+ ### Publishing to PyPI (trusted publishing)
177
+
178
+ No long-lived PyPI tokens. Releases use GitHub OIDC via `.github/workflows/publish.yml`.
179
+
180
+ 1. On PyPI, add a **pending** trusted publisher (project not published yet) at
181
+ [pypi.org/manage/account/publishing](https://pypi.org/manage/account/publishing/):
182
+ - Project: `xenosite-predict`
183
+ - Owner: `swamidasslab`
184
+ - Repo: `xenosite-predict`
185
+ - Workflow: `publish.yml`
186
+ - Environment: `pypi`
187
+ 2. In GitHub → Settings → Environments, create `pypi` (add required reviewers if you want a human gate).
188
+ 3. Merge the workflow, then either push a tag `v0.2.0` or run **Publish** manually.
189
+ 4. The first successful publish creates the PyPI project; later releases reuse the same publisher.
190
+
191
+ Do **not** commit `XENOSITE_ONNX_URL`, API keys, or weight hostnames. Keep those in local env / deployment secrets only.
192
+
193
+ Vendored-tree comparison (sibling checkout, not committed):
194
+
195
+ ```
196
+ uv run python tools/compare_vendored.py --root ../xenosite-legacy/src/libridass
197
+ ```
@@ -0,0 +1,182 @@
1
+ # xenosite.predict
2
+
3
+ Python 3.11+ RDKit + ONNX predictors for XenoSite. Dist name **xenosite-predict**; import **`xenosite.predict`** (PEP 420 namespace). Checkout as a sibling of `xenosite-api` and `xenosite-legacy`; origin is [github.com/swamidasslab/xenosite-predict](https://github.com/swamidasslab/xenosite-predict).
4
+
5
+ Publish **sdist only** (no wheel): ONNX weights stay local (`weights/`, gitignored). OpenBabel comes from PyPI (`uv add openbabel`, currently 3.2.x wheels). `make test` is Docker-free. Feature tests compare the installed OpenBabel to the committed `tests/fixtures/ob_dumps.json.gz` (Git LFS). `make test-live` skips if Docker, the legacy image, or ONNX files are missing. Do not commit model weights, pickles, or extracted `libridass/` trees.
6
+
7
+ This package is **not** wired into `xenosite-api` yet.
8
+
9
+ ## User API
10
+
11
+ ```python
12
+ from xenosite.predict import predict, predict_many, apredict, apredict_many, list_models
13
+
14
+ mol = predict("O=C(C)Oc1ccccc1C(=O)O", model="epoxidation")
15
+ mol = predict("O=C(C)Oc1ccccc1C(=O)O", models=["epoxidation", ("ugt", "0")])
16
+ mol = predict(mol, models=["quinone"]) # append
17
+ list_models() # what this process can actually run (backend-aware)
18
+
19
+ # Many molecules (process pool for ONNX; sync API)
20
+ mols = predict_many(smiles_list, model="ugt", workers=4)
21
+
22
+ # Async (event-loop friendly; same workers under the hood)
23
+ mol = await apredict(smi, model="ugt")
24
+ mols = await apredict_many(smiles_list, model="ugt", workers=4)
25
+ mols = await asyncio.gather(*[apredict(s, model="ugt") for s in smiles_list])
26
+ ```
27
+
28
+ - **One molecule at a time** for ``predict`` / ``apredict`` (no multi-mol batch inside a single call).
29
+ - **Many molecules:** ``predict_many`` / ``apredict_many`` run each input independently in parallel.
30
+ - **Parse once** when several models run on one molecule. Canonical SMILES is **non-isomeric** (`isomericSmiles=False`).
31
+ - **`models=`** is a name (default version `"0"`) or `(name, version)` pairs. Do not pass one version string for a whole list.
32
+ - **Indices** are 0-based RDKit atom/bond indices. Scores are floats (`atol=1e-4` in tests).
33
+ - **Name lookup is omitted.** Pass SMILES, not drug names.
34
+ - Import does **not** open ONNX, HTTP, or OpenBabel. Load on first use of that `(model, version)`. Callers never import `openbabel` / `pybel`.
35
+ - First `predict()` downloads ONNX weights when `XENOSITE_ONNX_URL` is set and none are cached (an **INFO** line reports when they are found or downloaded). No separate `download_weights()` call is required.
36
+ - **Workers:** ONNX batch/async paths use a process pool (descriptor generation is CPU-bound; threads do not help). Set ``workers=`` or ``XENOSITE_WORKERS``. ``XENOSITE_ORT_INTRA_OP`` caps ORT threads per process under concurrency.
37
+ - **Legacy vs principled:** production defaults differ from golden-test-api parity in four internal `_parameter` flags (ndealk site keys, quinone OMP paths, bond symmetry, bond NRings). **Score impact summary:** [`docs/legacy-vs-principled.md`](docs/legacy-vs-principled.md#expected-score-impact-production-vs-legacy). Walkthrough: `tests/v0_legacy/test_legacy_vs_principled_guide.py`.
38
+
39
+ ### `predict_many` / `apredict` / `apredict_many`
40
+
41
+ | Helper | Meaning |
42
+ |---|---|
43
+ | `predict_many(inputs, …, workers=…)` | Sync batch: one molecule per input, process pool for ONNX |
44
+ | `apredict(inp, …)` | Async single molecule (offloads to the shared pool) |
45
+ | `apredict_many(inputs, …)` | Async batch (same workers as `predict_many`) |
46
+
47
+ `workers` defaults to CPU count (`XENOSITE_WORKERS` overrides). New models reuse the existing `predict` / runner path — no per-model async code.
48
+
49
+ ### `predict(inp, model=..., models=..., backend=..., backends=..., env=...)`
50
+
51
+ | Arg | Meaning |
52
+ |---|---|
53
+ | `inp` | SMILES or an existing `Molecule` (results append) |
54
+ | `model` | Single name; ignored if `models` is set |
55
+ | `models` | `str` or `(name, version)` iterable |
56
+ | `backend` | Pin the whole call: `"onnx"`, `"http"`, `"legacy"`, a URL, or a backend object |
57
+ | `backends` | Per-`(name, version)` override (ONNX epoxidation + HTTP bioactivation) |
58
+ | `env` | Picker mapping; `None` uses `os.environ`. Tests clear `XENOSITE_*` |
59
+
60
+ ### Return type (`Molecule`)
61
+
62
+ Ported from `xenosite-api` `types.py`: `smiles`, `atoms`, `bonds`, `results`. Result variants: `MolBondResult`, `MolAtomResult`, `MolAtomPairResult`, `AtomResult`, `BondResult`, `AtomBondResult`. Each result has `model` and `version`.
63
+
64
+ ### `list_models()`
65
+
66
+ Returns dicts `{name, version, available, backend, reason, heads, two_stage, pipeline}` for **this process**, not a fictional union of every backend.
67
+
68
+ ### ONNX weights
69
+
70
+ ONNX graphs are not in the sdist. Set `XENOSITE_ONNX_URL` to an https tarball
71
+ or a local `.tgz` path (the URL is not stored in this repo). The first
72
+ `predict()` (or `list_models()`) downloads into `$XDG_CACHE_HOME/xenosite/onnx/v0`
73
+ (or `~/.cache/xenosite/onnx/v0`, or `XENOSITE_MODELS_WEIGHTS` if set) and prints
74
+ an INFO line when weights are found or downloaded. Download logs and errors
75
+ never echo the URL (so a private weight location does not leak via stderr or
76
+ tracebacks). Tests that pass `env={}` never fetch. `python -m xenosite.predict download` and `make download-onnx`
77
+ are optional pre-fetch helpers.
78
+
79
+ ### Errors
80
+
81
+ `InvalidMolecule`, `UnknownModel`, `BackendNotConfigured`, `WeightsNotFound`, `WeightsDownloadError`, `ModelNotAvailable`, `OpenBabelNotAvailable`.
82
+
83
+ ## Backends
84
+
85
+ Picker (explicit env wins; first match):
86
+
87
+ 1. `XENOSITE_BACKEND` is an `http://` / `https://` URL → **HTTP** against that deployed **xenosite-api**. Optional `XENOSITE_API_KEY` as Bearer.
88
+ 2. Else `XENOSITE_MODELS_WEIGHTS` → local **ONNX** directory.
89
+ 3. Else auto-detect `./weights/onnx/v0` (or a flat `./weights/onnx` tree) → local ONNX.
90
+ 4. Else user cache (`$XDG_CACHE_HOME/xenosite/onnx/v0`) if `*.onnx` exist.
91
+ 5. Else, when `XENOSITE_ONNX_URL` is set in the process env, download that archive into the cache (INFO on found/download).
92
+ 6. Else raise `BackendNotConfigured`.
93
+
94
+ Live parity compares **ONNX vs the legacy test-API**, not vs production HTTP. Tests must pass `backend=` and must not inherit a developer shell (`XENOSITE_*` are cleared in `conftest.py`).
95
+
96
+ | Backend | Role |
97
+ |---|---|
98
+ | ONNX | Converted numpy-NN heads under `weights/onnx/v0/<model>/<head>.onnx` |
99
+ | HTTP | `GET {origin}/v0/<model>?smiles=` (xenosite-api) |
100
+ | Legacy | Derived Docker test API (`POST /predict/<model>`, `POST /nn/<model>/<head>`) |
101
+
102
+ Per-model override: `predict(..., backends={("bioactivation", "0"): "http"})`.
103
+
104
+ ## Built-in models (version `"0"`)
105
+
106
+ | Name | User results | Notes |
107
+ |---|---|---|
108
+ | `epoxidation` | `MolBondResult` | Two-stage: bond ONNX then mol ONNX (Top-N site scores). Averages two atom orderings. |
109
+ | `quinone` | `MolAtomPairResult` | Atom → pair → mol. Includes null-pair molecule `O=C(Br)C(F)(F)F`. |
110
+ | `reactivity` | four `MolAtomResult` (`reactivity.gsh` / `.protein` / `.cyanide` / `.dna`) | Two-stage atom then mol. |
111
+ | `ugt` | `AtomResult` | Internal OpenBabel topological + mol descriptors. No MOPAC/SmartCYP on the inference path. |
112
+ | `ndealk` | `BondResult` (HLM slice) | Same ONNX as isozyme. Check `CCCC1CCCNC1C=O` for off-by-1. |
113
+ | `isozyme` | ten `BondResult` (`isozyme.3a4`, … `isozyme.hlm`) | Production Flask uses **ndealk1** for `metabolism1`, not the MOPAC metabolism predictor. |
114
+ | `phase1` | five `AtomBondResult` | TF `molecularNN` → ONNX (`site` + `mol`). Bond_and_LonePair descriptors + topology-group pooling. |
115
+ | `bioactivation` | `MolAtomResult` + metabolites | **Pipeline last** (enumeration + other models), not a single ONNX. |
116
+
117
+ ## Makefile (tools are not in the sdist)
118
+
119
+ ```
120
+ make extract-weights # Docker image or fallback tarball → weights/legacy/
121
+ make convert-onnx # pickle → ONNX; MODEL=epoxidation for one model
122
+ make pack-onnx # weights/xenosite_onnx_v0.tgz (runtime graphs, no _dump)
123
+ make extract-onnx # unpack that tarball into weights/onnx/v0/
124
+ make download-onnx # fetch $XENOSITE_ONNX_URL into weights/onnx/v0/
125
+ make test # pytest -m "not live" (no Docker)
126
+ make test-live # pytest -m live; fixture skips if Docker/image missing
127
+ make py2-dump-image # python:2.7-slim + numpy + Debian OpenBabel 2.4
128
+ make dump-ob # OpenBabel feature dump via that image (no WashU)
129
+ make legacy-test-api # build/run derived test image
130
+ make legacy-test-api-down
131
+ ```
132
+
133
+ Convert deps: `uv run --group convert`. Installed runtime: rdkit, openbabel (PyPI 3.2.x), numpy, onnxruntime, httpx, pydantic. OpenBabel is **internal** (not part of the public API). No TensorFlow, pandas, or pickle at inference.
134
+
135
+ The dump image remains the OpenBabel **2.4.1** feature oracle. Host inference uses the PyPI **3.2.x** wheel; `tests/test_ob_features.py` reports 3.x vs 2.4 drift at atol `1e-4` / rtol `0`. Do not vendor OpenBabel sources (GPL).
136
+
137
+ Populate pickles from `dockerreg01.accounts.ad.wustl.edu/swamidass/xenosite-legacy:api` (needs registry login) or the sibling tarball `xenosite-legacy/data/xenosite_legacy_data_trimmed.tgz`. `make convert-onnx` unpickles in a public **python:2.7-slim** dump image (`tools/py2-dump/`), not the WashU API image.
138
+
139
+ The same dump image is the OpenBabel **feature oracle**: Debian Buster `python-openbabel` 2.4.1 and `python-rdkit` from archive.debian.org, running as `/usr/bin/python` (the image's `/usr/local` CPython cannot load the multiarch SWIG module). `make dump-ob` feeds an RDKit molblock so 1-based OB indices align with 0-based RDKit, and dumps BondTD/AtomTD/UGT/Heuristic/Bond_and_LonePair rows from sibling `xenosite-legacy/src`. It is **idempotent**: molecule/model pairs already in the suite are skipped, and the JSON is checkpointed after each chunk. The gzipped suite `tests/fixtures/ob_dumps.json.gz` is committed via Git LFS so dump tests run without Docker; uncompressed JSON stays gitignored. Clone with Git LFS (`git lfs pull`).
140
+
141
+ Public parse/canonicalize stays RDKit. Feature graphs call OpenBabel internally (PyPI 3.2.x). `tests/test_ob_features.py` compares host OpenBabel 3.2 rows to 2.4 dumps at atol `1e-4` / rtol `0`. Missing dumps fail. Hypothesis draws random finite matrices for ONNX heads (`test_onnx_random_matrix_finite`) and live `/nn` vs ONNX (`test_random_vector_nn`). The convert dump `tests/fixtures/random_vectors.json` is the Python-2 regression (ONNX == pickled numpy NN).
142
+
143
+ ## Layout
144
+
145
+ ```
146
+ src/xenosite/predict/ # user API (installed)
147
+ tools/ # extract, convert, legacy-test-api (not in the wheel)
148
+ weights/ # local only — README + .gitignore committed
149
+ tests/ # unit + @pytest.mark.live
150
+ docs/vendored-diffs.md # NN/feature hashes, MOPAC/SmartCYP gate
151
+ docs/legacy-vs-principled.md # production defaults vs golden legacy modes
152
+ ```
153
+
154
+ ## Development
155
+
156
+ ```
157
+ uv sync --group dev
158
+ make test
159
+ ```
160
+
161
+ ### Publishing to PyPI (trusted publishing)
162
+
163
+ No long-lived PyPI tokens. Releases use GitHub OIDC via `.github/workflows/publish.yml`.
164
+
165
+ 1. On PyPI, add a **pending** trusted publisher (project not published yet) at
166
+ [pypi.org/manage/account/publishing](https://pypi.org/manage/account/publishing/):
167
+ - Project: `xenosite-predict`
168
+ - Owner: `swamidasslab`
169
+ - Repo: `xenosite-predict`
170
+ - Workflow: `publish.yml`
171
+ - Environment: `pypi`
172
+ 2. In GitHub → Settings → Environments, create `pypi` (add required reviewers if you want a human gate).
173
+ 3. Merge the workflow, then either push a tag `v0.2.0` or run **Publish** manually.
174
+ 4. The first successful publish creates the PyPI project; later releases reuse the same publisher.
175
+
176
+ Do **not** commit `XENOSITE_ONNX_URL`, API keys, or weight hostnames. Keep those in local env / deployment secrets only.
177
+
178
+ Vendored-tree comparison (sibling checkout, not committed):
179
+
180
+ ```
181
+ uv run python tools/compare_vendored.py --root ../xenosite-legacy/src/libridass
182
+ ```