Metadata-Version: 2.4
Name: omtx
Version: 2.0.12
Summary: Official Python SDK for the Om API
Author-email: Om <hello@omtx.ai>
License: MIT
Project-URL: Homepage, https://omtx.ai
Project-URL: Documentation, https://docs.omtx.ai
Project-URL: Source, https://github.com/omtx-ai/vibe-discovery
Project-URL: Issues, https://github.com/omtx-ai/vibe-discovery/issues
Project-URL: LULA-1 Weights, https://huggingface.co/omtx/lula-1
Project-URL: LULA-1 Quickstart, https://huggingface.co/omtx/lula-1/blob/main/notebooks/lula1_quickstart.ipynb
Keywords: om,omtx,bioinformatics,drug-discovery,protein-design
Classifier: Intended Audience :: Science/Research
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Operating System :: OS Independent
Classifier: Topic :: Scientific/Engineering :: Bio-Informatics
Classifier: Typing :: Typed
Requires-Python: >=3.9
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: requests>=2.31.0
Requires-Dist: polars>=1.17
Requires-Dist: rdkit>=2023.9.5
Provides-Extra: lula
Requires-Dist: huggingface_hub<2.0.0,>=0.24.0; extra == "lula"
Requires-Dist: numpy>=1.24; extra == "lula"
Requires-Dist: rdkit>=2023.9.1; extra == "lula"
Requires-Dist: torch>=2.4; extra == "lula"
Requires-Dist: transformers<5.0.0,>=4.35.0; extra == "lula"
Requires-Dist: safetensors>=0.4.3; extra == "lula"
Dynamic: license-file

# Om Python SDK (`omtx`)

Official Python SDK for the Om API.

The SDK talks to the public Om API `/v2/*` surface for Om-owned workflows and
to RCSB public file URLs for exact PDB structure downloads. It covers:
- Diligence workflows and job polling
- Active public Hub workflows through `client.hub.submit(...)` and selected typed helpers
- Hosted LULA-1 and LULA-2 protein-sequence plus SMILES scoring
- Artifact upload for artifact-backed Hub jobs
- Entitlement-scoped dataset catalog, shard exports, and Polars-backed `OmData` loaders
- Private data-generation order helpers for subscription quota and invoice-backed orders
- OnePot-backed molecule availability, quote, checkout, and order helpers
- Public RCSB PDB/mmCIF structure download helpers
- Health and Wallet Credits helpers

Public docs: `https://docs.omtx.ai`

## Installation

```bash
pip install omtx
```

## Compatibility

For best compatibility:
- Linux: modern Ubuntu on x86_64 or arm64
- macOS: Apple Silicon with a native `arm64` Python interpreter
- macOS Python distribution: Miniforge or Mambaforge recommended

The dataframe helpers in `omtx` use `polars`:
- `load_binders(...)`
- `load_nonbinders(...)`
- `load_data(...)`

If you are on Apple Silicon, use a native `arm64` shell and Python. Avoid
Rosetta / `x86_64` Python for dataframe-backed workflows.

If you are on older `x86_64` hardware, or if the default `polars` runtime fails
with CPU-feature errors, install the compatibility runtime:

```bash
pip install "polars[rtcompat]"
```

Or install both in one step:

```bash
pip install omtx "polars[rtcompat]"
```

JSON-based SDK methods may still work without `rtcompat`, but dataframe helpers
are not guaranteed on older `x86_64` CPUs unless the compatibility runtime is
installed.

## Setup

```bash
export OMTX_API_KEY="your-api-key"
```

The SDK targets `https://api.omtx.ai`.

## Quick Start

```python
from omtx import OmClient

with OmClient() as client:
    print(client.status())

    job = client.diligence.deep_diligence(
        query="CRISPR applications in cancer therapy",
        preset="quick",
    )

    result = client.jobs.wait(
        job["job_id"],
        result_endpoint="/v2/jobs/deep-diligence/{job_id}",
    )
    print(result.get("result", {}).get("total_claims"))
```

## Hub Quick Start

```python
from omtx import OmClient

with OmClient() as client:
    artifact = client.artifacts.upload("target.cif")

    job = client.hub.boltzgen(
        protocol="protein_anything",
        target_cif_artifact_id=artifact["artifact_id"],
        target_chain_id="A",
        binder_length_min=90,
        binder_length_max=110,
        idempotency_key="boltzgen-demo-20260325",
    )

    status = client.jobs.wait(job["job_id"], poll_interval=5, timeout=3600)
    print(status["status"])
```

