diff --git a/CLAUDE.md b/CLAUDE.md
new file mode 100644
index 0000000..829bcc7
--- /dev/null
+++ b/CLAUDE.md
@@ -0,0 +1,86 @@
+# CLAUDE.md
+
+## Project overview
+
+pyvcell is the Python interface for [Virtual Cell](https://vcell.org) — spatial modeling, simulation, and analysis of cell biological systems. It wraps the VCell finite volume solver and provides a high-level API for building models, running simulations, and analyzing results.
+
+- **Repository**: https://github.com/virtualcell/pyvcell
+- **Documentation**: https://virtualcell.github.io/pyvcell/
+- **Python**: >= 3.11
+
+## Key commands
+
+```bash
+make check # Run all quality checks (lint, type check, deptry)
+make test # Run pytest with coverage
+make docs # Build and serve documentation locally
+make docs-test # Test documentation build
+```
+
+### Verification (run after code changes)
+
+Run `make check`, which does:
+
+1. `poetry check --lock` — verify lock file consistency
+2. `poetry run pre-commit run -a` — linting (ruff, ruff-format, prettier)
+3. `poetry run mypy` — static type checking
+4. `poetry run deptry --exclude=.venv --exclude=.venv_jupyter --exclude=examples --exclude=tests .` — obsolete dependency check
+
+Then run tests: `poetry run pytest tests -v`
+
+## Project structure
+
+```
+pyvcell/
+ vcml/ # Main public API — models, reader, writer, simulation, remote
+ sbml/ # SBML spatial model support
+ sim_results/ # Result objects, plotting, VTK data
+ _internal/
+ api/ # Generated REST client (OpenAPI) — do not edit by hand
+ geometry/ # Geometry utilities (SegmentedImageGeometry)
+ simdata/ # Simulation data: zarr, mesh, field data, VTK
+ solvers/ # FV solver wrapper
+tests/
+ vcml/ # VCML reader/writer/simulation tests
+ sbml/ # SBML tests
+ sim_results/ # Result and plotting tests
+ guides/ # Notebook execution tests
+ _internal/ # Internal module tests
+ fixtures/ # Test data and fixtures
+docs/ # mkdocs-material site with guides and API reference
+scripts/
+ generate.sh # Regenerate OpenAPI client
+ python-fix.sh # Post-generation fixes for known codegen bugs
+ openapi.yaml # OpenAPI spec for VCell server
+```
+
+## Public API
+
+The main entry point is `import pyvcell.vcml as vc`. Key functions:
+
+- **Load models**: `vc.load_vcml_file()`, `vc.load_vcml_url()`, `vc.load_biomodel(id)`, `vc.load_sbml_file()`, `vc.load_antimony_str()`
+- **Simulate**: `vc.simulate(biomodel, sim_name)` (local), `vc.run_remote(...)` (server)
+- **Results**: `result.plotter.plot_concentrations()`, `result.plotter.plot_slice_3d()`
+
+## Code conventions
+
+- **Line length**: 120 characters (ruff)
+- **Type checking**: mypy strict mode; the `pyvcell._internal.api.*` module has relaxed overrides since it's auto-generated
+- **Linting**: ruff with flake8-bandit (S), bugbear (B), isort (I), pyupgrade (UP) and others enabled
+- **Pre-commit hooks exclude** `pyvcell/_internal/api/.*\.py` from ruff (generated code)
+- **Test fixtures**: defined in `tests/fixtures/` and imported via `tests/conftest.py`
+
+## Generated API client
+
+The `pyvcell/_internal/api/vcell_client/` directory is auto-generated from `scripts/openapi.yaml` using OpenAPI Generator. After regeneration:
+
+1. Run `scripts/generate.sh` (which calls `scripts/python-fix.sh` to patch known codegen bugs)
+2. Run `make check` to reformat and verify
+3. The generated code uses pydantic models with camelCase aliases — use alias names (e.g., `simulationName=`) when constructing models to satisfy mypy
+
+## Testing notes
+
+- Tests that call `plt.show()` or use VTK rendering need `matplotlib.use("Agg")` to avoid hanging on interactive display
+- CI uses `xvfb-run` for tests that need a display server (VTK/PyVista)
+- The `remote-simulations` notebook test is skipped (requires interactive auth)
+- Test expected values for `result.concentrations` use domain-masked means from the zarr writer
diff --git a/README.md b/README.md
index 083e26b..50be058 100644
--- a/README.md
+++ b/README.md
@@ -63,7 +63,7 @@ results.plotter.plot_concentrations()
# Documentation
-coming soon.
+Full documentation is available at **https://virtualcell.github.io/pyvcell/**
# Examples:
diff --git a/docs/getting-started/installation.md b/docs/getting-started/installation.md
index 458c420..147557b 100644
--- a/docs/getting-started/installation.md
+++ b/docs/getting-started/installation.md
@@ -41,16 +41,14 @@ vc.set_workspace_dir("/path/to/my/workspace")
The workspace directory is created automatically if it doesn't exist.
-## Optional dependencies
+## Included visualization libraries
-pyvcell includes visualization tools that depend on:
+pyvcell bundles several visualization libraries, all installed automatically:
- **Matplotlib** — 2D plots and concentration time series
- **VTK / PyVista** — 3D volume rendering and mesh visualization
- **Trame** — Interactive browser-based 3D widgets (for Jupyter notebooks)
-All of these are installed automatically with `pip install pyvcell`.
-
## Next steps
- [Quick Start](quickstart.md) — Load a model, simulate, and plot results
diff --git a/docs/getting-started/quickstart.md b/docs/getting-started/quickstart.md
index 6fb842d..f9c6695 100644
--- a/docs/getting-started/quickstart.md
+++ b/docs/getting-started/quickstart.md
@@ -11,7 +11,38 @@ biomodel = vc.load_vcml_file("path/to/model.vcml")
print(biomodel)
```
-You can also load models from a URL:
+You can also load public models directly from the VCell database by ID:
+
+```python
+biomodel = vc.load_biomodel("279851639")
+```
+
+To browse or search models by name, authenticate first:
+
+```python
+from pyvcell._internal.api.vcell_client.auth.auth_utils import login_interactive
+
+api_client = login_interactive() # opens a browser for login
+
+# List available models (public, shared, and your private models)
+for m in vc.list_biomodels(api_client=api_client)[:2]:
+ print(m)
+
+# Load by name and owner
+biomodel = vc.load_biomodel(name="Tutorial_MultiApp", owner="tutorial", api_client=api_client)
+
+# Load by database ID
+biomodel = vc.load_biomodel("279851639", api_client=api_client)
+```
+
+Output:
+
+```
+{'id': '117367327', 'name': ' Design dose in mammal MTB37rv', 'owner': 'mcgama88'}
+{'id': '102571573', 'name': ' Zika- denge differential test to fetus x 1', 'owner': 'mcgama88'}
+```
+
+Or from a URL:
```python
biomodel = vc.load_vcml_url(
diff --git a/docs/index.md b/docs/index.md
index 954ce29..591e102 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -30,13 +30,9 @@ result.plotter.plot_slice_3d(time_index=3, channel_id="s1")
## Getting started
-
-
- **[Installation](getting-started/installation.md)** — Install pyvcell and set up your environment
- **[Quick Start](getting-started/quickstart.md)** — Load a model, run a simulation, and plot results
-
-
## Guides
| Guide | Description |
diff --git a/pyproject.toml b/pyproject.toml
index 23f22d3..7ea2a9b 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
[tool.poetry]
name = "pyvcell"
-version = "0.1.21"
+version = "0.1.22"
description = "This is the python wrapper for vcell modeling and simulation"
authors = ["Jim Schaff "]
repository = "https://github.com/virtualcell/pyvcell"
diff --git a/pyvcell/vcml/__init__.py b/pyvcell/vcml/__init__.py
index 78a1c5f..793dd78 100644
--- a/pyvcell/vcml/__init__.py
+++ b/pyvcell/vcml/__init__.py
@@ -26,8 +26,10 @@
)
from pyvcell.vcml.utils import (
field_data_refs,
+ list_biomodels,
load_antimony_file,
load_antimony_str,
+ load_biomodel,
load_sbml_file,
load_sbml_str,
load_sbml_url,
@@ -98,6 +100,8 @@
"wait_for_simulation",
"export_n5",
"Field",
+ "list_biomodels",
+ "load_biomodel",
"load_vcml_url",
"load_sbml_url",
"suppress_stdout",
diff --git a/pyvcell/vcml/utils.py b/pyvcell/vcml/utils.py
index 197f7a7..958bca2 100644
--- a/pyvcell/vcml/utils.py
+++ b/pyvcell/vcml/utils.py
@@ -1,8 +1,14 @@
+from __future__ import annotations
+
import logging
import os
import sys
import tempfile
from os import PathLike
+from typing import TYPE_CHECKING
+
+if TYPE_CHECKING:
+ from pyvcell._internal.api.vcell_client.api_client import ApiClient
from pathlib import Path
import sympy # type: ignore[import-untyped]
@@ -196,12 +202,135 @@ def _download_url(url: str) -> str:
raise ValueError(f"Failed to download file from {url}: {response.status_code}")
-def load_vcml_biomodel_id(biomodel_id: str) -> Biomodel:
+def list_biomodels(api_client: ApiClient | None = None) -> list[dict[str, str | None]]:
+ """Return a list of accessible BioModels from the VCell server.
+
+ Each entry is a dictionary with ``"id"``, ``"name"``, and ``"owner"`` keys.
+ Requires an authenticated *api_client*.
+
+ Args:
+ api_client: A pre-configured, authenticated :class:`ApiClient`.
+
+ Returns:
+ A list of dictionaries, e.g.
+ ``[{"id": "279851639", "name": "My Model", "owner": "jsmith"}, ...]``
"""
- Load a VCML model from a VCell Biomodel ID.
+ from pyvcell._internal.api.vcell_client.api_client import ApiClient
+ from pyvcell._internal.api.vcell_client.configuration import Configuration
+
+ if api_client is None:
+ api_client = ApiClient(configuration=Configuration())
+
+ host = api_client.configuration.host
+ headers: dict[str, str] = {"Accept": "application/json"}
+ token = api_client.configuration.access_token
+ if token:
+ headers["Authorization"] = f"Bearer {token}"
+
+ import requests as _requests
+
+ resp = _requests.get(f"{host}/api/v1/bioModel/summaries", headers=headers, timeout=30)
+ if resp.status_code != 200:
+ raise ValueError(f"Failed to list BioModels: {resp.status_code} {resp.text[:200]}")
+
+ results: list[dict[str, str | None]] = []
+ for item in resp.json():
+ v = item.get("version")
+ if v is None:
+ continue
+ owner_info = v.get("owner")
+ results.append({
+ "id": v.get("versionKey"),
+ "name": v.get("name"),
+ "owner": owner_info.get("userName") if owner_info else None,
+ })
+ return results
+
+
+def load_biomodel(
+ biomodel_id: str | None = None,
+ *,
+ name: str | None = None,
+ owner: str | None = None,
+ api_client: ApiClient | None = None,
+) -> Biomodel:
+ """Load a VCell BioModel by database key or by name/owner lookup.
+
+ Provide either *biomodel_id* **or** *name* (optionally with *owner*)
+ to identify the model. When searching by name, the VCell server is
+ queried for all accessible BioModel summaries and filtered
+ client-side.
+
+ For public models loaded by *biomodel_id*, no *api_client* is needed —
+ an anonymous client is created automatically. Searching by *name*
+ requires an authenticated :class:`ApiClient`.
+
+ Args:
+ biomodel_id: The BioModel database key (e.g. ``"279851639"``).
+ name: BioModel name to search for (case-insensitive substring match).
+ Requires an authenticated *api_client*.
+ owner: Owner username to narrow the search (exact, case-insensitive).
+ api_client: Optional pre-configured :class:`ApiClient` for
+ accessing private models or searching by name.
+
+ Returns:
+ A parsed :class:`Biomodel` instance.
+
+ Raises:
+ ValueError: If no matching model is found or if the arguments are
+ ambiguous.
"""
- uri = f"https://vcell.cam.uchc.edu/api/v0/biomodel/{biomodel_id}/biomodel.vcml"
- return load_vcml_url(uri)
+ from pyvcell._internal.api.vcell_client.api.bio_model_resource_api import BioModelResourceApi
+ from pyvcell._internal.api.vcell_client.api_client import ApiClient
+ from pyvcell._internal.api.vcell_client.configuration import Configuration
+
+ if biomodel_id is None and name is None:
+ raise ValueError("Provide either biomodel_id or name to identify the model")
+ if biomodel_id is not None and name is not None:
+ raise ValueError("Provide either biomodel_id or name, not both")
+
+ if api_client is None:
+ api_client = ApiClient(configuration=Configuration())
+
+ bm_api = BioModelResourceApi(api_client)
+
+ if biomodel_id is not None:
+ vcml_str: str = bm_api.get_bio_model_vcml(biomodel_id, _headers={"Accept": "text/xml"})
+ return load_vcml_str(vcml_str)
+
+ # Search by name (and optionally owner) — name is guaranteed non-None here
+ # because we checked (biomodel_id is None and name is None) above.
+ name_lower = name.lower() # type: ignore[union-attr]
+ owner_lower = owner.lower() if owner else None
+
+ all_models = list_biomodels(api_client=api_client)
+ matches = []
+ for m in all_models:
+ m_name = m.get("name")
+ if m_name is None:
+ continue
+ if name_lower not in m_name.lower():
+ continue
+ if owner_lower is not None:
+ m_owner = m.get("owner")
+ if m_owner is None or m_owner.lower() != owner_lower:
+ continue
+ matches.append(m)
+
+ if len(matches) == 0:
+ msg = f"No BioModel found with name containing '{name}'"
+ if owner:
+ msg += f" owned by '{owner}'"
+ raise ValueError(msg)
+ if len(matches) > 1:
+ match_desc = ", ".join(f"'{m['name']}' (id={m['id']}, owner={m['owner']})" for m in matches)
+ raise ValueError(f"Multiple BioModels match name '{name}': {match_desc}. Use biomodel_id or owner to narrow.")
+
+ found_id = matches[0]["id"]
+ if found_id is None:
+ raise ValueError("Matched BioModel has no version key")
+ vcml_str = bm_api.get_bio_model_vcml(found_id, _headers={"Accept": "text/xml"})
+ return load_vcml_str(vcml_str)
def load_vcml_url(vcml_url: str) -> Biomodel:
diff --git a/tests/vcml/test_load_biomodel.py b/tests/vcml/test_load_biomodel.py
new file mode 100644
index 0000000..9f5bcb1
--- /dev/null
+++ b/tests/vcml/test_load_biomodel.py
@@ -0,0 +1,20 @@
+import pyvcell.vcml as vc
+from pyvcell._internal.api.vcell_client import ApiClient, Configuration
+
+
+def test_load_biomodel_public() -> None:
+ """Load a public BioModel by ID using the default anonymous client."""
+ # Dolgitzer 2025 — a published, public model
+ bm = vc.load_biomodel("279851639")
+ assert bm.name == "Dolgitzer 2025 A Continuum Model of Mechanosensation Based on Contractility Kit Assembly"
+ assert len(bm.applications) > 0
+ assert bm.version is not None
+ assert bm.version.key == "279851639"
+
+
+def test_load_biomodel_with_explicit_client() -> None:
+ """Load a public BioModel using an explicitly provided ApiClient."""
+ client = ApiClient(configuration=Configuration(host="https://vcell.cam.uchc.edu"))
+ bm = vc.load_biomodel("279851639", api_client=client)
+ assert bm.name == "Dolgitzer 2025 A Continuum Model of Mechanosensation Based on Contractility Kit Assembly"
+ assert len(bm.applications) > 0