├── polaris ├── hub │ ├── __init__.py │ ├── settings.py │ └── oauth.py ├── utils │ ├── __init__.py │ ├── constants.py │ ├── zarr │ │ ├── __init__.py │ │ ├── _memmap.py │ │ ├── _utils.py │ │ ├── _manifest.py │ │ └── codecs.py │ ├── misc.py │ ├── context.py │ ├── errors.py │ ├── dict2html.py │ └── types.py ├── experimental │ └── __init__.py ├── prediction │ ├── __init__.py │ └── _predictions_v2.py ├── loader │ ├── __init__.py │ └── load.py ├── mixins │ ├── __init__.py │ ├── _format_text.py │ └── _checksum.py ├── _version.py ├── evaluate │ ├── metrics │ │ ├── __init__.py │ │ ├── generic_metrics.py │ │ └── docking_metrics.py │ ├── __init__.py │ ├── _metadata.py │ └── utils.py ├── dataset │ ├── converters │ │ ├── __init__.py │ │ ├── _base.py │ │ └── _zarr.py │ ├── __init__.py │ ├── _adapters.py │ └── _column.py ├── benchmark │ ├── __init__.py │ ├── _definitions.py │ ├── _task.py │ ├── _split.py │ └── _split_v2.py ├── __init__.py ├── cli.py ├── model │ └── __init__.py └── _artifact.py ├── docs ├── community │ └── community.md ├── api │ ├── model.md │ ├── competition.evaluation.md │ ├── competition.md │ ├── subset.md │ ├── adapters.md │ ├── load.md │ ├── base.md │ ├── utils.types.md │ ├── hub.external_client.md │ ├── hub.storage.md │ ├── benchmark.md │ ├── dataset.md │ ├── hub.client.md │ ├── factory.md │ ├── converters.md │ └── evaluation.md ├── images │ └── zarr.png ├── resources.md ├── assets │ └── css │ │ └── custom-polaris.css ├── index.md ├── tutorials │ ├── create_a_model.ipynb │ └── submit_to_competition.ipynb └── quickstart.md ├── .github ├── CODEOWNERS ├── ISSUE_TEMPLATE │ ├── config.yml │ ├── feature-request.md │ ├── default-template.md │ └── bug-report.yml ├── workflows │ ├── code-check.yml │ ├── doc.yml │ ├── test.yml │ └── release.yml ├── PULL_REQUEST_TEMPLATE.md ├── changelog_config.json └── CODE_OF_CONDUCT.md ├── tests ├── test_import.py ├── test_codecs.py ├── test_hub_integration.py ├── test_oauth.py ├── test_type_checks.py ├── test_metrics.py ├── test_subset.py ├── test_storage.py ├── test_benchmark_predictions_v2.py ├── test_integration.py ├── test_competition.py ├── test_zarr_checksum.py ├── test_factory.py └── test_dataset_v2.py ├── NOTICE ├── env.yml ├── .gitignore ├── README.md ├── mkdocs.yml └── pyproject.toml /polaris/hub/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/community/community.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /polaris/utils/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @cwognum 2 | -------------------------------------------------------------------------------- /polaris/experimental/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/api/model.md: -------------------------------------------------------------------------------- 1 | ::: polaris.model.Model 2 | 3 | --- -------------------------------------------------------------------------------- /docs/api/competition.evaluation.md: -------------------------------------------------------------------------------- 1 | ::: polaris.evaluate.CompetitionPredictions 2 | -------------------------------------------------------------------------------- /docs/api/competition.md: -------------------------------------------------------------------------------- 1 | ::: polaris.competition.CompetitionSpecification 2 | 3 | --- -------------------------------------------------------------------------------- /docs/images/zarr.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/polaris-hub/polaris/HEAD/docs/images/zarr.png -------------------------------------------------------------------------------- /docs/api/subset.md: -------------------------------------------------------------------------------- 1 | ::: polaris.dataset.Subset 2 | options: 3 | members: no 4 | 5 | --- 6 | -------------------------------------------------------------------------------- /docs/api/adapters.md: -------------------------------------------------------------------------------- 1 | 2 | ::: polaris.dataset._adapters 3 | options: 4 | filters: ["!^_"] 5 | -------------------------------------------------------------------------------- /tests/test_import.py: -------------------------------------------------------------------------------- 1 | def test_import(): 2 | """Sanity check.""" 3 | import polaris # noqa: F401 4 | -------------------------------------------------------------------------------- /docs/api/load.md: -------------------------------------------------------------------------------- 1 | 2 | ::: polaris.load_dataset 3 | 4 | --- 5 | 6 | ::: polaris.load_benchmark 7 | 8 | --- 9 | -------------------------------------------------------------------------------- /docs/api/base.md: -------------------------------------------------------------------------------- 1 | ::: polaris._artifact.BaseArtifactModel 2 | options: 3 | filters: ["!^_"] 4 | 5 | --- -------------------------------------------------------------------------------- /polaris/prediction/__init__.py: -------------------------------------------------------------------------------- 1 | from ._predictions_v2 import BenchmarkPredictionsV2 2 | 3 | __all__ = ["BenchmarkPredictionsV2"] 4 | -------------------------------------------------------------------------------- /docs/api/utils.types.md: -------------------------------------------------------------------------------- 1 | ::: polaris.utils.types 2 | options: 3 | show_root_heading: false 4 | show_root_toc_entry: false 5 | 6 | --- -------------------------------------------------------------------------------- /polaris/utils/constants.py: -------------------------------------------------------------------------------- 1 | import platformdirs 2 | 3 | # Default base dir to cache any data 4 | DEFAULT_CACHE_DIR = platformdirs.user_cache_dir("polaris") 5 | -------------------------------------------------------------------------------- /polaris/loader/__init__.py: -------------------------------------------------------------------------------- 1 | from .load import load_benchmark, load_dataset, load_competition, load_model 2 | 3 | _all__ = ["load_benchmark", "load_dataset", "load_competition", "load_model"] 4 | -------------------------------------------------------------------------------- /polaris/mixins/__init__.py: -------------------------------------------------------------------------------- 1 | from polaris.mixins._checksum import ChecksumMixin 2 | from polaris.mixins._format_text import FormattingMixin 3 | 4 | __all__ = ["ChecksumMixin", "FormattingMixin"] 5 | -------------------------------------------------------------------------------- /docs/api/hub.external_client.md: -------------------------------------------------------------------------------- 1 | ::: polaris.hub.external_client.ExternalAuthClient 2 | options: 3 | merge_init_into_class: true 4 | filters: ["!create_authorization_url", "!fetch_token"] 5 | --- 6 | -------------------------------------------------------------------------------- /polaris/_version.py: -------------------------------------------------------------------------------- 1 | from importlib.metadata import PackageNotFoundError, version 2 | 3 | try: 4 | __version__ = version("polaris-lib") 5 | except PackageNotFoundError: 6 | # package is not installed 7 | __version__ = "dev" 8 | -------------------------------------------------------------------------------- /docs/api/hub.storage.md: -------------------------------------------------------------------------------- 1 | ::: polaris.hub.storage.StorageSession 2 | options: 3 | merge_init_into_class: true 4 | 5 | --- 6 | 7 | ::: polaris.hub.storage.S3Store 8 | options: 9 | merge_init_into_class: true 10 | --- 11 | -------------------------------------------------------------------------------- /docs/api/benchmark.md: -------------------------------------------------------------------------------- 1 | ::: polaris.benchmark.BenchmarkV2Specification 2 | options: 3 | filters: ["!^_", "!md5sum", "!get_cache_path"] 4 | 5 | 6 | ::: polaris.benchmark.BenchmarkV1Specification 7 | options: 8 | filters: ["!^_", "!md5sum", "!get_cache_path"] 9 | 10 | --- -------------------------------------------------------------------------------- /polaris/evaluate/metrics/__init__.py: -------------------------------------------------------------------------------- 1 | from polaris.evaluate.metrics.docking_metrics import rmsd_coverage 2 | from polaris.evaluate.metrics.generic_metrics import ( 3 | absolute_average_fold_error, 4 | average_precision_score, 5 | cohen_kappa_score, 6 | pearsonr, 7 | spearman, 8 | ) 9 | -------------------------------------------------------------------------------- /docs/api/dataset.md: -------------------------------------------------------------------------------- 1 | ::: polaris.dataset.DatasetV2 2 | options: 3 | filters: ["!^_"] 4 | 5 | --- 6 | 7 | ::: polaris.dataset._base.BaseDataset 8 | options: 9 | filters: ["!^_"] 10 | 11 | --- 12 | 13 | ::: polaris.dataset.ColumnAnnotation 14 | options: 15 | filters: ["!^_"] 16 | 17 | --- 18 | 19 | -------------------------------------------------------------------------------- /docs/api/hub.client.md: -------------------------------------------------------------------------------- 1 | ::: polaris.hub.settings.PolarisHubSettings 2 | options: 3 | filters: ["!^_"] 4 | 5 | --- 6 | 7 | 8 | ::: polaris.hub.client.PolarisHubClient 9 | options: 10 | merge_init_into_class: true 11 | filters: ["!^_", "!create_authorization_url", "!fetch_token", "!request", "!token"] 12 | --- 13 | -------------------------------------------------------------------------------- /docs/api/factory.md: -------------------------------------------------------------------------------- 1 | ::: polaris.dataset.DatasetFactory 2 | options: 3 | filters: ["!^_"] 4 | 5 | --- 6 | 7 | ::: polaris.dataset.create_dataset_from_file 8 | options: 9 | filters: ["!^_"] 10 | 11 | --- 12 | 13 | ::: polaris.dataset.create_dataset_from_files 14 | options: 15 | filters: ["!^_"] 16 | 17 | --- 18 | -------------------------------------------------------------------------------- /polaris/dataset/converters/__init__.py: -------------------------------------------------------------------------------- 1 | from polaris.dataset.converters._base import Converter 2 | from polaris.dataset.converters._sdf import SDFConverter 3 | from polaris.dataset.converters._zarr import ZarrConverter 4 | from polaris.dataset.converters._pdb import PDBConverter 5 | 6 | 7 | __all__ = ["Converter", "SDFConverter", "ZarrConverter", "PDBConverter"] 8 | -------------------------------------------------------------------------------- /polaris/utils/zarr/__init__.py: -------------------------------------------------------------------------------- 1 | from ._checksum import ZarrFileChecksum, compute_zarr_checksum 2 | from ._manifest import generate_zarr_manifest 3 | from ._memmap import MemoryMappedDirectoryStore 4 | 5 | __all__ = [ 6 | "MemoryMappedDirectoryStore", 7 | "compute_zarr_checksum", 8 | "ZarrFileChecksum", 9 | "generate_zarr_manifest", 10 | ] 11 | -------------------------------------------------------------------------------- /polaris/mixins/_format_text.py: -------------------------------------------------------------------------------- 1 | class FormattingMixin: 2 | """Mixin class for formatting strings to be output in the console""" 3 | 4 | BOLD = "\033[1m" 5 | YELLOW = "\033[93m" 6 | _END_CODE = "\033[0m" 7 | 8 | def format(self, text: str, codes: str | list[str]): 9 | if not isinstance(codes, list): 10 | codes = [codes] 11 | 12 | return "".join(codes) + text + self._END_CODE 13 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: ❓ Discuss something on GitHub Discussions 4 | url: https://github.com/polaris-hub/polaris/discussions 5 | about: For questions like "How do I do X with Polaris?", you can move to GitHub Discussions. 6 | - name: ❓ Discuss something on Discord 7 | url: https://discord.gg/vBFd8p6H7u 8 | about: For more interactive discussions, you can join our Discord server. -------------------------------------------------------------------------------- /docs/api/converters.md: -------------------------------------------------------------------------------- 1 | ::: polaris.dataset.converters.Converter 2 | options: 3 | filters: ["!^_"] 4 | 5 | --- 6 | 7 | 8 | ::: polaris.dataset.converters.SDFConverter 9 | options: 10 | filters: ["!^_"] 11 | 12 | --- 13 | 14 | ::: polaris.dataset.converters.ZarrConverter 15 | options: 16 | filters: ["!^_"] 17 | 18 | --- 19 | 20 | ::: polaris.dataset.converters.PDBConverter 21 | options: 22 | filters: ["!^_"] 23 | 24 | --- 25 | -------------------------------------------------------------------------------- /tests/test_codecs.py: -------------------------------------------------------------------------------- 1 | import datamol as dm 2 | import zarr 3 | 4 | from polaris.utils.zarr.codecs import RDKitMolCodec 5 | 6 | 7 | def test_rdkit_mol_codec(): 8 | mol = dm.to_mol("C1=CC=CC=C1") 9 | 10 | arr = zarr.empty(shape=10, chunks=2, dtype=object, object_codec=RDKitMolCodec()) 11 | 12 | arr[0] = mol 13 | arr[1] = mol 14 | arr[2] = mol 15 | 16 | assert dm.same_mol(arr[0], mol) 17 | assert dm.same_mol(arr[1], mol) 18 | assert dm.same_mol(arr[2], mol) 19 | -------------------------------------------------------------------------------- /polaris/benchmark/__init__.py: -------------------------------------------------------------------------------- 1 | from polaris.benchmark._base import ( 2 | BenchmarkV1Specification, 3 | BenchmarkV1Specification as BenchmarkSpecification, 4 | ) 5 | from polaris.benchmark._benchmark_v2 import BenchmarkV2Specification 6 | from polaris.benchmark._definitions import MultiTaskBenchmarkSpecification, SingleTaskBenchmarkSpecification 7 | 8 | __all__ = [ 9 | "BenchmarkSpecification", 10 | "BenchmarkV1Specification", 11 | "BenchmarkV2Specification", 12 | "SingleTaskBenchmarkSpecification", 13 | "MultiTaskBenchmarkSpecification", 14 | ] 15 | -------------------------------------------------------------------------------- /docs/resources.md: -------------------------------------------------------------------------------- 1 | # Resources 2 | 3 | ## Publications 4 | 5 | - Correspondence in Nature Biotechnology: [10.1038/s42256-024-00911-w](https://doi.org/10.1038/s42256-024-00911-w). 6 | - Preprint on Method Comparison Protocols: [10.26434/chemrxiv-2024-6dbwv-v2](https://doi.org/10.26434/chemrxiv-2024-6dbwv-v2). 7 | 8 | ## Talks 9 | 10 | - PyData London (June, 2024): [https://www.youtube.com/watch?v=YZDfD9D7mtE](https://www.youtube.com/watch?v=YZDfD9D7mtE) 11 | - MoML (June, 2024): [https://www.youtube.com/watch?v=Tsz_T1WyufI](https://www.youtube.com/watch?v=Tsz_T1WyufI) 12 | 13 | --- -------------------------------------------------------------------------------- /tests/test_hub_integration.py: -------------------------------------------------------------------------------- 1 | import polaris as po 2 | from polaris.benchmark._base import BenchmarkV1Specification 3 | from polaris.dataset._base import BaseDataset 4 | from polaris.hub.settings import PolarisHubSettings 5 | 6 | settings = PolarisHubSettings() 7 | 8 | 9 | def test_load_dataset_flow(): 10 | dataset = po.load_dataset("polaris/hello-world") 11 | assert isinstance(dataset, BaseDataset) 12 | 13 | 14 | def test_load_benchmark_flow(): 15 | benchmark = po.load_benchmark("polaris/hello-world-benchmark") 16 | assert isinstance(benchmark, BenchmarkV1Specification) 17 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | Copyright 2025 Valence Labs 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | -------------------------------------------------------------------------------- /docs/api/evaluation.md: -------------------------------------------------------------------------------- 1 | ::: polaris.evaluate.BenchmarkPredictions 2 | 3 | --- 4 | 5 | ::: polaris.evaluate.ResultsMetadata 6 | options: 7 | filters: ["!^_"] 8 | 9 | --- 10 | 11 | ::: polaris.evaluate.EvaluationResult 12 | 13 | --- 14 | 15 | ::: polaris.evaluate.BenchmarkResults 16 | 17 | --- 18 | 19 | ::: polaris.evaluate.MetricInfo 20 | 21 | --- 22 | 23 | ::: polaris.evaluate.Metric 24 | options: 25 | filters: ["!^_", "!fn", "!is_multitask", "!y_type"] 26 | 27 | --- 28 | 29 | ::: polaris.evaluate.metrics.generic_metrics 30 | ::: polaris.evaluate.metrics.docking_metrics 31 | 32 | --- 33 | -------------------------------------------------------------------------------- /polaris/utils/zarr/_memmap.py: -------------------------------------------------------------------------------- 1 | import mmap 2 | 3 | import zarr 4 | 5 | 6 | class MemoryMappedDirectoryStore(zarr.DirectoryStore): 7 | """ 8 | A Zarr Store to open chunks as memory-mapped files. 9 | See also [this Github issue](https://github.com/zarr-developers/zarr-python/issues/1245). 10 | 11 | Memory mapping leverages low-level OS functionality to reduce the time it takes 12 | to read the content of a file by directly mapping to memory. 13 | """ 14 | 15 | def _fromfile(self, fn): 16 | with open(fn, "rb") as fh: 17 | return memoryview(mmap.mmap(fh.fileno(), 0, access=mmap.ACCESS_READ)) 18 | -------------------------------------------------------------------------------- /.github/workflows/code-check.yml: -------------------------------------------------------------------------------- 1 | name: code-check 2 | 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | tags: [ "*" ] 7 | pull_request: 8 | branches: 9 | - "*" 10 | - "!gh-pages" 11 | 12 | jobs: 13 | 14 | python-lint-ruff: 15 | name: Python lint [ruff] 16 | runs-on: ubuntu-latest 17 | steps: 18 | - name: Checkout the code 19 | uses: actions/checkout@v4 20 | 21 | - name: Install uv 22 | uses: astral-sh/setup-uv@v5 23 | 24 | - name: Install the project 25 | run: uv sync --group dev 26 | 27 | - name: Lint 28 | run: uv run ruff check 29 | 30 | - name: Format 31 | run: uv run ruff format --check 32 | -------------------------------------------------------------------------------- /polaris/__init__.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from rich.logging import RichHandler 4 | 5 | from ._version import __version__ 6 | from .loader import load_benchmark, load_competition, load_dataset, load_model 7 | 8 | __all__ = ["load_dataset", "load_benchmark", "load_competition", "load_model", "__version__"] 9 | 10 | # Polaris specific logger 11 | logger = logging.getLogger(__name__) 12 | 13 | # Only add handler if the logger has not already been configured externally 14 | if not logger.handlers: 15 | handler = RichHandler(rich_tracebacks=True) 16 | handler.setFormatter(logging.Formatter("%(message)s", datefmt="[%Y-%m-%d %X]")) 17 | logger.addHandler(handler) 18 | logger.setLevel(logging.INFO) 19 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature-request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 🚀 Feature request 3 | about: Suggest an idea for a new Polaris feature 4 | title: '' 5 | labels: feature 6 | assignees: '' 7 | --- 8 | 9 | ### Is your feature request related to a problem? Please describe. 10 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 11 | 12 | ### Describe the solution you'd like 13 | A clear and concise description of what you want to happen. 14 | 15 | ### Describe alternatives you've considered 16 | A clear and concise description of any alternative solutions or features you've considered. 17 | 18 | ### Additional context 19 | Add any other context or screenshots about the feature request here. 20 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## Changelogs 2 | 3 | - _enumerate the changes of that PR._ 4 | 5 | --- 6 | 7 | _Checklist:_ 8 | 9 | - [ ] _Was this PR discussed in an issue? It is recommended to first discuss a new feature into a GitHub issue before opening a PR._ 10 | - [ ] _Add tests to cover the fixed bug(s) or the newly introduced feature(s) (if appropriate)._ 11 | - [ ] _Update the API documentation if a new function is added, or an existing one is deleted._ 12 | - [ ] _Write concise and explanatory changelogs above._ 13 | - [ ] _If possible, assign one of the following labels to the PR: `feature`, `fix`, `chore`, `documentation` or `test` (or ask a maintainer to do it for you)._ 14 | 15 | --- 16 | 17 | _discussion related to that PR_ 18 | -------------------------------------------------------------------------------- /polaris/dataset/__init__.py: -------------------------------------------------------------------------------- 1 | from polaris.dataset._column import ColumnAnnotation, KnownContentType, Modality 2 | from polaris.dataset._dataset import DatasetV1 3 | from polaris.dataset._dataset import DatasetV1 as Dataset 4 | from polaris.dataset._dataset_v2 import DatasetV2 5 | from polaris.dataset._factory import DatasetFactory, create_dataset_from_file, create_dataset_from_files 6 | from polaris.dataset._subset import Subset 7 | from polaris.utils.zarr import codecs 8 | 9 | __all__ = [ 10 | "create_dataset_from_file", 11 | "create_dataset_from_files", 12 | "ColumnAnnotation", 13 | "Dataset", 14 | "DatasetFactory", 15 | "DatasetV1", 16 | "DatasetV2", 17 | "KnownContentType", 18 | "Modality", 19 | "Subset", 20 | ] 21 | -------------------------------------------------------------------------------- /.github/changelog_config.json: -------------------------------------------------------------------------------- 1 | { 2 | "categories": [ 3 | { 4 | "title": "## 🚀 Features", 5 | "labels": ["feature"] 6 | }, 7 | { 8 | "title": "## 🐛 Fixes", 9 | "labels": ["fix"] 10 | }, 11 | { 12 | "key": "tests", 13 | "title": "## 🧪 Tests", 14 | "labels": ["test"] 15 | }, 16 | { 17 | "key": "docs", 18 | "title": "## 📚 Documentation", 19 | "labels": ["documentation"] 20 | }, 21 | { 22 | "key": "chore", 23 | "title": "## 🧹 Chores", 24 | "labels": ["chore"] 25 | }, 26 | { 27 | "title": "## 📦 Other", 28 | "labels": [] 29 | } 30 | ] 31 | } 32 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/default-template.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Default Template 3 | about: Default, generic issue template 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | --- 8 | 9 | # Context 10 | 11 | _Provide some context for this issue: why is this change being requested, what constraint are there on the solution, are there any relevant artifacts(design documents, discussions, others) to this issue, etc._ 12 | 13 | # Description 14 | 15 | _Describe the expected work that will be needed to address the issue, leading into the following acceptance criteria. Add any relevant element that could impact the solution: limits, performance, security, compatibility, etc._ 16 | 17 | # Acceptance Criteria 18 | 19 | - List what needs to be checked and valid to determine that this issue can be closed 20 | 21 | # Links 22 | 23 | - [Link to other issues/PRs/external tasks](www.example.com) -------------------------------------------------------------------------------- /env.yml: -------------------------------------------------------------------------------- 1 | channels: 2 | - conda-forge 3 | 4 | dependencies: 5 | - python >=3.10,<3.13 6 | - pip 7 | - typer 8 | - pyyaml 9 | - pydantic >=2 10 | - pydantic-settings >=2 11 | - fsspec 12 | - typing-extensions >=4.12.0 13 | - boto3 <1.36.0 14 | - pyroaring 15 | - rich >=13.9.4 16 | 17 | # Hub client 18 | - authlib 19 | - httpx 20 | - requests 21 | - aiohttp 22 | 23 | # Scientific 24 | - numpy < 3 25 | - pandas 26 | - scipy 27 | - scikit-learn 28 | - seaborn 29 | 30 | # Chemistry 31 | - datamol >=0.12.1 32 | - fastpdb 33 | 34 | # Storage 35 | - zarr >=2,<3 36 | - pyarrow <18 37 | - numcodecs >=0.13.1,<0.16.0 38 | 39 | # Dev 40 | - pytest 41 | - pytest-xdist 42 | - pytest-cov 43 | - ruff 44 | - jupyterlab 45 | - ipywidgets 46 | - moto >=5.0.0 47 | 48 | # Doc 49 | - mkdocs 50 | - mkdocs-material >=9.4.7 51 | - mkdocstrings 52 | - mkdocstrings-python 53 | - mkdocs-jupyter >=0.24.8 54 | - markdown-include 55 | - mdx_truly_sane_lists 56 | - nbconvert 57 | - mike >=1.0.0 58 | -------------------------------------------------------------------------------- /.github/workflows/doc.yml: -------------------------------------------------------------------------------- 1 | name: doc 2 | 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | 7 | # Prevent doc action on `main` to conflict with each others. 8 | concurrency: 9 | group: doc-${{ github.ref }} 10 | cancel-in-progress: true 11 | 12 | jobs: 13 | doc: 14 | runs-on: "ubuntu-latest" 15 | timeout-minutes: 30 16 | 17 | defaults: 18 | run: 19 | shell: bash -l {0} 20 | 21 | steps: 22 | - name: Checkout the code 23 | uses: actions/checkout@v4 24 | 25 | - name: Install uv 26 | uses: astral-sh/setup-uv@v5 27 | 28 | - name: Install the project 29 | run: uv sync --group doc 30 | 31 | - name: Configure git 32 | run: | 33 | git config --global user.name "${GITHUB_ACTOR}" 34 | git config --global user.email "${GITHUB_ACTOR}@users.noreply.github.com" 35 | 36 | - name: Deploy the doc 37 | run: | 38 | echo "Get the gh-pages branch" 39 | git fetch origin gh-pages 40 | 41 | echo "Build and deploy the doc on main" 42 | uv run mike deploy --push main 43 | -------------------------------------------------------------------------------- /polaris/evaluate/__init__.py: -------------------------------------------------------------------------------- 1 | from polaris.evaluate._metadata import ResultsMetadataV1, ResultsMetadataV2 2 | from polaris.evaluate._metadata import ResultsMetadataV1 as ResultsMetadata 3 | from polaris.evaluate._metric import Metric, MetricInfo 4 | from polaris.evaluate._predictions import BenchmarkPredictions, CompetitionPredictions 5 | from polaris.evaluate._results import ( 6 | BenchmarkResultsV1 as BenchmarkResults, 7 | BenchmarkResultsV1, 8 | BenchmarkResultsV2, 9 | CompetitionResults, 10 | EvaluationResultV1 as EvaluationResult, 11 | EvaluationResultV1, 12 | EvaluationResultV2, 13 | ) 14 | from polaris.evaluate.utils import evaluate_benchmark 15 | 16 | __all__ = [ 17 | "ResultsMetadata", 18 | "ResultsMetadataV1", 19 | "ResultsMetadataV2", 20 | "Metric", 21 | "MetricInfo", 22 | "EvaluationResult", 23 | "EvaluationResultV1", 24 | "EvaluationResultV2", 25 | "BenchmarkResults", 26 | "BenchmarkResultsV1", 27 | "BenchmarkResultsV2", 28 | "CompetitionResults", 29 | "evaluate_benchmark", 30 | "CompetitionPredictions", 31 | "BenchmarkPredictions", 32 | ] 33 | -------------------------------------------------------------------------------- /polaris/dataset/converters/_base.py: -------------------------------------------------------------------------------- 1 | import abc 2 | from typing import TypeAlias 3 | 4 | import pandas as pd 5 | 6 | from polaris.dataset import ColumnAnnotation 7 | from polaris.dataset._adapters import Adapter 8 | from polaris.dataset._dataset import _INDEX_SEP 9 | 10 | FactoryProduct: TypeAlias = tuple[pd.DataFrame, dict[str, ColumnAnnotation], dict[str, Adapter]] 11 | 12 | 13 | class Converter(abc.ABC): 14 | @abc.abstractmethod 15 | def convert(self, path: str, append: bool = False) -> FactoryProduct: 16 | """This converts a file into a table and possibly annotations""" 17 | raise NotImplementedError 18 | 19 | @staticmethod 20 | def get_pointer(column: str, index: int | slice) -> str: 21 | """ 22 | Creates a pointer. 23 | 24 | Args: 25 | column: The name of the column. Each column has its own group in the root. 26 | index: The index or slice of the pointer. 27 | """ 28 | if isinstance(index, slice): 29 | index_substr = f"{_INDEX_SEP}{index.start}:{index.stop}" 30 | else: 31 | index_substr = f"{_INDEX_SEP}{index}" 32 | return f"{column}{index_substr}" 33 | -------------------------------------------------------------------------------- /docs/assets/css/custom-polaris.css: -------------------------------------------------------------------------------- 1 | :root { 2 | 3 | /* 4 | For a list of all available variables, see 5 | https://github.com/squidfunk/mkdocs-material/blob/master/src/assets/stylesheets/main/_colors.scss 6 | */ 7 | --polaris-primary: hsla(236, 100%, 19%, 1.0); 8 | --polaris-secondary: hsla(290, 61%, 43%, 1.0); 9 | --polaris-ternary: hsla(236, 100%, 9%, 1.0); 10 | } 11 | 12 | /* Change the header background to use a gradient */ 13 | .md-header { 14 | background-image: linear-gradient(to right, var(--polaris-secondary), var(--polaris-primary)); 15 | } 16 | 17 | /* Change the footer background to use a gradient */ 18 | .md-footer { 19 | background-image: linear-gradient(to right, var(--polaris-primary), var(--polaris-ternary)); 20 | } 21 | 22 | /* Change the tabs background to use a gradient */ 23 | .md-tabs { 24 | background-image: linear-gradient(to right, #F4F6F9, #dfc3e2); 25 | color: var(--polaris-ternary); 26 | } 27 | 28 | /* Remove the `In` and `Out` block in rendered Jupyter notebooks */ 29 | .md-container .jp-Cell-outputWrapper .jp-OutputPrompt.jp-OutputArea-prompt, 30 | .md-container .jp-Cell-inputWrapper .jp-InputPrompt.jp-InputArea-prompt { 31 | display: none !important; 32 | } 33 | -------------------------------------------------------------------------------- /polaris/dataset/_adapters.py: -------------------------------------------------------------------------------- 1 | from enum import Enum, auto, unique 2 | 3 | import datamol as dm 4 | 5 | 6 | @unique 7 | class Adapter(Enum): 8 | """ 9 | Adapters are predefined callables that change the format of the data. 10 | Adapters are serializable and can thus be saved alongside datasets. 11 | 12 | Attributes: 13 | SMILES_TO_MOL: Convert a SMILES string to a RDKit molecule. 14 | BYTES_TO_MOL: Convert a RDKit binary string to a RDKit molecule. 15 | ARRAY_TO_PDB: Convert a Zarr arrays to PDB arrays. 16 | """ 17 | 18 | SMILES_TO_MOL = auto() 19 | BYTES_TO_MOL = auto() 20 | ARRAY_TO_PDB = auto() 21 | 22 | def __call__(self, data): 23 | # Import here to prevent a cyclic import 24 | # Given the close coupling between `zarr_to_pdb` and the PDB converter, 25 | # we wanted to keep those functions in one file which was leading to a cyclic import. 26 | from polaris.dataset.converters._pdb import zarr_to_pdb 27 | 28 | conversion_map = {"SMILES_TO_MOL": dm.to_mol, "BYTES_TO_MOL": dm.Mol, "ARRAY_TO_PDB": zarr_to_pdb} 29 | 30 | if isinstance(data, tuple): 31 | return tuple(conversion_map[self.name](d) for d in data) 32 | return conversion_map[self.name](data) 33 | -------------------------------------------------------------------------------- /polaris/utils/misc.py: -------------------------------------------------------------------------------- 1 | from typing import Any 2 | 3 | import numpy as np 4 | 5 | from polaris.utils.types import ( 6 | ListOrArrayType, 7 | SlugCompatibleStringType, 8 | SlugStringType, 9 | ArtifactUrn, 10 | HubOwner, 11 | ) 12 | 13 | 14 | def listit(t: Any): 15 | """ 16 | Converts all tuples in a possibly nested object to lists 17 | https://stackoverflow.com/questions/1014352/how-do-i-convert-a-nested-tuple-of-tuples-and-lists-to-lists-of-lists-in-python 18 | """ 19 | return list(map(listit, t)) if isinstance(t, (list, tuple)) else t 20 | 21 | 22 | def slugify(sluggable: SlugCompatibleStringType) -> SlugStringType: 23 | """ 24 | Converts a slug-compatible string to a slug. 25 | """ 26 | return sluggable.lower().replace("_", "-").strip("-") 27 | 28 | 29 | def convert_lists_to_arrays(predictions: ListOrArrayType | dict) -> np.ndarray | dict: 30 | """ 31 | Recursively converts all plain Python lists in the predictions object to numpy arrays 32 | """ 33 | 34 | def convert_to_array(v): 35 | if isinstance(v, np.ndarray): 36 | return v 37 | elif isinstance(v, list): 38 | return np.array(v) 39 | elif isinstance(v, dict): 40 | return {k: convert_to_array(v) for k, v in v.items()} 41 | 42 | return convert_to_array(predictions) 43 | 44 | 45 | def build_urn(artifact_type: str, owner: str | HubOwner, slug: str) -> ArtifactUrn: 46 | return f"urn:polaris:{artifact_type}:{owner}:{slug}" 47 | -------------------------------------------------------------------------------- /polaris/cli.py: -------------------------------------------------------------------------------- 1 | from typing import Annotated 2 | 3 | import typer 4 | 5 | from polaris.hub.client import PolarisHubClient 6 | from polaris.hub.settings import PolarisHubSettings 7 | 8 | app = typer.Typer( 9 | add_completion=False, 10 | help="Polaris is a framework for benchmarking methods in drug discovery.", 11 | ) 12 | 13 | 14 | @app.command("login") 15 | def login( 16 | client_env_file: Annotated[ 17 | str, typer.Option(help="Environment file to overwrite the default environment variables") 18 | ] = ".env", 19 | auto_open_browser: Annotated[ 20 | bool, typer.Option(help="Whether to automatically open the link in a browser to retrieve the token") 21 | ] = True, 22 | overwrite: Annotated[ 23 | bool, typer.Option(help="Whether to overwrite the access token if you are already logged in") 24 | ] = False, 25 | ): 26 | """Authenticate to the Polaris Hub. 27 | 28 | This CLI will use the OAuth2 protocol to gain token-based access to the Polaris Hub API. 29 | """ 30 | client = PolarisHubClient(settings=PolarisHubSettings(_env_file=client_env_file)) 31 | client.login(auto_open_browser=auto_open_browser, overwrite=overwrite) 32 | 33 | 34 | @app.command(hidden=True) 35 | def secret(): 36 | # NOTE (cwognum): Empty, hidden command to force Typer to not collapse the subcommand. 37 | # Added because I anticipate we will want to add more subcommands later on. This will keep 38 | # the API consistent in the meantime. Once there are other subcommands, it can be removed. 39 | # See also: https://github.com/tiangolo/typer/issues/315 40 | raise NotImplementedError() 41 | 42 | 43 | if __name__ == "__main__": 44 | app() 45 | -------------------------------------------------------------------------------- /polaris/utils/zarr/_utils.py: -------------------------------------------------------------------------------- 1 | import zarr 2 | import zarr.storage 3 | 4 | from polaris.utils.errors import InvalidZarrCodec 5 | 6 | try: 7 | # Register imagecodecs if they are available. 8 | from imagecodecs.numcodecs import register_codecs 9 | 10 | register_codecs() 11 | except ImportError: 12 | pass 13 | 14 | 15 | def load_zarr_group_to_memory(group: zarr.Group) -> dict: 16 | """Loads an entire Zarr group into memory.""" 17 | 18 | if isinstance(group, dict): 19 | # If a Zarr group is already loaded to memory (e.g. with dataset.load_to_memory()), 20 | # the adapter would receive a dictionary instead of a Zarr group. 21 | return group 22 | 23 | data = {} 24 | for key, item in group.items(): 25 | if isinstance(item, zarr.Array): 26 | data[key] = item[:] 27 | elif isinstance(item, zarr.Group): 28 | data[key] = load_zarr_group_to_memory(item) 29 | return data 30 | 31 | 32 | def check_zarr_codecs(group: zarr.Group): 33 | """Check if all codecs in the Zarr group are registered.""" 34 | try: 35 | for key, item in group.items(): 36 | if isinstance(item, zarr.Group): 37 | check_zarr_codecs(item) 38 | 39 | except ValueError as error: 40 | # Zarr raises a generic ValueError if a codec is not registered. 41 | # See also: https://github.com/zarr-developers/zarr-python/issues/2508 42 | prefix = "codec not available: " 43 | error_message = str(error) 44 | 45 | if not error_message.startswith(prefix): 46 | raise error 47 | 48 | # Remove prefix and apostrophes 49 | codec_id = error_message.removeprefix(prefix).strip("'") 50 | raise InvalidZarrCodec(codec_id) 51 | -------------------------------------------------------------------------------- /polaris/utils/context.py: -------------------------------------------------------------------------------- 1 | from contextlib import contextmanager 2 | from contextvars import ContextVar 3 | from itertools import cycle 4 | 5 | from rich.progress import ( 6 | BarColumn, 7 | MofNCompleteColumn, 8 | Progress, 9 | SpinnerColumn, 10 | TextColumn, 11 | TimeElapsedColumn, 12 | ) 13 | 14 | # Singleton Progress instance to be used for all calls to `track_progress` 15 | progress_instance = ContextVar( 16 | "progress", 17 | default=Progress( 18 | SpinnerColumn(), 19 | TextColumn("[progress.description]{task.description}"), 20 | BarColumn(), 21 | MofNCompleteColumn(), 22 | TimeElapsedColumn(), 23 | ), 24 | ) 25 | 26 | colors = cycle( 27 | { 28 | "green", 29 | "cyan", 30 | "magenta", 31 | } 32 | ) 33 | 34 | 35 | @contextmanager 36 | def track_progress(description: str, total: float | None = 1.0): 37 | """ 38 | Use the Progress instance to track a task's progress 39 | """ 40 | progress = progress_instance.get() 41 | 42 | # Make sure the Progress is started 43 | progress.start() 44 | 45 | task = progress.add_task(f"[{next(colors)}]{description}", total=total) 46 | 47 | try: 48 | # Yield the task and Progress instance, for more granular control 49 | yield progress, task 50 | 51 | # Mark the task as completed 52 | progress.update(task, completed=total, refresh=True) 53 | progress.log(f"[green] Success: {description}") 54 | except Exception: 55 | progress.log(f"[red] Error: {description}") 56 | raise 57 | finally: 58 | # Remove the task from the UI, and stop the progress bar if all tasks are completed 59 | progress.remove_task(task) 60 | if progress.finished: 61 | progress.stop() 62 | -------------------------------------------------------------------------------- /tests/test_oauth.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | from polaris.hub.oauth import CachedTokenAuth 4 | 5 | 6 | def test_cached_token_auth_empty_on_no_cache(tmp_path): 7 | filename = "test_token.json" 8 | auth = CachedTokenAuth(cache_dir=tmp_path, filename=filename) 9 | assert not auth.token 10 | 11 | 12 | def test_cached_token_auth_reads_from_cache(tmp_path): 13 | filename = "test_token.json" 14 | cache_file = tmp_path / filename 15 | cache_file.write_text( 16 | json.dumps( 17 | { 18 | "access_token": "test_token", 19 | "issued_token_type": "urn:ietf:params:oauth:token-type:jwt", 20 | "token_type": "Bearer", 21 | "expires_in": 576618, 22 | "expires_at": 1720122005, 23 | } 24 | ) 25 | ) 26 | 27 | auth = CachedTokenAuth(cache_dir=tmp_path, filename=filename) 28 | 29 | assert auth.token is not None 30 | assert auth.token["access_token"] == "test_token" 31 | assert auth.token["expires_at"] == 1720122005 32 | assert auth.token["expires_in"] == 576618 33 | assert auth.token["token_type"] == "Bearer" 34 | assert auth.token["issued_token_type"] == "urn:ietf:params:oauth:token-type:jwt" 35 | 36 | 37 | def test_cached_token_auth_writes_to_cache(tmp_path): 38 | filename = "test_token.json" 39 | cache_file = tmp_path / filename 40 | 41 | auth = CachedTokenAuth(cache_dir=tmp_path, filename=filename) 42 | auth.set_token( 43 | { 44 | "access_token": "test_token", 45 | "issued_token_type": "urn:ietf:params:oauth:token-type:jwt", 46 | "token_type": "Bearer", 47 | "expires_in": 576618, 48 | "expires_at": 1720122005, 49 | } 50 | ) 51 | 52 | assert cache_file.exists() 53 | assert json.loads(cache_file.read_text()) == auth.token 54 | -------------------------------------------------------------------------------- /polaris/benchmark/_definitions.py: -------------------------------------------------------------------------------- 1 | from typing import Collection 2 | 3 | from pydantic import computed_field, field_validator 4 | 5 | from polaris.benchmark._base import BenchmarkV1Specification 6 | from polaris.utils.types import TaskType 7 | 8 | 9 | class SingleTaskMixin: 10 | """ 11 | Mixin for single-task benchmarks. 12 | """ 13 | 14 | @field_validator("target_cols", check_fields=False) 15 | @classmethod 16 | def validate_target_cols(cls, v: Collection[str]) -> Collection[str]: 17 | if len(v) != 1: 18 | raise ValueError("A single-task benchmark should specify exactly one target column.") 19 | return v 20 | 21 | @computed_field 22 | @property 23 | def task_type(self) -> str: 24 | """Return SINGLE_TASK for single-task benchmarks.""" 25 | return TaskType.SINGLE_TASK.value 26 | 27 | 28 | class MultiTaskMixin: 29 | """ 30 | Mixin for multi-task benchmarks. 31 | """ 32 | 33 | @field_validator("target_cols", check_fields=False) 34 | @classmethod 35 | def validate_target_cols(cls, v: Collection[str]) -> Collection[str]: 36 | if len(v) <= 1: 37 | raise ValueError("A multi-task benchmark should specify at least two target columns.") 38 | return v 39 | 40 | @computed_field 41 | @property 42 | def task_type(self) -> str: 43 | """ 44 | Return MULTI_TASK for multi-task benchmarks. 45 | """ 46 | return TaskType.MULTI_TASK.value 47 | 48 | 49 | class SingleTaskBenchmarkSpecification(SingleTaskMixin, BenchmarkV1Specification): 50 | """ 51 | Single-task benchmark for the base specification. 52 | """ 53 | 54 | pass 55 | 56 | 57 | class MultiTaskBenchmarkSpecification(MultiTaskMixin, BenchmarkV1Specification): 58 | """ 59 | Multitask benchmark for the base specification. 60 | """ 61 | 62 | pass 63 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug-report.yml: -------------------------------------------------------------------------------- 1 | name: 🐛 File a bug report 2 | description: X's behavior is deviating from its documented behavior. 3 | labels: ["bug"] 4 | body: 5 | - type: markdown 6 | attributes: 7 | value: | 8 | Please provide the following information. 9 | - type: input 10 | id: Polaris-version 11 | attributes: 12 | label: Polaris version 13 | description: Value of ``polaris.__version__`` 14 | placeholder: 0.2.5, 0.3.0, 0.3.1, etc. 15 | validations: 16 | required: true 17 | - type: input 18 | id: Python-version 19 | attributes: 20 | label: Python Version 21 | description: Version of Python interpreter 22 | placeholder: 3.9, 3.10, 3.11, etc. 23 | validations: 24 | required: true 25 | - type: input 26 | id: OS 27 | attributes: 28 | label: Operating System 29 | description: Operating System 30 | placeholder: (Linux/Windows/Mac) 31 | validations: 32 | required: true 33 | - type: input 34 | id: installation 35 | attributes: 36 | label: Installation 37 | description: How was Polaris installed? 38 | placeholder: e.g., "using pip into virtual environment", or "using conda" 39 | validations: 40 | required: true 41 | - type: textarea 42 | id: description 43 | attributes: 44 | label: Description 45 | description: Explain why the current behavior is a problem, what the expected output/behaviour is, and why the expected output/behaviour is a better solution. 46 | validations: 47 | required: true 48 | - type: textarea 49 | id: reproduce 50 | attributes: 51 | label: Steps to reproduce 52 | description: Minimal, reproducible code sample, a copy-pastable example if possible. 53 | validations: 54 | required: true 55 | - type: textarea 56 | id: additional-output 57 | attributes: 58 | label: Additional output 59 | description: If you think it might be relevant, please provide the output from ``pip freeze`` or ``conda env export`` depending on which was used to install Polaris. -------------------------------------------------------------------------------- /polaris/evaluate/metrics/generic_metrics.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from scipy import stats 3 | from sklearn.metrics import average_precision_score as sk_average_precision_score 4 | from sklearn.metrics import cohen_kappa_score as sk_cohen_kappa_score 5 | 6 | 7 | def pearsonr(y_true: np.ndarray, y_pred: np.ndarray): 8 | """Calculate a pearson r correlation""" 9 | return stats.pearsonr(y_true, y_pred).statistic 10 | 11 | 12 | def spearman(y_true: np.ndarray, y_pred: np.ndarray): 13 | """Calculate a Spearman correlation""" 14 | return stats.spearmanr(y_true, y_pred).statistic 15 | 16 | 17 | def absolute_average_fold_error(y_true: np.ndarray, y_pred: np.ndarray) -> float: 18 | """ 19 | Calculate the Absolute Average Fold Error (AAFE) metric. 20 | It measures the fold change between predicted values and observed values. 21 | The implementation is based on [this paper](https://pubs.acs.org/doi/10.1021/acs.chemrestox.3c00305). 22 | 23 | Args: 24 | y_true: The true target values of shape (n_samples,) 25 | y_pred: The predicted target values of shape (n_samples,). 26 | 27 | Returns: 28 | aafe: The Absolute Average Fold Error. 29 | """ 30 | if len(y_true) != len(y_pred): 31 | raise ValueError("Length of y_true and y_pred must be the same.") 32 | 33 | if np.any(y_true == 0): 34 | raise ValueError("`y_true` contains zero which will result `Inf` value.") 35 | 36 | aafe = np.mean(np.abs(y_pred) / np.abs(y_true)) 37 | 38 | return aafe 39 | 40 | 41 | def cohen_kappa_score(y_true, y_pred, **kwargs): 42 | """Scikit learn cohen_kappa_score wraper with renamed arguments""" 43 | return sk_cohen_kappa_score(y1=y_true, y2=y_pred, **kwargs) 44 | 45 | 46 | def average_precision_score(y_true, y_score, **kwargs): 47 | """Scikit learn average_precision_score wrapper that throws an error if y_true has no positive class""" 48 | if len(y_true) == 0 or not np.any(y_true): 49 | raise ValueError("Average precision requires at least a single positive class") 50 | return sk_average_precision_score(y_true=y_true, y_score=y_score, **kwargs) 51 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | # Introduction 2 | 3 | Welcome to the Polaris documentation! 4 | 5 | ## What is Polaris? 6 | 7 | !!! info "Our mission" 8 | 9 | Polaris is on a mission to bring innovators and practitioners closer together to develop methods that matter. 10 | 11 | Polaris is an optimistic community that fundamentally believes in the ability of Machine Learning to radically improve lives by disrupting the drug discovery process. However, we recognize that the absence of standardized, domain-appropriate datasets, guidelines, and tools for method evaluation is limiting its current impact. 12 | 13 | Polaris is a Python library designed to interact with the [Polaris Hub](https://www.polarishub.io). Our aim is to build the leading benchmarking platform for drug discovery, promoting the use of high-quality resources and domain-appropriate evaluation protocols. Learn more through our [blog posts](https://polarishub.io/blog). 14 | 15 | ## Where to next? 16 | 17 | --- 18 | 19 | **:fontawesome-solid-rocket: Quickstart** 20 | 21 | If you are entirely new to Polaris, this is the place to start! Learn about the essential concepts and partake in your first benchmark. 22 | 23 | [:material-arrow-right: Let's get started](./quickstart.md) 24 | 25 | 26 | --- 27 | 28 | **:fontawesome-solid-graduation-cap: Tutorials** 29 | 30 | Dive deeper into the Polaris code and learn about advanced concepts to create your own benchmarks and datasets. 31 | 32 | [:material-arrow-right: Let's get started](./tutorials/submit_to_benchmark.ipynb) 33 | 34 | --- 35 | 36 | **:fontawesome-solid-code: API Reference** 37 | 38 | This is where you will find the technical documentation of the code itself. Learn the intricate details of how the various methods and classes work. 39 | 40 | [:material-arrow-right: Let's get started](./api/dataset.md) 41 | 42 | --- 43 | 44 | **:fontawesome-solid-comments: Community** 45 | 46 | Whether you are a first-time contributor or open-source veteran, we welcome any contribution to Polaris. Learn more about our community initiatives. 47 | 48 | [:material-arrow-right: Let's get started](https://discord.gg/vBFd8p6H7u) 49 | 50 | --- 51 | 52 | -------------------------------------------------------------------------------- /polaris/evaluate/_metadata.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | 3 | from pydantic import Field, PrivateAttr, computed_field 4 | 5 | from polaris._artifact import BaseArtifactModel 6 | from polaris.utils.dict2html import dict2html 7 | from polaris.utils.types import HttpUrlString, HubUser 8 | from polaris.model import Model 9 | 10 | 11 | class ResultsMetadataV1(BaseArtifactModel): 12 | """V1 implementation of evaluation results without model field support 13 | 14 | Attributes: 15 | github_url: The URL to the code repository that was used to generate these results. 16 | paper_url: The URL to the paper describing the methodology used to generate these results. 17 | contributors: The users that are credited for these results. 18 | 19 | For additional metadata attributes, see the base classes. 20 | """ 21 | 22 | # Additional metadata 23 | github_url: HttpUrlString | None = Field(None, alias="code_url") 24 | paper_url: HttpUrlString | None = Field(None, alias="report_url") 25 | contributors: list[HubUser] = Field(default_factory=list) 26 | 27 | # Private attributes 28 | _created_at: datetime = PrivateAttr(default_factory=datetime.now) 29 | 30 | def _repr_html_(self) -> str: 31 | """For pretty-printing in Jupyter Notebooks""" 32 | return dict2html(self.model_dump()) 33 | 34 | def __repr__(self): 35 | return self.model_dump_json(indent=2) 36 | 37 | 38 | class ResultsMetadataV2(BaseArtifactModel): 39 | """V2 implementation of evaluation results with model field replacing URLs 40 | 41 | Attributes: 42 | model: The model that was used to generate these results. 43 | contributors: The users that are credited for these results. 44 | 45 | For additional metadata attributes, see the base classes. 46 | """ 47 | 48 | # Additional metadata 49 | model: Model | None = Field(None, exclude=True) 50 | contributors: list[HubUser] = Field(default_factory=list) 51 | 52 | # Private attributes 53 | _created_at: datetime = PrivateAttr(default_factory=datetime.now) 54 | 55 | @computed_field 56 | @property 57 | def model_artifact_id(self) -> str: 58 | return self.model.artifact_id if self.model else None 59 | 60 | def _repr_html_(self) -> str: 61 | return dict2html(self.model_dump()) 62 | 63 | def __repr__(self): 64 | return self.model_dump_json(indent=2) 65 | -------------------------------------------------------------------------------- /polaris/model/__init__.py: -------------------------------------------------------------------------------- 1 | from polaris._artifact import BaseArtifactModel 2 | from polaris.utils.types import HttpUrlString 3 | from polaris.utils.types import HubOwner 4 | from pydantic import Field 5 | 6 | 7 | class Model(BaseArtifactModel): 8 | """ 9 | Represents a Model artifact in the Polaris ecosystem. 10 | 11 | A Model artifact serves as a centralized representation of a method or model, encapsulating its metadata. 12 | It can be associated with multiple result artifacts but is immutable after upload, except for the README field. 13 | 14 | Examples: 15 | Basic API usage: 16 | ```python 17 | from polaris.model import Model 18 | 19 | # Create a new Model Card 20 | model = Model( 21 | name="MolGPS", 22 | description="Graph transformer foundation model for molecular modeling", 23 | code_url="https://github.com/datamol-io/graphium" 24 | ) 25 | 26 | # Upload the model card to the Hub 27 | model.upload_to_hub(owner="recursion") 28 | ``` 29 | 30 | Attributes: 31 | readme (str): A detailed README describing the model. 32 | code_url (HttpUrlString | None): Optional URL pointing to the model's code repository. 33 | report_url (HttpUrlString | None): Optional URL linking to a report or publication related to the model. 34 | artifact_version: The version of the model. 35 | artifact_changelog: A description of the changes made in this model version. 36 | 37 | Methods: 38 | upload_to_hub(owner: HubOwner | str | None = None): 39 | Uploads the model artifact to the Polaris Hub, associating it with a specified owner. 40 | 41 | For additional metadata attributes, see the base class. 42 | """ 43 | 44 | _artifact_type = "model" 45 | 46 | readme: str = "" 47 | code_url: HttpUrlString | None = None 48 | report_url: HttpUrlString | None = None 49 | 50 | # Version-related fields 51 | artifact_version: int = Field(default=1, frozen=True) 52 | artifact_changelog: str | None = None 53 | 54 | def upload_to_hub( 55 | self, 56 | owner: HubOwner | str | None = None, 57 | parent_artifact_id: str | None = None, 58 | ): 59 | """ 60 | Uploads the model to the Polaris Hub. 61 | """ 62 | from polaris.hub.client import PolarisHubClient 63 | 64 | with PolarisHubClient() as client: 65 | client.upload_model(self, owner=owner, parent_artifact_id=parent_artifact_id) 66 | -------------------------------------------------------------------------------- /polaris/utils/zarr/_manifest.py: -------------------------------------------------------------------------------- 1 | import os 2 | from hashlib import md5 3 | from pathlib import Path 4 | 5 | from pyarrow import Table, schema, string 6 | from pyarrow.parquet import write_table 7 | 8 | # PyArrow table schema for the V2 Zarr manifest file 9 | ZARR_MANIFEST_SCHEMA = schema([("path", string()), ("md5_checksum", string())]) 10 | 11 | ROW_GROUP_SIZE = 128 * 1024 * 1024 # 128 MB 12 | 13 | 14 | def generate_zarr_manifest(zarr_root_path: str, output_dir: str) -> str: 15 | """ 16 | Entry point function which triggers the creation of a Zarr manifest for a V2 dataset. 17 | 18 | Parameters: 19 | zarr_root_path: The path to the root of a Zarr archive 20 | output_dir: The path to the directory which will hold the generated manifest 21 | """ 22 | zarr_manifest_path = f"{output_dir}/zarr_manifest.parquet" 23 | 24 | entries = manifest_entries(zarr_root_path, zarr_root_path) 25 | manifest = Table.from_pylist(mapping=entries, schema=ZARR_MANIFEST_SCHEMA) 26 | write_table(manifest, zarr_manifest_path, row_group_size=ROW_GROUP_SIZE) 27 | 28 | return zarr_manifest_path 29 | 30 | 31 | def manifest_entries(dir_path: str, root_path: str) -> list[dict[str, str]]: 32 | """ 33 | Recursive function that traverses a directory, returning entries consisting of every file's path and MD5 hash 34 | 35 | Parameters: 36 | dir_path: The path to the current directory being traversed 37 | root_path: The root path from which to compute a relative path 38 | """ 39 | entries = [] 40 | with os.scandir(dir_path) as it: 41 | for entry in it: 42 | if entry.is_file(): 43 | entries.append( 44 | { 45 | "path": str(Path(entry.path).relative_to(root_path)), 46 | "md5_checksum": calculate_file_md5(entry.path), 47 | } 48 | ) 49 | elif entry.is_dir(): 50 | entries.extend(manifest_entries(entry.path, root_path)) 51 | 52 | return entries 53 | 54 | 55 | def calculate_file_md5(file_path: str) -> str: 56 | """Calculates the md5 hash for a file at a given path""" 57 | 58 | md5_hash = md5() 59 | with open(file_path, "rb") as file: 60 | # 61 | # Read the file in chunks to avoid using too much memory for large files 62 | for chunk in iter(lambda: file.read(4096), b""): 63 | md5_hash.update(chunk) 64 | 65 | # Return the hex representation of the digest 66 | return md5_hash.hexdigest() 67 | -------------------------------------------------------------------------------- /polaris/evaluate/metrics/docking_metrics.py: -------------------------------------------------------------------------------- 1 | # This script includes docking related evaluation metrics. 2 | 3 | 4 | import numpy as np 5 | from rdkit.Chem.rdMolAlign import CalcRMS 6 | 7 | import datamol as dm 8 | 9 | 10 | def _rmsd(mol_probe: dm.Mol, mol_ref: dm.Mol) -> float: 11 | """Calculate RMSD between predicted molecule and closest ground truth molecule. 12 | The RMSD is calculated with first conformer of predicted molecule and only consider heavy atoms for RMSD calculation. 13 | It is assumed that the predicted binding conformers are extracted from the docking output, where the receptor (protein) coordinates have been aligned with the original crystal structure. 14 | 15 | Args: 16 | mol_probe: Predicted molecule (docked ligand) with exactly one conformer. 17 | mol_ref: Ground truth molecule (crystal ligand) with at least one conformer. If multiple conformers are 18 | present, the lowest RMSD will be reported. 19 | 20 | Returns: 21 | Returns the RMS between two molecules, taking symmetry into account. 22 | """ 23 | 24 | # copy the molecule for modification. 25 | mol_probe = dm.copy_mol(mol_probe) 26 | mol_ref = dm.copy_mol(mol_ref) 27 | 28 | # remove hydrogen from molecule 29 | mol_probe = dm.remove_hs(mol_probe) 30 | mol_ref = dm.remove_hs(mol_ref) 31 | 32 | # calculate RMSD 33 | return CalcRMS( 34 | prbMol=mol_probe, refMol=mol_ref, symmetrizeConjugatedTerminalGroups=True, prbId=-1, refId=-1 35 | ) 36 | 37 | 38 | def rmsd_coverage(y_pred: str | list[dm.Mol], y_true: str | list[dm.Mol], max_rsmd: float = 2): 39 | """ 40 | Calculate the coverage of molecules with an RMSD less than a threshold (2 Å by default) compared to the reference molecule conformer. 41 | 42 | It is assumed that the predicted binding conformers are extracted from the docking output, where the receptor (protein) coordinates have been aligned with the original crystal structure. 43 | 44 | Attributes: 45 | y_pred: List of predicted binding conformers. 46 | y_true: List of ground truth binding confoermers. 47 | max_rsmd: The threshold for determining acceptable rsmd. 48 | """ 49 | 50 | if len(y_pred) != len(y_true): 51 | assert ValueError( 52 | f"The list of probing molecules and the list of reference molecules are different sizes. {len(y_pred)} != {len(y_true)} " 53 | ) 54 | 55 | rmsds = np.array( 56 | [_rmsd(mol_probe=mol_probe, mol_ref=mol_ref) for mol_probe, mol_ref in zip(y_pred, y_true)] 57 | ) 58 | 59 | return np.sum(rmsds <= max_rsmd) / len(rmsds) 60 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | tags: [ "*" ] 7 | pull_request: 8 | branches: 9 | - "*" 10 | - "!gh-pages" 11 | schedule: 12 | - cron: "0 4 * * MON" 13 | 14 | concurrency: 15 | group: "test-${{ github.ref }}" 16 | cancel-in-progress: true 17 | 18 | jobs: 19 | test-uv: 20 | strategy: 21 | fail-fast: false 22 | matrix: 23 | python-version: [ "3.10", "3.11", "3.12" ] 24 | os: [ "ubuntu-latest", "macos-latest", "windows-latest" ] 25 | 26 | runs-on: ${{ matrix.os }} 27 | timeout-minutes: 30 28 | 29 | defaults: 30 | run: 31 | shell: bash -l {0} 32 | 33 | name: PyPi os=${{ matrix.os }} - python=${{ matrix.python-version }} 34 | 35 | steps: 36 | - name: Checkout the code 37 | uses: actions/checkout@v4 38 | 39 | - name: Install uv 40 | uses: astral-sh/setup-uv@v5 41 | 42 | - name: Install the project 43 | run: uv sync --all-groups --python ${{ matrix.python-version }} 44 | 45 | - name: Run tests 46 | run: uv run pytest 47 | env: 48 | POLARIS_USERNAME: ${{ secrets.POLARIS_USERNAME }} 49 | POLARIS_PASSWORD: ${{ secrets.POLARIS_PASSWORD }} 50 | 51 | - name: Test CLI 52 | run: uv run polaris --help 53 | 54 | - name: Test building the doc 55 | run: uv run mkdocs build 56 | 57 | test-conda: 58 | strategy: 59 | fail-fast: false 60 | matrix: 61 | python-version: [ "3.10", "3.11", "3.12" ] 62 | os: [ "ubuntu-latest", "macos-latest", "windows-latest" ] 63 | 64 | runs-on: ${{ matrix.os }} 65 | timeout-minutes: 30 66 | 67 | defaults: 68 | run: 69 | shell: bash -l {0} 70 | 71 | name: Conda os=${{ matrix.os }} - python=${{ matrix.python-version }} 72 | 73 | steps: 74 | - name: Checkout the code 75 | uses: actions/checkout@v4 76 | 77 | - name: Setup mamba 78 | uses: mamba-org/setup-micromamba@v2 79 | with: 80 | environment-file: env.yml 81 | environment-name: polaris_testing_env 82 | cache-environment: true 83 | cache-downloads: true 84 | create-args: >- 85 | python=${{ matrix.python-version }} 86 | 87 | - name: Install library 88 | run: python -m pip install --no-deps . 89 | 90 | - name: Run pytest 91 | run: pytest 92 | env: 93 | POLARIS_USERNAME: ${{ secrets.POLARIS_USERNAME }} 94 | POLARIS_PASSWORD: ${{ secrets.POLARIS_PASSWORD }} 95 | 96 | - name: Test CLI 97 | run: polaris --help 98 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | 131 | # PyCharm files 132 | .idea/ 133 | # Rever 134 | rever/ 135 | 136 | # VS Code 137 | .vscode/ 138 | 139 | # Generated legacy requirements.txt 140 | requirements.txt 141 | 142 | # OS-specific files 143 | .DS_store 144 | -------------------------------------------------------------------------------- /polaris/dataset/converters/_zarr.py: -------------------------------------------------------------------------------- 1 | import os 2 | from collections import defaultdict 3 | from typing import TYPE_CHECKING 4 | 5 | import pandas as pd 6 | import zarr 7 | from typing_extensions import deprecated 8 | 9 | from polaris.dataset import ColumnAnnotation 10 | from polaris.dataset.converters._base import Converter, FactoryProduct 11 | 12 | if TYPE_CHECKING: 13 | from polaris.dataset import DatasetFactory 14 | 15 | 16 | @deprecated("Please use the custom codecs in `polaris.utils.zarr.codecs` instead.") 17 | class ZarrConverter(Converter): 18 | """Parse a [.zarr](https://zarr.readthedocs.io/en/stable/index.html) archive into a Polaris `Dataset`. 19 | 20 | Warning: Loading from `.zarr` 21 | Loading and saving datasets from and to `.zarr` is still experimental and currently not 22 | fully supported by the Hub. 23 | 24 | A `.zarr` file can contain groups and arrays, where each group can again contain groups and arrays. 25 | Within Polaris, the Zarr archive is expected to have a flat hierarchy where each array corresponds 26 | to a single column and each array contains the values for all datapoints in that column. 27 | """ 28 | 29 | def convert(self, path: str, factory: "DatasetFactory", append: bool = False) -> FactoryProduct: 30 | src = zarr.open(path, "r") 31 | 32 | v = next(src.group_keys(), None) 33 | if v is not None: 34 | raise ValueError("The root of the zarr hierarchy should only contain arrays.") 35 | 36 | # Copy to the source zarr, so everything is in one place 37 | pointer_start_dict = {col: 0 for col, _ in src.arrays()} 38 | if append: 39 | if not os.path.exists(factory.zarr_root.store.path): 40 | raise RuntimeError( 41 | f"Zarr store {factory.zarr_root.store.path} doesn't exist. \ 42 | Please make sure the zarr store {factory.zarr_root.store.path} is created. Or set `append` to `False`." 43 | ) 44 | else: 45 | for col, arr in src.arrays(): 46 | pointer_start_dict[col] += factory.zarr_root[col].shape[0] 47 | factory.zarr_root[col].append(arr) 48 | else: 49 | zarr.copy_store(source=src.store, dest=factory.zarr_root.store, if_exists="skip") 50 | 51 | # Construct the table 52 | # Parse any group into a column 53 | data = defaultdict(dict) 54 | for col, arr in src.arrays(): 55 | for i in range(len(arr)): 56 | data[col][i] = self.get_pointer(arr.name.removeprefix("/"), i) 57 | 58 | # Construct the dataset 59 | table = pd.DataFrame(data) 60 | return table, {k: ColumnAnnotation(is_pointer=True) for k in table.columns}, {} 61 | -------------------------------------------------------------------------------- /tests/test_type_checks.py: -------------------------------------------------------------------------------- 1 | import warnings 2 | 3 | import pytest 4 | from pydantic import BaseModel, ValidationError 5 | 6 | import polaris as po 7 | from polaris._artifact import BaseArtifactModel 8 | from polaris.utils.types import HttpUrlString, HubOwner 9 | 10 | 11 | def test_slug_string_type(): 12 | """ 13 | Verifies that the slug is validated correctly. 14 | Fails if: 15 | - Is too short (<4 characters) 16 | - Is too long (>64 characters) 17 | - Contains something other than lowercase letters, numbers, and hyphens. 18 | """ 19 | for name in [ 20 | "", 21 | "x", 22 | "xx", 23 | "xxx", 24 | "x" * 65, 25 | "invalid@", 26 | "invalid!", 27 | "InvalidName1", 28 | "invalid_name", 29 | ]: 30 | with pytest.raises(ValidationError): 31 | HubOwner(slug=name) 32 | 33 | for name in ["valid", "valid-name-1", "x" * 64, "x" * 4]: 34 | HubOwner(slug=name) 35 | 36 | 37 | def test_slug_compatible_string_type(): 38 | """Verifies that the artifact name is validated correctly.""" 39 | 40 | # Fails if: 41 | # - Is too short (<4 characters) 42 | # - Is too long (>64 characters) 43 | # - Contains non-alphanumeric characters 44 | for name in ["", "x", "xx", "xxx", "x" * 65, "invalid@", "invalid!"]: 45 | with pytest.raises(ValidationError): 46 | BaseArtifactModel(name=name) 47 | 48 | # Does not fail 49 | for name in [ 50 | "valid", 51 | "valid-name", 52 | "valid_name", 53 | "ValidName1", 54 | "Valid_", 55 | "Valid-", 56 | "x" * 64, 57 | "x" * 4, 58 | ]: 59 | BaseArtifactModel(name=name) 60 | 61 | 62 | def test_version(): 63 | with pytest.raises(ValidationError): 64 | BaseArtifactModel(polaris_version="invalid") 65 | assert BaseArtifactModel().polaris_version == po.__version__ 66 | assert BaseArtifactModel(polaris_version="0.1.2") 67 | 68 | 69 | def test_http_url_string(): 70 | """Verifies that a string validated correctly as a URL.""" 71 | 72 | class _TestModel(BaseModel): 73 | url: HttpUrlString 74 | 75 | m = _TestModel(url="https://example.com") 76 | assert isinstance(m.url, str) 77 | 78 | m = _TestModel(url="http://example.com") 79 | assert isinstance(m.url, str) 80 | 81 | m = _TestModel(url="http://example.io") 82 | assert isinstance(m.url, str) 83 | 84 | with warnings.catch_warnings(): 85 | # Crash if any warnings are raised 86 | warnings.simplefilter("error") 87 | m.model_dump() 88 | 89 | with pytest.raises(ValidationError): 90 | _TestModel(url="invalid") 91 | with pytest.raises(ValidationError): 92 | _TestModel(url="ftp://invalid.com") 93 | -------------------------------------------------------------------------------- /polaris/mixins/_checksum.py: -------------------------------------------------------------------------------- 1 | import abc 2 | import logging 3 | import re 4 | 5 | from pydantic import BaseModel, PrivateAttr, computed_field 6 | 7 | from polaris.utils.errors import PolarisChecksumError 8 | 9 | logger = logging.getLogger(__name__) 10 | 11 | 12 | class ChecksumMixin(BaseModel, abc.ABC): 13 | """ 14 | Mixin class to add checksum functionality to a class. 15 | """ 16 | 17 | _md5sum: str | None = PrivateAttr(None) 18 | 19 | @abc.abstractmethod 20 | def _compute_checksum(self) -> str: 21 | """Compute the checksum of the dataset.""" 22 | raise NotImplementedError 23 | 24 | @computed_field 25 | @property 26 | def md5sum(self) -> str: 27 | """Lazily compute the checksum once needed.""" 28 | if not self.has_md5sum: 29 | logger.info("Computing the checksum. This can be slow for large datasets.") 30 | self.md5sum = self._compute_checksum() 31 | return self._md5sum 32 | 33 | @md5sum.setter 34 | def md5sum(self, value: str): 35 | """Set the checksum.""" 36 | if not re.fullmatch(r"^[a-f0-9]{32}$", value): 37 | raise ValueError("The checksum should be the 32-character hexdigest of a 128 bit MD5 hash.") 38 | self._md5sum = value 39 | 40 | @property 41 | def has_md5sum(self) -> bool: 42 | """Whether the md5sum for this class has been computed and stored.""" 43 | return self._md5sum is not None 44 | 45 | def verify_checksum(self, md5sum: str | None = None): 46 | """ 47 | Recomputes the checksum and verifies whether it matches the stored checksum. 48 | 49 | Warning: Slow operation 50 | This operation can be slow for large datasets. 51 | 52 | Info: Only works for locally stored datasets 53 | The checksum verification only works for datasets that are stored locally in its entirety. 54 | We don't have to verify the checksum for datasets stored on the Hub, as the Hub will do this on upload. 55 | And if you're streaming the data from the Hub, we will check the checksum of each chunk on download. 56 | """ 57 | if md5sum is None: 58 | md5sum = self._md5sum 59 | if md5sum is None: 60 | logger.warning( 61 | "No checksum to verify against. Specify either the md5sum parameter or " 62 | "store the checksum in the dataset.md5sum attribute." 63 | ) 64 | return 65 | 66 | # Recompute the checksum 67 | logger.info("To verify the checksum, we need to recompute it. This can be slow for large datasets.") 68 | self.md5sum = self._compute_checksum() 69 | 70 | if self.md5sum != md5sum: 71 | raise PolarisChecksumError( 72 | f"The specified checksum {md5sum} does not match the computed checksum {self.md5sum}" 73 | ) 74 | -------------------------------------------------------------------------------- /tests/test_metrics.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import pandas as pd 3 | import pytest 4 | 5 | from polaris.benchmark import BenchmarkV1Specification 6 | from polaris.dataset import Dataset 7 | from polaris.evaluate._metric import Metric 8 | 9 | 10 | def test_absolute_average_fold_error(): 11 | y_true = np.random.uniform(low=50, high=100, size=200) 12 | y_pred_1 = y_true + np.random.uniform(low=0, high=5, size=200) 13 | y_pred_2 = y_true + np.random.uniform(low=5, high=20, size=200) 14 | y_pred_3 = y_true - 10 15 | y_zero = np.zeros(shape=200) 16 | 17 | metric = Metric(label="absolute_average_fold_error") 18 | # Optimal value 19 | aafe_0 = metric.fn(y_true=y_true, y_pred=y_true) 20 | assert aafe_0 == 1 21 | 22 | # small fold change 23 | aafe_1 = metric.fn(y_true=y_true, y_pred=y_pred_1) 24 | assert aafe_1 > 1 25 | 26 | # larger fold change 27 | aafe_2 = metric.fn(y_true=y_true, y_pred=y_pred_2) 28 | assert aafe_2 > aafe_1 29 | 30 | # undershoot 31 | aafe_3 = metric.fn(y_true=y_true, y_pred=y_pred_3) 32 | assert aafe_3 < 1 33 | 34 | # y_true contains zeros 35 | with pytest.raises(ValueError): 36 | metric.fn(y_true=y_zero, y_pred=y_pred_3) 37 | 38 | 39 | def test_grouped_metric(): 40 | metric = Metric(label="accuracy", config={"group_by": "group"}) 41 | 42 | table = pd.DataFrame({"group": ["a", "b", "b"], "y_true": [1, 1, 1]}) 43 | dataset = Dataset(table=table) 44 | benchmark = BenchmarkV1Specification( 45 | dataset=dataset, 46 | metrics=[metric], 47 | main_metric=metric, 48 | target_cols=["y_true"], 49 | input_cols=["group"], 50 | split=([], [0, 1, 2]), 51 | ) 52 | 53 | result = benchmark.evaluate([1, 0, 0]) 54 | 55 | # The global accuracy is only 33%, but because we compute it per group and then average, it's 50%. 56 | assert result.results.Score.values[0] == 0.5 57 | 58 | 59 | def test_metric_hash(): 60 | metric_1 = Metric(label="accuracy") 61 | metric_2 = Metric(label="accuracy") 62 | metric_3 = Metric(label="mean_absolute_error") 63 | assert hash(metric_1) == hash(metric_2) 64 | assert hash(metric_1) != hash(metric_3) 65 | 66 | metric_4 = Metric(label="accuracy", config={"group_by": "group1"}) 67 | assert hash(metric_4) != hash(metric_1) 68 | 69 | metric_5 = Metric(label="accuracy", config={"group_by": "group2"}) 70 | assert hash(metric_4) != hash(metric_5) 71 | 72 | metric_6 = Metric(label="accuracy", config={"group_by": "group1"}) 73 | assert hash(metric_4) == hash(metric_6) 74 | 75 | 76 | def test_metric_name(): 77 | metric = Metric(label="accuracy") 78 | assert metric.name == "accuracy" 79 | 80 | metric = Metric(label="accuracy", config={"group_by": "group"}) 81 | assert metric.name == "accuracy_grouped" 82 | 83 | metric.custom_name = "custom_name" 84 | assert metric.name == "custom_name" 85 | -------------------------------------------------------------------------------- /polaris/dataset/_column.py: -------------------------------------------------------------------------------- 1 | import enum 2 | from typing import Literal, TypeAlias 3 | 4 | import numpy as np 5 | from numpy.typing import DTypeLike 6 | from pydantic import BaseModel, ConfigDict, Field, field_serializer, field_validator 7 | from pydantic.alias_generators import to_camel 8 | 9 | 10 | class Modality(enum.Enum): 11 | """Used to Annotate columns in a dataset.""" 12 | 13 | UNKNOWN = "unknown" 14 | MOLECULE = "molecule" 15 | MOLECULE_3D = "molecule_3D" 16 | PROTEIN = "protein" 17 | PROTEIN_3D = "protein_3D" 18 | IMAGE = "image" 19 | 20 | 21 | KnownContentType: TypeAlias = Literal["chemical/x-smiles", "chemical/x-pdb"] 22 | 23 | 24 | class ColumnAnnotation(BaseModel): 25 | """ 26 | The `ColumnAnnotation` class is used to annotate the columns of the object. 27 | This mostly just stores metadata and does not affect the logic. The exception is the `is_pointer` attribute. 28 | 29 | Attributes: 30 | is_pointer: Annotates whether a column is a pointer column. If so, it does not contain data, 31 | but rather contains references to blobs of data from which the data is loaded. 32 | modality: The data modality describes the data type and is used to categorize datasets on the Hub 33 | and while it does not affect logic in this library, it does affect the logic of the Hub. 34 | description: Describes how the data was generated. 35 | user_attributes: Any additional metadata can be stored in the user attributes. 36 | content_type: Specify column's IANA content type. If the the content type matches with a known type for 37 | molecules (e.g. "chemical/x-smiles"), visualization for its content will be activated on the Hub side 38 | """ 39 | 40 | is_pointer: bool = Field(False, deprecated=True) 41 | modality: Modality = Modality.UNKNOWN 42 | description: str | None = None 43 | user_attributes: dict[str, str] = Field(default_factory=dict) 44 | dtype: np.dtype | None = None 45 | content_type: KnownContentType | str | None = None 46 | 47 | model_config = ConfigDict(arbitrary_types_allowed=True, alias_generator=to_camel, populate_by_name=True) 48 | 49 | @field_validator("modality", mode="before") 50 | def _validate_modality(cls, v): 51 | """Tries to convert a string to the Enum""" 52 | if isinstance(v, str): 53 | v = Modality[v.upper()] 54 | return v 55 | 56 | @field_validator("dtype", mode="before") 57 | def _validate_dtype(cls, v): 58 | """Tries to convert a string to the Enum""" 59 | if isinstance(v, str): 60 | v = np.dtype(v) 61 | return v 62 | 63 | @field_serializer("modality") 64 | def _serialize_modality(self, v: Modality): 65 | """Return the modality as a string, keeping it serializable""" 66 | return v.name 67 | 68 | @field_serializer("dtype") 69 | def _serialize_dtype(self, v: DTypeLike | None): 70 | """Return the dtype as a string, keeping it serializable""" 71 | if v is not None: 72 | v = v.name 73 | return v 74 | -------------------------------------------------------------------------------- /polaris/benchmark/_task.py: -------------------------------------------------------------------------------- 1 | from typing import Sequence 2 | 3 | from pydantic import ( 4 | BaseModel, 5 | Field, 6 | ValidationInfo, 7 | computed_field, 8 | field_serializer, 9 | field_validator, 10 | model_validator, 11 | ) 12 | from typing_extensions import Self 13 | 14 | from polaris.utils.errors import InvalidBenchmarkError 15 | from polaris.utils.types import ColumnName, TargetType, TaskType 16 | 17 | 18 | class PredictiveTaskSpecificationMixin(BaseModel): 19 | """A mixin for task benchmarks without metrics. 20 | 21 | Attributes: 22 | target_cols: The column(s) of the original dataset that should be used as the target. 23 | input_cols: The column(s) of the original dataset that should be used as input. 24 | target_types: A dictionary that maps target columns to their type. If not specified, this is automatically inferred. 25 | """ 26 | 27 | target_cols: set[ColumnName] = Field(min_length=1) 28 | input_cols: set[ColumnName] = Field(min_length=1) 29 | target_types: dict[ColumnName, TargetType] = Field(default_factory=dict, validate_default=True) 30 | 31 | @field_validator("target_cols", "input_cols", mode="before") 32 | @classmethod 33 | def _parse_cols(cls, v: str | Sequence[str], info: ValidationInfo) -> set[str]: 34 | """ 35 | Normalize columns input values to a set. 36 | """ 37 | if isinstance(v, str): 38 | v = {v} 39 | else: 40 | v = set(v) 41 | return v 42 | 43 | @field_validator("target_types", mode="before") 44 | @classmethod 45 | def _parse_target_types( 46 | cls, v: dict[ColumnName, TargetType | str | None] 47 | ) -> dict[ColumnName, TargetType]: 48 | """ 49 | Converts the target types to TargetType enums if they are strings. 50 | """ 51 | return { 52 | target: TargetType(val) if isinstance(val, str) else val 53 | for target, val in v.items() 54 | if val is not None 55 | } 56 | 57 | @model_validator(mode="after") 58 | def _validate_target_types(self) -> Self: 59 | """ 60 | Verifies that all target types are for benchmark targets. 61 | """ 62 | columns = set(self.target_types.keys()) 63 | if not columns.issubset(self.target_cols): 64 | raise InvalidBenchmarkError( 65 | f"Not all specified target types were found in the target columns. {columns} - {self.target_cols}" 66 | ) 67 | return self 68 | 69 | @field_serializer("target_types") 70 | def _serialize_target_types(self, target_types): 71 | """ 72 | Convert from enum to string to make sure it's serializable 73 | """ 74 | return {k: v.value for k, v in target_types.items()} 75 | 76 | @field_serializer("target_cols", "input_cols") 77 | def _serialize_columns(self, v: set[str]) -> list[str]: 78 | return list(v) 79 | 80 | @computed_field 81 | @property 82 | def task_type(self) -> str: 83 | """The high-level task type of the benchmark.""" 84 | v = TaskType.MULTI_TASK if len(self.target_cols) > 1 else TaskType.SINGLE_TASK 85 | return v.value 86 | -------------------------------------------------------------------------------- /tests/test_subset.py: -------------------------------------------------------------------------------- 1 | import datamol as dm 2 | import numpy as np 3 | import pandas as pd 4 | import pytest 5 | 6 | from polaris.dataset import Subset 7 | from polaris.utils.errors import TestAccessError 8 | 9 | 10 | def test_consistency_across_access_methods(test_dataset): 11 | """Using the various endpoints of the Subset API should not lead to the same data.""" 12 | indices = list(range(5)) 13 | task = Subset(test_dataset, indices, "smiles", "expt") 14 | 15 | # Ground truth 16 | expected_smiles = test_dataset.table.loc[indices, "smiles"] 17 | expected_targets = test_dataset.table.loc[indices, "expt"] 18 | 19 | # Indexing 20 | assert ([task[i][0] for i in range(5)] == expected_smiles).all() 21 | assert ([task[i][1] for i in range(5)] == expected_targets).all() 22 | 23 | # Iterator 24 | assert (list(smi for smi, y in task) == expected_smiles).all() 25 | assert (list(y for smi, y in task) == expected_targets).all() 26 | 27 | # Property 28 | assert (task.inputs == expected_smiles).all() 29 | assert (task.targets == expected_targets).all() 30 | assert (task.X == expected_smiles).all() 31 | assert (task.y == expected_targets).all() 32 | 33 | 34 | def test_access_to_test_set(test_single_task_benchmark): 35 | """A user should not have access to the test set targets.""" 36 | 37 | train, test = test_single_task_benchmark.get_train_test_split() 38 | assert test._hide_targets 39 | assert not train._hide_targets 40 | 41 | with pytest.raises(TestAccessError): 42 | test.as_array("y") 43 | with pytest.raises(TestAccessError): 44 | test.targets 45 | 46 | # Check if iterable style access returns just the SMILES 47 | for x in test: 48 | assert isinstance(x, str) 49 | for i in range(len(test)): 50 | assert isinstance(test[i], str) 51 | 52 | # For the train set it should work 53 | assert all(isinstance(y, float) for x, y in train) 54 | assert all(isinstance(train[i][1], float) for i in range(len(train))) 55 | 56 | # as_dataframe should work for both, but contain no targets for test 57 | train_df = train.as_dataframe() 58 | assert isinstance(train_df, pd.DataFrame) 59 | assert "expt" in train_df.columns 60 | test_df = test.as_dataframe() 61 | assert isinstance(test_df, pd.DataFrame) 62 | assert "expt" not in test_df.columns 63 | 64 | 65 | def test_input_featurization(test_single_task_benchmark): 66 | # Without a transformation, we expect a SMILES string 67 | train, test = test_single_task_benchmark.get_train_test_split() 68 | test_single_task_benchmark._n_splits_since_evaluate = 0 # Manually reset for sake of test 69 | 70 | x, y = train[0] 71 | assert isinstance(x, str) 72 | 73 | x = test[0] 74 | assert isinstance(x, str) 75 | 76 | train, test = test_single_task_benchmark.get_train_test_split(featurization_fn=dm.to_fp) 77 | 78 | # For all different flavours of accessing the data 79 | # Make sure the input is now featurized 80 | x, y = train[0] 81 | assert isinstance(x, np.ndarray) 82 | 83 | x = test[0] 84 | assert isinstance(x, np.ndarray) 85 | 86 | x, y = next(train) 87 | assert isinstance(x, np.ndarray) 88 | 89 | x = next(test) 90 | assert isinstance(x, np.ndarray) 91 | 92 | x = train.X[0] 93 | assert isinstance(x, np.ndarray) 94 | 95 | x = test.X[0] 96 | assert isinstance(x, np.ndarray) 97 | -------------------------------------------------------------------------------- /polaris/evaluate/utils.py: -------------------------------------------------------------------------------- 1 | import pandas as pd 2 | 3 | from polaris.dataset._subset import Subset 4 | from polaris.evaluate import BenchmarkPredictions, BenchmarkResults, Metric 5 | from polaris.utils.types import IncomingPredictionsType 6 | 7 | 8 | def _optionally_subset( 9 | preds: BenchmarkPredictions | None, 10 | test_set_labels: list[str] | str, 11 | target_labels: list[str] | str, 12 | ) -> BenchmarkPredictions | None: 13 | """ 14 | Returns the value in a nested dictionary associated with a sequence of keys 15 | if it exists, otherwise return None 16 | """ 17 | if preds is None: 18 | return None 19 | 20 | if not isinstance(test_set_labels, list): 21 | test_set_labels = [test_set_labels] 22 | 23 | if not isinstance(target_labels, list): 24 | target_labels = [target_labels] 25 | 26 | return preds.get_subset( 27 | test_set_subset=test_set_labels, 28 | target_subset=target_labels, 29 | ) 30 | 31 | 32 | def evaluate_benchmark( 33 | target_cols: list[str], 34 | test_set_labels: list[str], 35 | test_set_sizes: dict[str, int], 36 | metrics: set[Metric], 37 | y_true: dict[str, Subset], 38 | y_pred: IncomingPredictionsType | None = None, 39 | y_prob: IncomingPredictionsType | None = None, 40 | ): 41 | """ 42 | Utility function that contains the evaluation logic for a benchmark 43 | """ 44 | 45 | # Normalize the and predictions to a consistent, internal representation. 46 | # Format is a two-level dictionary: {test_set_label: {target_label: np.ndarray}} 47 | if y_pred is not None: 48 | y_pred = BenchmarkPredictions( 49 | predictions=y_pred, 50 | target_labels=target_cols, 51 | test_set_labels=test_set_labels, 52 | test_set_sizes=test_set_sizes, 53 | ) 54 | if y_prob is not None: 55 | y_prob = BenchmarkPredictions( 56 | predictions=y_prob, 57 | target_labels=target_cols, 58 | test_set_labels=test_set_labels, 59 | test_set_sizes=test_set_sizes, 60 | ) 61 | 62 | # Compute the results 63 | # Results are saved in a tabular format. For more info, see the BenchmarkResults docs. 64 | scores = pd.DataFrame(columns=BenchmarkResults.RESULTS_COLUMNS) 65 | 66 | # For every test set... 67 | for test_label in test_set_labels: 68 | # For every metric... 69 | for metric in metrics: 70 | if metric.is_multitask: 71 | # Multi-task but with a metric across targets 72 | score = metric( 73 | y_true=y_true[test_label], 74 | y_pred=_optionally_subset(y_pred, test_set_labels=test_label), 75 | y_prob=_optionally_subset(y_prob, test_set_labels=test_label), 76 | ) 77 | 78 | scores.loc[len(scores)] = (test_label, "aggregated", metric, score) 79 | continue 80 | 81 | # Otherwise, for every target... 82 | for target_label in target_cols: 83 | score = metric( 84 | y_true=y_true[test_label].filter_targets(target_label), 85 | y_pred=_optionally_subset(y_pred, test_set_labels=test_label, target_labels=target_label), 86 | y_prob=_optionally_subset(y_prob, test_set_labels=test_label, target_labels=target_label), 87 | ) 88 | 89 | scores.loc[len(scores)] = (test_label, target_label, metric.name, score) 90 | 91 | return scores 92 | -------------------------------------------------------------------------------- /polaris/hub/settings.py: -------------------------------------------------------------------------------- 1 | from urllib.parse import urljoin 2 | 3 | from pydantic import ValidationInfo, field_validator 4 | from pydantic_settings import BaseSettings, SettingsConfigDict 5 | 6 | from polaris.utils.types import HttpUrlString, TimeoutTypes 7 | 8 | 9 | class PolarisHubSettings(BaseSettings): 10 | """Settings for the OAuth2 Polaris Hub API Client. 11 | 12 | Info: Secrecy of these settings 13 | Since the Polaris Hub uses PCKE (Proof Key for Code Exchange) for OAuth2, 14 | these values thus do not have to be kept secret. 15 | See [RFC 7636](https://datatracker.ietf.org/doc/html/rfc7636) for more info. 16 | 17 | Attributes: 18 | hub_url: The URL to the main page of the Polaris Hub. 19 | api_url: The URL to the main entrypoint of the Polaris API. 20 | authorize_url: The URL of the OAuth2 authorization endpoint. 21 | callback_url: The URL to which the user is redirected after authorization. 22 | token_fetch_url: The URL of the OAuth2 token endpoint. 23 | user_info_url: The URL of the OAuth2 user info endpoint. 24 | scopes: The OAuth2 scopes that are requested. 25 | client_id: The OAuth2 client ID. 26 | ca_bundle: The path to a CA bundle file for requests. 27 | Allows for custom SSL certificates to be used. 28 | default_timeout: The default timeout for requests. 29 | hub_token_url: The URL of the Polaris Hub token endpoint. 30 | A default value is generated based on the Hub URL, and this should not need to be overridden. 31 | username: The username for the Polaris Hub, for the optional password-based authentication. 32 | password: The password for the specified username. 33 | """ 34 | 35 | # Configuration of the pydantic model 36 | model_config = SettingsConfigDict( 37 | env_file=".env", env_prefix="POLARIS_", extra="ignore", env_ignore_empty=True 38 | ) 39 | 40 | # Hub settings 41 | hub_url: HttpUrlString = "https://polarishub.io/" 42 | api_url: HttpUrlString | None = None 43 | custom_metadata_prefix: str = "X-Amz-Meta-" 44 | 45 | # Hub authentication settings 46 | hub_token_url: HttpUrlString | None = None 47 | username: str | None = None 48 | password: str | None = None 49 | 50 | # External authentication settings 51 | authorize_url: HttpUrlString = "https://clerk.polarishub.io/oauth/authorize" 52 | callback_url: HttpUrlString | None = None 53 | token_fetch_url: HttpUrlString = "https://clerk.polarishub.io/oauth/token" 54 | user_info_url: HttpUrlString = "https://clerk.polarishub.io/oauth/userinfo" 55 | scopes: str = "profile email" 56 | client_id: str = "agQP2xVM6JqMHvGc" 57 | 58 | # Networking settings 59 | ca_bundle: str | bool | None = None 60 | default_timeout: TimeoutTypes = (10, 200) 61 | 62 | @field_validator("api_url", mode="before") 63 | def validate_api_url(cls, v, info: ValidationInfo): 64 | if v is None: 65 | v = urljoin(str(info.data["hub_url"]), "/api") 66 | return v 67 | 68 | @field_validator("callback_url", mode="before") 69 | def validate_callback_url(cls, v, info: ValidationInfo): 70 | if v is None: 71 | v = urljoin(str(info.data["hub_url"]), "/oauth2/callback") 72 | return v 73 | 74 | @field_validator("hub_token_url", mode="before") 75 | def populate_hub_token_url(cls, v, info: ValidationInfo): 76 | if v is None: 77 | v = urljoin(str(info.data["hub_url"]), "/api/auth/token") 78 | return v 79 | -------------------------------------------------------------------------------- /docs/tutorials/create_a_model.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "A model in Polaris centralizes all data about a method and can be attached to different results.\n", 8 | "\n", 9 | "## Create a Model\n", 10 | "\n", 11 | "To create a model, you need to instantiate the `Model` class. " 12 | ] 13 | }, 14 | { 15 | "cell_type": "code", 16 | "execution_count": null, 17 | "metadata": {}, 18 | "outputs": [], 19 | "source": [ 20 | "from polaris.model import Model\n", 21 | "\n", 22 | "# Create a new Model Card\n", 23 | "model = Model(\n", 24 | " name=\"MolGPS\",\n", 25 | " description=\"Graph transformer foundation model for molecular modeling\",\n", 26 | " code_url=\"https://github.com/datamol-io/graphium\"\n", 27 | ")" 28 | ] 29 | }, 30 | { 31 | "cell_type": "markdown", 32 | "metadata": {}, 33 | "source": [ 34 | "## Share your model\n", 35 | "Want to share your model with the community? Upload it to the Polaris Hub!" 36 | ] 37 | }, 38 | { 39 | "cell_type": "code", 40 | "execution_count": null, 41 | "metadata": {}, 42 | "outputs": [], 43 | "source": [ 44 | "model.upload_to_hub(owner=\"your-username\")" 45 | ] 46 | }, 47 | { 48 | "cell_type": "markdown", 49 | "metadata": {}, 50 | "source": [ 51 | "If you want to upload a new version of your model, you can specify its previous version with the `parent_artifact_id` parameter. Don't forget to add a changelog describing your updates!" 52 | ] 53 | }, 54 | { 55 | "cell_type": "code", 56 | "execution_count": null, 57 | "metadata": {}, 58 | "outputs": [], 59 | "source": [ 60 | "model.artifact_changelog = \"In this version, I added...\"\n", 61 | "\n", 62 | "model.upload_to_hub(\n", 63 | " owner=\"your-username\",\n", 64 | " parent_artifact_id=\"your-username/tutorial-example\"\n", 65 | ")" 66 | ] 67 | }, 68 | { 69 | "cell_type": "markdown", 70 | "metadata": {}, 71 | "source": [ 72 | "## Attach a model with a result\n", 73 | "\n", 74 | "The model card can then be attached to a newly created result on upload." 75 | ] 76 | }, 77 | { 78 | "cell_type": "code", 79 | "execution_count": null, 80 | "metadata": {}, 81 | "outputs": [], 82 | "source": [ 83 | "from polaris import load_benchmark, load_model\n", 84 | "\n", 85 | "# Load a benchmark\n", 86 | "benchmark = load_benchmark(\"polaris/hello-world-benchmark\")\n", 87 | "\n", 88 | "# Get the results\n", 89 | "results = benchmark.evaluate(...)\n", 90 | "\n", 91 | "# Attach it to the result\n", 92 | "results.model = load_model(\"recursion/MolGPS\")\n", 93 | "\n", 94 | "# Upload the results\n", 95 | "results.upload_to_hub(owner=\"your-username\")" 96 | ] 97 | }, 98 | { 99 | "cell_type": "markdown", 100 | "metadata": {}, 101 | "source": [ 102 | "---\n", 103 | "\n", 104 | "The End. " 105 | ] 106 | } 107 | ], 108 | "metadata": { 109 | "kernelspec": { 110 | "display_name": ".venv", 111 | "language": "python", 112 | "name": "python3" 113 | }, 114 | "language_info": { 115 | "codemirror_mode": { 116 | "name": "ipython", 117 | "version": 3 118 | }, 119 | "file_extension": ".py", 120 | "mimetype": "text/x-python", 121 | "name": "python", 122 | "nbconvert_exporter": "python", 123 | "pygments_lexer": "ipython3", 124 | "version": "3.12.0" 125 | } 126 | }, 127 | "nbformat": 4, 128 | "nbformat_minor": 2 129 | } 130 | -------------------------------------------------------------------------------- /tests/test_storage.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import boto3 4 | import pytest 5 | from moto import mock_aws 6 | 7 | from polaris.hub.storage import S3Store 8 | 9 | 10 | @pytest.fixture(scope="function") 11 | def aws_credentials(): 12 | """ 13 | Mocked AWS Credentials for moto. 14 | """ 15 | os.environ["AWS_ACCESS_KEY_ID"] = "testing" 16 | os.environ["AWS_SECRET_ACCESS_KEY"] = "testing" 17 | os.environ["AWS_SECURITY_TOKEN"] = "testing" 18 | os.environ["AWS_SESSION_TOKEN"] = "testing" 19 | 20 | 21 | @pytest.fixture(scope="function") 22 | def mocked_aws(aws_credentials): 23 | """ 24 | Mock all AWS interactions 25 | Requires you to create your own boto3 clients 26 | """ 27 | with mock_aws(): 28 | yield 29 | 30 | 31 | @pytest.fixture 32 | def s3_store(mocked_aws): 33 | # Setup mock S3 environment 34 | s3 = boto3.client("s3", region_name="us-east-1") 35 | bucket_name = "test-bucket" 36 | s3.create_bucket(Bucket=bucket_name) 37 | 38 | # Create an instance of your S3Store 39 | store = S3Store( 40 | path=f"{bucket_name}/prefix", 41 | access_key="fake-access-key", 42 | secret_key="fake-secret-key", 43 | token="fake-token", 44 | endpoint_url="https://s3.amazonaws.com", 45 | ) 46 | 47 | yield store 48 | 49 | 50 | def test_set_and_get_item(s3_store): 51 | key = "test-key" 52 | value = b"test-value" 53 | s3_store[key] = value 54 | 55 | retrieved_value = s3_store[key] 56 | assert retrieved_value == value 57 | 58 | 59 | def test_get_nonexistent_item(s3_store): 60 | with pytest.raises(KeyError): 61 | _ = s3_store["nonexistent-key"] 62 | 63 | 64 | def test_contains_item(s3_store): 65 | key = "test-key" 66 | value = b"test-value" 67 | s3_store[key] = value 68 | 69 | assert key in s3_store 70 | assert "nonexistent-key" not in s3_store 71 | 72 | 73 | def test_store_iterator_empty(s3_store): 74 | stored_keys = list(s3_store) 75 | assert stored_keys == [] 76 | 77 | 78 | def test_store_iterator(s3_store): 79 | keys = ["dir1/subdir1", "dir1/subdir2", "dir1/file1.ext", "dir2/file2.ext"] 80 | for key in keys: 81 | s3_store[key] = b"test" 82 | 83 | stored_keys = list(s3_store) 84 | assert sorted(stored_keys) == sorted(keys) 85 | 86 | 87 | def test_store_length(s3_store): 88 | keys = ["dir1/subdir1", "dir1/subdir2", "dir1/file1.ext", "dir2/file2.ext"] 89 | for key in keys: 90 | s3_store[key] = b"test" 91 | 92 | assert len(s3_store) == len(keys) 93 | 94 | 95 | def test_listdir(s3_store): 96 | keys = ["dir1/subdir1", "dir1/subdir2", "dir1/file1.ext", "dir2/file2.ext"] 97 | for key in keys: 98 | s3_store[key] = b"test" 99 | 100 | dir1_contents = list(s3_store.listdir("dir1")) 101 | assert set(dir1_contents) == {"file1.ext", "subdir1", "subdir2"} 102 | 103 | dir1_contents = list(s3_store.listdir()) 104 | assert set(dir1_contents) == {"dir1", "dir2"} 105 | 106 | 107 | def test_getsize(s3_store): 108 | key = "test-key" 109 | value = b"test-value" 110 | s3_store[key] = value 111 | 112 | size = s3_store.getsize(key) 113 | assert size == len(value) 114 | 115 | 116 | def test_getitems(s3_store): 117 | keys = ["dir1/subdir1", "dir1/subdir2", "dir1/file1.ext", "dir2/file2.ext"] 118 | for key in keys: 119 | s3_store[key] = b"test" 120 | 121 | items = s3_store.getitems(keys, contexts={}) 122 | assert len(items) == len(keys) 123 | assert all(key in items for key in keys) 124 | 125 | 126 | def test_delete_item_not_supported(s3_store): 127 | with pytest.raises(NotImplementedError): 128 | del s3_store["some-key"] 129 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: release 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | release-version: 7 | description: "A valid Semver version string" 8 | required: true 9 | 10 | permissions: 11 | contents: write 12 | pull-requests: write 13 | id-token: write 14 | 15 | concurrency: 16 | group: "release-${{ github.ref }}" 17 | cancel-in-progress: false 18 | 19 | defaults: 20 | run: 21 | shell: bash -l {0} 22 | 23 | jobs: 24 | check-semver: 25 | # Do not release if not triggered from the default branch 26 | if: github.ref == format('refs/heads/{0}', github.event.repository.default_branch) 27 | 28 | runs-on: ubuntu-latest 29 | timeout-minutes: 30 30 | 31 | steps: 32 | - name: Checkout the code 33 | uses: actions/checkout@v4 34 | with: 35 | fetch-depth: 0 36 | 37 | - name: Get version 38 | id: version 39 | run: | 40 | version=$(git describe --abbrev=0 --tags) 41 | echo $version 42 | echo "version=${version}" >> $GITHUB_OUTPUT 43 | 44 | - name: Semver check 45 | id: semver_check 46 | uses: madhead/semver-utils@v4 47 | with: 48 | lenient: false 49 | version: ${{ inputs.release-version }} 50 | compare-to: ${{ steps.version.outputs.version }} 51 | 52 | - name: Semver ok 53 | if: steps.semver_check.outputs.comparison-result != '>' 54 | run: | 55 | echo "The release version is not valid Semver (${{ inputs.release-version }}) that is greater than the current version ${{ steps.version.outputs.version }}." 56 | exit 1 57 | 58 | release: 59 | needs: check-semver 60 | 61 | runs-on: ubuntu-latest 62 | timeout-minutes: 30 63 | 64 | env: 65 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 66 | 67 | steps: 68 | - name: Checkout the code 69 | uses: actions/checkout@v4 70 | with: 71 | fetch-depth: 0 72 | 73 | - name: Install uv 74 | uses: astral-sh/setup-uv@v5 75 | 76 | - name: Install the project 77 | run: uv sync --frozen --all-groups --python 3.12 78 | 79 | - name: Build Changelog 80 | id: github_release 81 | uses: mikepenz/release-changelog-builder-action@v5 82 | with: 83 | toTag: "main" 84 | configuration: ".github/changelog_config.json" 85 | 86 | - name: Create and push git tag 87 | run: | 88 | # Configure git 89 | git config --global user.name "${GITHUB_ACTOR}" 90 | git config --global user.email "${GITHUB_ACTOR}@users.noreply.github.com" 91 | 92 | # Tag the release 93 | git tag -a "${{ inputs.release-version }}" -m "Release version ${{ inputs.release-version }}" 94 | 95 | # Checkout the git tag 96 | git checkout "${{ inputs.release-version }}" 97 | 98 | # Push the modified changelogs 99 | git push origin main 100 | 101 | # Push the tags 102 | git push origin "${{ inputs.release-version }}" 103 | 104 | - name: Build the wheel and sdist 105 | run: uv build 106 | 107 | - name: Publish package to PyPI 108 | uses: pypa/gh-action-pypi-publish@release/v1 109 | with: 110 | password: ${{ secrets.PYPI_API_TOKEN }} 111 | packages-dir: dist/ 112 | 113 | - name: Create GitHub Release 114 | uses: softprops/action-gh-release@de2c0eb89ae2a093876385947365aca7b0e5f844 115 | with: 116 | tag_name: ${{ inputs.release-version }} 117 | body: ${{steps.github_release.outputs.changelog}} 118 | 119 | - name: Deploy the doc 120 | run: | 121 | echo "Get the gh-pages branch" 122 | git fetch origin gh-pages 123 | 124 | echo "Build and deploy the doc on ${{ inputs.release-version }}" 125 | uv run mike deploy --push stable 126 | uv run mike deploy --push ${{ inputs.release-version }} 127 | -------------------------------------------------------------------------------- /docs/quickstart.md: -------------------------------------------------------------------------------- 1 | # Quickstart 2 | Welcome to the Polaris Quickstart guide! This page will introduce you to core concepts and you'll submit a first result to a benchmark on the [Polaris Hub](https://www.polarishub.io). 3 | 4 | ## Installation 5 | !!! warning "`polaris-lib` vs `polaris`" 6 | Be aware that the package name differs between _pip_ and _conda_. 7 | 8 | Polaris can be installed via _pip_: 9 | 10 | ```bash 11 | pip install polaris-lib 12 | ``` 13 | 14 | or _conda_: 15 | ```bash 16 | conda install -c conda-forge polaris 17 | ``` 18 | 19 | ## Core concepts 20 | Polaris explicitly distinguished **datasets** and **benchmarks**. 21 | 22 | - A _dataset_ is simply a tabular collection of data, storing datapoints in a row-wise manner. 23 | - A _benchmark_ defines the ML task and evaluation logic (e.g. split and metrics) for a dataset. 24 | 25 | One dataset can therefore be associated with multiple benchmarks. 26 | 27 | ## Login 28 | To submit or upload artifacts to the [Polaris Hub](https://polarishub.io/) from the client, you must first authenticate yourself. If you don't have an account yet, you can create one [here](https://polarishub.io/sign-up). 29 | 30 | You can do this via the following command in your terminal: 31 | 32 | ```bash 33 | polaris login 34 | ``` 35 | 36 | or in Python: 37 | ```py 38 | from polaris.hub.client import PolarisHubClient 39 | 40 | with PolarisHubClient() as client: 41 | client.login() 42 | ``` 43 | 44 | ## Benchmark API 45 | To get started, we will submit a result to the [`polaris/hello-world-benchmark`](https://polarishub.io/benchmarks/polaris/hello-world-benchmark). 46 | 47 | ```python 48 | import polaris as po 49 | 50 | # Load the benchmark from the Hub 51 | benchmark = po.load_benchmark("polaris/hello-world-benchmark") 52 | 53 | # Get the train and test data-loaders 54 | train, test = benchmark.get_train_test_split() 55 | 56 | # Use the training data to train your model 57 | # Get the input as an array with 'train.inputs' and 'train.targets' 58 | # Or simply iterate over the train object. 59 | for x, y in train: 60 | ... 61 | 62 | # Work your magic to accurately predict the test set 63 | predictions = [0.0 for x in test] 64 | 65 | # Evaluate your predictions 66 | results = benchmark.evaluate(predictions) 67 | 68 | # Submit your results 69 | results.upload_to_hub(owner="dummy-user") 70 | ``` 71 | 72 | Through immutable datasets and standardized benchmarks, Polaris aims to serve as a source of truth for machine learning in drug discovery. The limited flexibility might differ from your typical experience, but this is by design to improve reproducibility. Learn more [here](https://polarishub.io/blog/reproducible-machine-learning-in-drug-discovery-how-polaris-serves-as-a-single-source-of-truth). 73 | 74 | ## Dataset API 75 | Loading a benchmark will automatically load the underlying dataset. We can also directly access the [`polaris/hello-world`](https://polarishub.io/datasets/polaris/hello-world) dataset. 76 | 77 | ```python 78 | import polaris as po 79 | 80 | # Load the dataset from the Hub 81 | dataset = po.load_dataset("polaris/hello-world") 82 | 83 | # Get information on the dataset size 84 | dataset.size() 85 | 86 | # Load a datapoint in memory 87 | dataset.get_data( 88 | row=dataset.rows[0], 89 | col=dataset.columns[0], 90 | ) 91 | 92 | # Or, similarly: 93 | dataset[dataset.rows[0], dataset.columns[0]] 94 | 95 | # Get an entire data point 96 | dataset[0] 97 | ``` 98 | 99 | Drug discovery research involves a maze of file formats (e.g. PDB for 3D structures, SDF for small molecules, and so on). Each format requires specialized knowledge to parse and interpret properly. At Polaris, we wanted to remove that barrier. We use a universal data format based on [Zarr](https://zarr.dev/). Learn more [here](https://polarishub.io/blog/dataset-v2-built-to-scale). 100 | 101 | ## Where to next? 102 | 103 | Now that you've seen how easy it is to use Polaris, let's dive into the details through [a set of tutorials](./tutorials/submit_to_benchmark.ipynb)! 104 | 105 | --- 106 | -------------------------------------------------------------------------------- /polaris/utils/errors.py: -------------------------------------------------------------------------------- 1 | import certifi 2 | 3 | 4 | class InvalidDatasetError(ValueError): 5 | pass 6 | 7 | 8 | class InvalidBenchmarkError(ValueError): 9 | pass 10 | 11 | 12 | class InvalidCompetitionError(ValueError): 13 | pass 14 | 15 | 16 | class InvalidResultError(ValueError): 17 | pass 18 | 19 | 20 | class TestAccessError(Exception): 21 | # Prevent pytest to collect this as a test 22 | __test__ = False 23 | 24 | pass 25 | 26 | 27 | class PolarisChecksumError(ValueError): 28 | pass 29 | 30 | 31 | class InvalidZarrChecksum(Exception): 32 | pass 33 | 34 | 35 | class InvalidZarrCodec(Exception): 36 | """Raised when an expected codec is not registered.""" 37 | 38 | def __init__(self, codec_id: str): 39 | self.codec_id = codec_id 40 | super().__init__( 41 | f"This Zarr archive requires the {self.codec_id} codec. " 42 | "Install all optional codecs with 'pip install polaris-lib[codecs]'." 43 | ) 44 | 45 | 46 | class PolarisHubError(Exception): 47 | BOLD = "\033[1m" 48 | YELLOW = "\033[93m" 49 | _END_CODE = "\033[0m" 50 | 51 | def __init__(self, message: str = "", response_text: str = ""): 52 | parts = filter( 53 | bool, 54 | [ 55 | f"{self.BOLD}The request to the Polaris Hub has failed.{self._END_CODE}", 56 | f"{self.YELLOW}{message}{self._END_CODE}" if message else "", 57 | f"----------------------\nError reported was:\n{response_text}" if response_text else "", 58 | ], 59 | ) 60 | 61 | super().__init__("\n".join(parts)) 62 | 63 | 64 | class PolarisUnauthorizedError(PolarisHubError): 65 | def __init__(self, response_text: str = ""): 66 | message = ( 67 | "You are not logged in to Polaris or your login has expired. " 68 | "You can use the Polaris CLI to easily authenticate yourself again with `polaris login --overwrite`." 69 | ) 70 | super().__init__(message, response_text) 71 | 72 | 73 | class PolarisCreateArtifactError(PolarisHubError): 74 | def __init__(self, response_text: str = ""): 75 | message = ( 76 | "Note: If you can confirm that you are authorized to perform this action, " 77 | "please call 'polaris login --overwrite' and try again. If the issue persists, please reach out to the Polaris team for support." 78 | ) 79 | super().__init__(message, response_text) 80 | 81 | 82 | class PolarisRetrieveArtifactError(PolarisHubError): 83 | def __init__(self, response_text: str = ""): 84 | message = ( 85 | "Note: If this artifact exists and you can confirm that you are authorized to retrieve it, " 86 | "please call 'polaris login --overwrite' and try again. If the issue persists, please reach out to the Polaris team for support." 87 | ) 88 | super().__init__(message, response_text) 89 | 90 | 91 | class PolarisSSLError(PolarisHubError): 92 | def __init__(self, response_text: str = ""): 93 | message = ( 94 | "We could not verify the SSL certificate. " 95 | f"Please ensure the installed version ({certifi.__version__}) of the `certifi` package is the latest. " 96 | "If you require the usage of a custom CA bundle, you can set the POLARIS_CA_BUNDLE " 97 | "environment variable to the path of your CA bundle. For debugging, you can temporarily disable " 98 | "SSL verification by setting the POLARIS_CA_BUNDLE environment variable to `false`." 99 | ) 100 | super().__init__(message, response_text) 101 | 102 | 103 | class PolarisDeprecatedError(PolarisHubError): 104 | def __init__(self, feature: str, response_text: str = ""): 105 | message = ( 106 | f"The '{feature}' feature has been deprecated and is no longer supported. " 107 | "Please contact the Polaris team for more information about alternative approaches." 108 | ) 109 | super().__init__(message, response_text) 110 | -------------------------------------------------------------------------------- /tests/test_benchmark_predictions_v2.py: -------------------------------------------------------------------------------- 1 | from polaris.prediction._predictions_v2 import BenchmarkPredictionsV2 2 | from polaris.utils.zarr.codecs import RDKitMolCodec, AtomArrayCodec 3 | from rdkit import Chem 4 | import numpy as np 5 | import pytest 6 | import datamol as dm 7 | import zarr 8 | from fastpdb import struc 9 | 10 | 11 | def assert_deep_equal(result, expected): 12 | assert isinstance(result, type(expected)), f"Types differ: {type(result)} != {type(expected)}" 13 | if isinstance(expected, dict): 14 | assert result.keys() == expected.keys() 15 | for key in expected: 16 | assert_deep_equal(result[key], expected[key]) 17 | elif isinstance(expected, np.ndarray): 18 | assert np.array_equal(result, expected) 19 | else: 20 | assert result == expected 21 | 22 | 23 | def test_v2_rdkit_object_codec(v2_benchmark_with_rdkit_object_dtype): 24 | mols = [dm.to_mol("CCO"), dm.to_mol("CCN")] 25 | preds = {"test": {"expt": mols}} 26 | bp = BenchmarkPredictionsV2( 27 | predictions=preds, 28 | dataset_zarr_root=v2_benchmark_with_rdkit_object_dtype.dataset.zarr_root, 29 | benchmark_artifact_id=v2_benchmark_with_rdkit_object_dtype.artifact_id, 30 | target_labels=["expt"], 31 | test_set_labels=["test"], 32 | test_set_sizes={"test": 2}, 33 | ) 34 | assert isinstance(bp.predictions["test"]["expt"], np.ndarray) 35 | assert bp.predictions["test"]["expt"].dtype == object 36 | assert_deep_equal(bp.predictions, {"test": {"expt": np.array(mols, dtype=object)}}) 37 | 38 | # Check Zarr archive 39 | zarr_path = bp.to_zarr() 40 | assert zarr_path.exists() 41 | root = zarr.open(str(zarr_path), mode="r") 42 | arr = root["test"]["expt"][:] 43 | arr_smiles = [Chem.MolToSmiles(m) for m in arr] 44 | mols_smiles = [Chem.MolToSmiles(m) for m in mols] 45 | assert arr_smiles == mols_smiles 46 | 47 | # Check that object_codec is correctly set as a filter (Zarr stores object_codec as filters) 48 | zarr_array = root["test"]["expt"] 49 | assert zarr_array.filters is not None 50 | assert len(zarr_array.filters) > 0 51 | assert any(isinstance(f, RDKitMolCodec) for f in zarr_array.filters) 52 | 53 | 54 | def test_v2_atomarray_object_codec(v2_benchmark_with_atomarray_object_dtype, pdbs_structs): 55 | # Use fastpdb.AtomArray objects 56 | preds = {"test": {"expt": np.array(pdbs_structs[:2], dtype=object)}} 57 | bp = BenchmarkPredictionsV2( 58 | predictions=preds, 59 | dataset_zarr_root=v2_benchmark_with_atomarray_object_dtype.dataset.zarr_root, 60 | benchmark_artifact_id=v2_benchmark_with_atomarray_object_dtype.artifact_id, 61 | target_labels=["expt"], 62 | test_set_labels=["test"], 63 | test_set_sizes={"test": 2}, 64 | ) 65 | assert isinstance(bp.predictions["test"]["expt"], np.ndarray) 66 | assert bp.predictions["test"]["expt"].dtype == object 67 | assert_deep_equal(bp.predictions, {"test": {"expt": np.array(pdbs_structs[:2], dtype=object)}}) 68 | 69 | # Check Zarr archive (dtype and shape only) 70 | zarr_path = bp.to_zarr() 71 | assert zarr_path.exists() 72 | root = zarr.open(str(zarr_path), mode="r") 73 | arr = root["test"]["expt"][:] 74 | assert arr.dtype == object 75 | assert arr.shape == (2,) 76 | assert all(isinstance(x, struc.AtomArray) for x in arr) 77 | 78 | # Check that object_codec is correctly set as a filter (Zarr stores object_codec as filters) 79 | zarr_array = root["test"]["expt"] 80 | assert zarr_array.filters is not None 81 | assert len(zarr_array.filters) > 0 82 | assert any(isinstance(f, AtomArrayCodec) for f in zarr_array.filters) 83 | 84 | 85 | def test_v2_dtype_mismatch_raises(test_benchmark_v2): 86 | # Create a list of rdkit.Chem.Mol objects (object dtype) to test against float dtype dataset 87 | mols = [dm.to_mol("CCO"), dm.to_mol("CCN")] 88 | preds = {"test": {"A": mols}} # Using column "A" which has float dtype in test_dataset_v2 89 | with pytest.raises(ValueError, match="Dtype mismatch"): 90 | BenchmarkPredictionsV2( 91 | predictions=preds, 92 | dataset_zarr_root=test_benchmark_v2.dataset.zarr_root, 93 | benchmark_artifact_id=test_benchmark_v2.artifact_id, 94 | target_labels=["A"], 95 | test_set_labels=["test"], 96 | test_set_sizes={"test": 2}, 97 | ) 98 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
7 | 8 | ✨ Polaris Hub 9 | | 10 | 11 | 📚 Client Doc 12 | 13 |
14 | 15 | --- 16 | 17 | | | | 18 | | --- | --- | 19 | | Latest Release | [](https://pypi.org/project/polaris-lib/) | 20 | | | [](https://anaconda.org/conda-forge/polaris) | 21 | | Python Version | [](https://pypi.org/project/polaris-lib/) | 22 | | License | [](https://github.com/polaris-hub/polaris/blob/main/LICENSE) | 23 | | Downloads | [](https://pypi.org/project/polaris-lib/) | 24 | | | [](https://anaconda.org/conda-forge/polaris) | 25 | | Citation | [](https://doi.org/10.1038/s42256-024-00911-w) | 26 | 27 | Polaris establishes a novel, industry‑certified standard to foster the development of impactful methods in AI-based drug discovery. 28 | 29 | This library is a Python client to interact with the [Polaris Hub](https://polarishub.io/). It allows you to: 30 | 31 | - Download Polaris datasets and benchmarks. 32 | - Evaluate a custom method against a Polaris benchmark. 33 | 34 | ## Quick API Tour 35 | 36 | ```python 37 | import polaris as po 38 | 39 | # Load the benchmark from the Hub 40 | benchmark = po.load_benchmark("polaris/hello-world-benchmark") 41 | 42 | # Get the train and test data-loaders 43 | train, test = benchmark.get_train_test_split() 44 | 45 | # Use the training data to train your model 46 | # Get the input as an array with 'train.inputs' and 'train.targets' 47 | # Or simply iterate over the train object. 48 | for x, y in train: 49 | ... 50 | 51 | # Work your magic to accurately predict the test set 52 | predictions = [0.0 for x in test] 53 | 54 | # Evaluate your predictions 55 | results = benchmark.evaluate(predictions) 56 | 57 | # Submit your results 58 | results.upload_to_hub(owner="dummy-user") 59 | ``` 60 | 61 | ## Documentation 62 | 63 | Please refer to the [documentation](https://polaris-hub.github.io/polaris/), which contains tutorials for getting started with `polaris` and detailed descriptions of the functions provided. 64 | 65 | ## How to cite 66 | 67 | Please cite Polaris if you use it in your research. A list of relevant publications: 68 | 69 | - [](https://doi.org/10.26434/chemrxiv-2024-6dbwv-v2) - Preprint, Method Comparison Guidelines. 70 | - [](https://doi.org/10.1038/s42256-024-00911-w) - Nature Correspondence, Call to Action. 71 | - [](https://doi.org/10.5281/zenodo.13652587) - Zenodo, Code Repository. 72 | 73 | ## Installation 74 | 75 | You can install `polaris` using conda/mamba/micromamba: 76 | 77 | ```bash 78 | conda install -c conda-forge polaris 79 | ``` 80 | 81 | You can also use pip: 82 | 83 | ```bash 84 | pip install polaris-lib 85 | ``` 86 | 87 | ## Development lifecycle 88 | 89 | ### Setup dev environment 90 | 91 | ```shell 92 | conda env create -n polaris -f env.yml 93 | conda activate polaris 94 | 95 | pip install --no-deps -e . 96 | ``` 97 | 98 |Where are my results?
\n", 138 | "The results will only be published at predetermined intervals, as detailed in the competition details. Keep an eye on that leaderboard when it goes public and best of luck!
\n", 139 | "| " + " | ".join(column_headers) + " |
|---|---|
| " 124 | converted_output += " | ".join( 125 | [self.convert_json_node(list_entry[column_header]) for column_header in column_headers] 126 | ) 127 | converted_output += " |