For active public Hub models without a dedicated typed helper, use
`client.hub.submit(job_type="hub.<model>", payload=...)`.

## Hosted LULA Scoring

```python
from omtx import OmClient

with OmClient() as client:
    scores = client.lula2.score(
        protein_sequence="MEEPQSDPSV",
        smiles=["CCO", "c1ccccc1"],
    )
    print(scores[0]["score"])
```

Hosted `client.lula1.score(...)` and `client.lula2.score(...)` accept only a
protein sequence and SMILES. The SDK does not expose `mode`; LULA-1 and LULA-2
are separate product namespaces. Legacy async `client.hub.lula1(...)` remains
available for current Hub job compatibility.

LULA score records include `score`, `rank`, and `top_percentile_in_batch`.
`score` is a bounded 0-1 model score intended for ranking and enrichment, not a
calibrated binding probability. Rank and percentile are numeric values computed
within the submitted batch.

## Local LULA-1 Open-Weight Scoring

Install the optional local scorer only when you want to run LULA-1 weights on
your own machine:

```bash
pip install "omtx[lula]>=2.0.12"
omtx lula download
omtx lula verify
```

Model card, weights, license, and notebook:
[`huggingface.co/omtx/lula-1`](https://huggingface.co/omtx/lula-1).
Quickstart notebook:
[`lula1_quickstart.ipynb`](https://huggingface.co/omtx/lula-1/blob/main/notebooks/lula1_quickstart.ipynb).
LULA-1 is public ungated on Hugging Face. By downloading, accessing, or using
LULA-1, you agree to the
[Om LULA Community License 1.0](https://huggingface.co/omtx/lula-1/blob/main/LICENSE).
Commercial hosted inference, paid API/SaaS access, resale, paid
support/deployment, bundling model access into a paid product, and competing
model services require a separate Om commercial license.

No Hugging Face login is required for the public LULA-1 weights. In Colab,
`omtx>=2.0.12` does not pin NumPy below 2.

Score a real target:

```python
from omtx.lula import load_model

CA2 = (
    "MSHHWGYGKHNGPEHWHKDFPIAKGERQSPVDIDTHTAKYDPSLKPLSVSYDQATSLRIL"
    "NNGHAFNVEFDDSQDKAVLKGGPLDGTYRLIQFHFHWGSLDGQGSEHTVDKKKYAAELHL"
    "VHWNTKYGDFGKAVQQPDGLAVLGIFLKVGSAKPGLQKVVDVLDSIKTKGKSADFTNFDP"
    "RGLLPESLDYWTYPGSLTTPPLLECVTWIVLKEPISVSSEQVLKFRKLNFNGEGEPEELM"
    "VDNWRPAQPLKNRQIKASFK"
)
mols = [
    "CC(=O)Nc1nnc(s1)S(N)(=O)=O",
    "Cc1ccc(cc1)S(=O)(=O)N",
    "CC(C)Cc1ccc(cc1)C(C)C(=O)O",
    "CCN(CC)CCNC(=O)c1ccc(N)cc1",
    "c1ccc(cc1)C(=O)O",
    "CCO",
]

model = load_model()
for row in sorted(model.score(protein_sequence=CA2, smiles=mols), key=lambda r: r["rank"]):
    print(row["rank"], round(row["score"], 4), row["smiles"])
```

Local scoring uses downloaded weights and cached encoder assets. It does not
contact Om or Hugging Face during scoring unless you explicitly run a download.
The local scorer returns the same customer-facing score fields as hosted LULA:
`score`, `rank`, and `top_percentile_in_batch`. The canonical `score` remains the
0-1 model score; rank and percentile are batch-ranking aids. Do not interpret
any LULA score field as an experimental binding probability unless you have
calibrated it against your own assay data.
If you pass `--encoder-cache-dir` to `omtx lula download`, pass the same value to
`omtx lula score`.

## Molecule Fulfillment

```python
from omtx import OmClient

with OmClient() as client:
    scores = client.lula2.score(
        protein_sequence="MEEPQSDPSV",
        smiles=["CC(=O)Oc1ccccc1C(=O)O"],
    )
    selected = [row["smiles"] for row in scores]

    hits = client.molecules.search(
        smiles_list=selected,
        max_results=5,
    )
    quote = client.molecules.quote(
        items=[{"smiles": smiles, "quantity": 1} for smiles in selected]
    )
    addresses = client.molecules.shipping_addresses()
    shipping_address_id = addresses["default_shipping_address_id"]
    invoice = client.molecules.checkout(
        items=[{"smiles": smiles, "quantity": 1} for smiles in selected],
        shipping_address_id=shipping_address_id,
    )
```

The molecule namespace calls the canonical Om API `/v2/molecules/*` Gateway
routes, including `/v2/molecules/fulfillment/shipping-addresses` for saved
shipping address reads.
routes. Provider matching, quote validation, invoice creation, and order state are
owned by the Gateway and Wallet services.

## Data-Generation Orders

```python
from omtx import OmClient

with OmClient() as client:
    sequences = [{"name": "target", "sequence": "M" * 120}]
    quota_order = client.data_generation.quota_order(
        sequences=sequences,
        idempotency_key="dg-quota-target-001",
    )
    invoice_order = client.data_generation.invoice_order(
        sequences=sequences,
        idempotency_key="dg-invoice-target-001",
    )
```

Public API data-generation order creation is private by default. Extra
data-generation orders are invoice-only through the SDK/API/MCP surface; card
checkout remains a frontend/internal flow.

## Data Access

Browse published protein-specific models with `client.models.catalog()`.
Generated and covered raw-data access is entitlement-scoped; fetch a `protein_uuid` from
`client.datasets.catalog()` or `client.datasets.generated_protein_uuids()` for
the authenticated account before loading generated or otherwise covered data.

Primary training flow (single call):

```python
loaded = client.load_data(
    protein_uuid="YOUR_GENERATED_PROTEIN_UUID",
    binders=50000,          # required
    nonbinder_multiplier=5, # optional, default 5x background negatives
    # nonbinders=200000,    # optional explicit override (wins over multiplier)
    sample_seed=42,         # optional: deterministic sampling
)

binders = loaded["binders"]
nonbinders = loaded["nonbinders"]
print(binders.shape, nonbinders.shape)
binders.show(top_n=24)  # defaults: smiles_col="smiles", sort_by="binding_score"
binders.show(top_n=24, sort_by="selectivity_score")
# show() renders inline in notebooks; no extra display() wrapper needed.
```

`load_data(...)` samples non-binders from the Gateway-provided non-binder
source. New binder-only vintages use canonical NEGATIVE-control binder shards;
the SDK adaptively reads a bounded randomized NEGATIVE window distributed across
returned shard URLs, excludes molecules already seen as target binders, and
reads up to the full NEGATIVE pool only when needed. Requested non-binders are
capped at `20x` requested binders; if the
anti-joined NEGATIVE pool is smaller than the requested count, the SDK returns
the available non-overlapping rows.
Canonical NEGATIVE responses must include explicit source metadata
(`source_protein_uuid` and `source_vintage_id`); missing metadata is treated as
a contract error. Legacy target-specific non-binder shards remain supported.

Explicit per-pool loading (advanced control):

```python
binders = client.load_binders(
    protein_uuid="YOUR_GENERATED_PROTEIN_UUID",
    n=1000,          # optional: random sample size
    sample_seed=42,  # optional: deterministic sampling
)
nonbinders = client.load_nonbinders(
    protein_uuid="YOUR_GENERATED_PROTEIN_UUID",
    n=10000,         # optional: random sample size
    sample_seed=42,  # optional: deterministic sampling
)
print(binders.shape, nonbinders.shape)

# Omit n (or set n=None) to load the full pool.
# binders = client.load_binders(protein_uuid="...")
# nonbinders = client.load_nonbinders(protein_uuid="...")  # legacy or derived pools
```

Manual shard export URLs (advanced use):

```python
urls = client.binders.urls(
    protein_uuid="YOUR_GENERATED_PROTEIN_UUID",
)
print("Binder shard URLs:", len(urls["binder_urls"]))
print("Non-binder shard URLs:", len(urls["non_binder_urls"]))
print("First binder URL:", urls["binder_urls"][0] if urls["binder_urls"] else None)
```

Generated proteins available now:

```python
protein_uuids = client.datasets.generated_protein_uuids()
print("Generated protein UUIDs:", protein_uuids[:5])
```

Module-level convenience:

```python
import omtx as om

loaded = om.load_data(
    protein_uuid="YOUR_GENERATED_PROTEIN_UUID",
    binders=50000,
    nonbinder_multiplier=5,
    sample_seed=42,
)
print(loaded["binders"].shape, loaded["nonbinders"].shape)

binders = om.load_binders(
    protein_uuid="YOUR_GENERATED_PROTEIN_UUID",
    n=1000,
    sample_seed=42,
)
nonbinders = om.load_nonbinders(
    protein_uuid="YOUR_GENERATED_PROTEIN_UUID",
    n=10000,
    sample_seed=42,
)
print(binders.shape, nonbinders.shape)
```

## Idempotency

- Every non-GET call gets an idempotency key automatically.
- Auto-generated keys are per-call convenience and are not retry-stable.
- For retry dedupe, pass and reuse your own `idempotency_key` (logical operation ID).

Example (retry-safe launch):

```python
request_key = "search-protein-x-20260303-001"

job = client.diligence.search(
    query="MKNK2 inhibitor landscape",
    idempotency_key=request_key,
)

# If you retry the same logical launch, reuse the same idempotency key.
# retried = client.diligence.search(query="MKNK2 inhibitor landscape", idempotency_key=request_key)
```

## Wallet Funding

Use `client.wallet.topup(...)` to explicitly fund Wallet Credits by saved card
or invoice. Saved-card top-ups are capped at `$500` per request; invoice
top-ups can be larger. Wallet funding requires an explicit retry-stable
`idempotency_key` and exact user approval text.

```python
confirmation = client.wallet.expected_topup_confirmation(
    amount_cents=50_000,
    payment_mode="saved_card",
)

topup = client.wallet.topup(
    amount_cents=50_000,
    payment_mode="saved_card",
    user_approval_confirmation=confirmation,
    idempotency_key="wallet-topup-target-x-001",
)
```

## Helper Surface

- `diligence.deep_diligence(query, preset=None, idempotency_key=None)`
- `diligence.synthesize_report(gene_key, idempotency_key=None)`
- `diligence.search(query, idempotency_key=None)`
- `diligence.gather(query, preset=None, idempotency_key=None)`
- `diligence.crawl(url, preset=None, idempotency_key=None)`
- `diligence.list_gene_keys()`
- `artifacts.upload(file_path, content_type=None)`, `artifacts.upload_bytes(...)`, `artifacts.get(artifact_id)`
- `hub.submit(job_type, payload, idempotency_key=None)` for the full active public Hub route set
- selected typed `hub.<model>(...)` helpers for `boltz2`, `boltzgen`, `rosettafold3`, `chai1`, `rfd3`, `bindcraft`, `alphafold`, `proteinttt`, `diffdock`, `flowdock`, `neuralplexer`, `openfold3`, `lula1`, `protein_specific_models`
- `lula1.score(protein_sequence, smiles, idempotency_key=None, include_metadata=False)`
- `lula2.score(protein_sequence, smiles, idempotency_key=None, include_metadata=False)`
- optional local LULA-1 open-weight helpers through `omtx.lula.load_model(...)`
  after installing `omtx[lula]`
- optional CLI commands installed by the `omtx` package: `omtx lula download`,
  `omtx lula verify`, and `omtx lula score`
- `data_generation.quota_order(sequences, ...)`
- `data_generation.invoice_order(sequences, ...)`
- `molecules.pricing()`
- `molecules.search(smiles_list, ...)`
- `molecules.quote(items, ...)`
- `molecules.shipping_addresses()`
- `molecules.checkout(items, shipping_address_id, ...)`
- `molecules.orders(limit=20)`
- `molecules.status(order_number)`
- `models.catalog(protein_uuid=None, q=None, limit=100, offset=0)`
- `structures.pdb_download(pdb_id, format="pdb")`
- `structures.pdb_save(pdb_id, output_dir=".", format="pdb", overwrite=False)`
- `wallet.expected_topup_confirmation(amount_cents, payment_mode)`
- `wallet.topup(amount_cents, payment_mode, user_approval_confirmation, idempotency_key)`
- `vibe_video.submit(music_prompt, visual_prompt=None, artist_name=None, title_hint=None, duration_seconds=890, credit_artist=True, idempotency_key=None)`
- `vibe_video.list(limit=20)`
- `vibe_video.status(submission_id)`
- `jobs.history(...)`, `jobs.status(job_id)`, `jobs.wait(job_id, ...)`
- `binders.get_shards(...)`
- `binders.urls(...)`
- `load_binders(...)`
- `load_nonbinders(...)`
- `load_data(...)` (combined binder/non-binder load)
- `datasets.catalog()`
- `datasets.generated_protein_uuids()`
- `status()`
- `users.profile()`

Visualization column contract:
- `OmData.show(...)` is strict (no column fallback aliases).
- Default columns are `smiles` and `binding_score`.
- For selectivity views, pass `sort_by="selectivity_score"`.

Route policy:
- `/v2/diligence/getTargetDiligenceReport` remains an alias route and is not a separate SDK helper.
- `/v2/rag/search` is intentionally not exposed in the SDK.
- Public Hub coverage follows the active public model set in the canonical
  gateway route inventory.
- `hub.submit(...)` covers the full active public model set.
- Typed helpers are the selected subset listed above.
- Molecule fulfillment helpers call only the canonical `/v2/molecules/*`
  Gateway routes. Public molecule orders are invoice-only.
- `models.catalog(...)` calls `/v2/models/catalog` for published protein-specific
  model discovery.
- `structures.pdb_download(...)` fetches exact public structure files from
  `https://files.rcsb.org/download/{PDB_ID}.{format}`. It accepts only
  canonical `pdb` or `cif` formats.
- Data-generation order helpers call only the canonical
  `/v2/data-generation/*` Gateway routes. Public extra orders are invoice-only;
  quota orders consume active subscription capacity.
- Vibe video helpers call only the canonical `/v2/vibe-video/*` Gateway routes.

## Hub and Artifacts

```python
artifact = client.artifacts.upload("target.pdb")

job = client.hub.diffdock(
    protein_artifact_id=artifact["artifact_id"],
    ligand_smiles="CCO",
    idempotency_key="diffdock-demo-20260316",
)

status = client.jobs.wait(job["job_id"], poll_interval=5, timeout=1800)
history = client.jobs.history(limit=20)
```

Notes:

- Artifact-backed Hub workflows upload via `client.artifacts.*` first, then pass
  artifact IDs into canonical `client.hub.*` request fields.
- `client.hub.submit(...)` is the generic escape hatch for active Hub models
  using canonical `job_type="hub.<model>"`.
- Active public models without a dedicated helper, such as `neuralplexer`, are
  launched through `client.hub.submit(...)`.
- `jobs.history(...)` returns recent user jobs newest-first and paginates with
  `limit` and `cursor`.

## Migration

Breaking changes in `2.0.0`:
- `OMTXClient` removed.
- `OmClient` is now the only supported client class.
- Legacy pricing helpers removed from SDK surface.
- Legacy binder batch-cost helper removed from SDK surface.
- Shard access now resolves latest accessible dataset by `protein_uuid`.
- Generated data access is granted by qualifying covered data-generation
  sequences for the matching `protein_uuid`, including legacy-public snapshots
  where the account has covered access.
- `client.status()` is the primary health helper.
- `load_data(...)` is the primary training-set helper; it samples target binders and background negatives from binder pools.
- `load_binders(...)` remains the primary binder loader; `load_nonbinders(...)` remains available for legacy target non-binders and canonical NEGATIVE-control derived pools.
- Flat shard URL aliases are available as `binder_urls` / `non_binder_urls`.
- Core SDK runtime includes `polars` + `rdkit`.

Migration mapping (`1.x` -> `2.x`):
- `from omtx import OMTXClient` -> `from omtx import OmClient`
- `OMTXClient(...)` -> `OmClient(...)`

Breaking changes in `1.0.0`:
- `binders.get(...)` removed from core SDK.
- `binders.iter(...)` removed from core SDK.
- `pandas` removed from required dependencies.

Migration mapping (`0.x` -> `1.x`):
- `binders.get(...)` -> `client.load_binders(...)` / `client.load_nonbinders(...)` or `binders.get_shards(...)`
- `binders.iter(...)` -> `binders.get_shards(...)` + application-level streaming
- `pip install omtx` (with pandas) -> `pip install omtx` (with polars + rdkit)

Full details: see [`MIGRATION.md`](MIGRATION.md).

## Requirements

- Python `>=3.9`
- OMTX API key

## License

MIT. See `LICENSE`.
