smbt 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- smbt-0.1.0/LICENSE +72 -0
- smbt-0.1.0/MANIFEST.in +19 -0
- smbt-0.1.0/PKG-INFO +178 -0
- smbt-0.1.0/README.md +154 -0
- smbt-0.1.0/bindings/smbt_core.cpp +235 -0
- smbt-0.1.0/pyproject.toml +49 -0
- smbt-0.1.0/python/smbt/__init__.py +47 -0
- smbt-0.1.0/python/smbt.egg-info/PKG-INFO +178 -0
- smbt-0.1.0/python/smbt.egg-info/SOURCES.txt +51 -0
- smbt-0.1.0/python/smbt.egg-info/dependency_links.txt +1 -0
- smbt-0.1.0/python/smbt.egg-info/top_level.txt +1 -0
- smbt-0.1.0/setup.cfg +4 -0
- smbt-0.1.0/setup.py +40 -0
- smbt-0.1.0/src/build.cpp +165 -0
- smbt-0.1.0/src/lib/bit_array.cpp +164 -0
- smbt-0.1.0/src/lib/bit_array.hpp +62 -0
- smbt-0.1.0/src/lib/checked_read.hpp +30 -0
- smbt-0.1.0/src/lib/error.hpp +27 -0
- smbt-0.1.0/src/lib/suc_array.cpp +101 -0
- smbt-0.1.0/src/lib/suc_array.hpp +68 -0
- smbt-0.1.0/src/lib/var_byte.cpp +94 -0
- smbt-0.1.0/src/lib/var_byte.hpp +58 -0
- smbt-0.1.0/src/mbt/multibit_tree.cpp +434 -0
- smbt-0.1.0/src/mbt/multibit_tree.hpp +240 -0
- smbt-0.1.0/src/rsdic/lib/Const.hpp +36 -0
- smbt-0.1.0/src/rsdic/lib/EnumCoder.cpp +264 -0
- smbt-0.1.0/src/rsdic/lib/EnumCoder.hpp +53 -0
- smbt-0.1.0/src/rsdic/lib/EnumCoderTest.cpp +46 -0
- smbt-0.1.0/src/rsdic/lib/RSDic.cpp +182 -0
- smbt-0.1.0/src/rsdic/lib/RSDic.hpp +87 -0
- smbt-0.1.0/src/rsdic/lib/RSDicBuilder.cpp +111 -0
- smbt-0.1.0/src/rsdic/lib/RSDicBuilder.hpp +58 -0
- smbt-0.1.0/src/rsdic/lib/RSDicBuilderTest.cpp +76 -0
- smbt-0.1.0/src/rsdic/lib/RSDicTest.cpp +157 -0
- smbt-0.1.0/src/rsdic/lib/Type.hpp +30 -0
- smbt-0.1.0/src/rsdic/lib/Util.hpp +69 -0
- smbt-0.1.0/src/rsdic/lib/UtilTest.cpp +55 -0
- smbt-0.1.0/src/rsdic/tool/Combination.hpp +35 -0
- smbt-0.1.0/src/rsdic/tool/CombinationGenerator.cpp +47 -0
- smbt-0.1.0/src/rsdic/tool/CombinationTest.cpp +16 -0
- smbt-0.1.0/src/rsdic/tool/PerformanceTest.cpp +102 -0
- smbt-0.1.0/src/search.cpp +140 -0
- smbt-0.1.0/src/smbt_trie/succinct_multibit_tree_trie.cpp +709 -0
- smbt-0.1.0/src/smbt_trie/succinct_multibit_tree_trie.hpp +333 -0
- smbt-0.1.0/src/smbt_vla/succinct_multibit_tree_vla.cpp +489 -0
- smbt-0.1.0/src/smbt_vla/succinct_multibit_tree_vla.hpp +291 -0
- smbt-0.1.0/tests/conftest.py +68 -0
- smbt-0.1.0/tests/test_errors.py +74 -0
- smbt-0.1.0/tests/test_golden.py +53 -0
- smbt-0.1.0/tests/test_inmemory_vs_file.py +32 -0
- smbt-0.1.0/tests/test_modes_agree.py +72 -0
- smbt-0.1.0/tests/test_save_load.py +25 -0
- smbt-0.1.0/tests/test_verbose.py +21 -0
smbt-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2013-2026 Yasuo Tabei
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
================================================================================
|
|
25
|
+
THIRD-PARTY NOTICES
|
|
26
|
+
================================================================================
|
|
27
|
+
|
|
28
|
+
This distribution bundles third-party source code, licensed as follows.
|
|
29
|
+
|
|
30
|
+
--------------------------------------------------------------------------------
|
|
31
|
+
1. rsdic — rank/select dictionary (src/rsdic/)
|
|
32
|
+
--------------------------------------------------------------------------------
|
|
33
|
+
|
|
34
|
+
Copyright (c) 2012 Daisuke Okanohara
|
|
35
|
+
Licensed under the BSD 3-Clause License.
|
|
36
|
+
|
|
37
|
+
Vendored from https://code.google.com/p/rsdic (commit bf206fb, "fix bug in
|
|
38
|
+
select", 2012-04-22), included unmodified.
|
|
39
|
+
|
|
40
|
+
Redistribution and use in source and binary forms, with or without
|
|
41
|
+
modification, are permitted provided that the following conditions
|
|
42
|
+
are met:
|
|
43
|
+
|
|
44
|
+
1. Redistributions of source code must retain the above Copyright
|
|
45
|
+
notice, this list of conditions and the following disclaimer.
|
|
46
|
+
|
|
47
|
+
2. Redistributions in binary form must reproduce the above Copyright
|
|
48
|
+
notice, this list of conditions and the following disclaimer in the
|
|
49
|
+
documentation and/or other materials provided with the distribution.
|
|
50
|
+
|
|
51
|
+
3. Neither the name of the authors nor the names of its contributors
|
|
52
|
+
may be used to endorse or promote products derived from this
|
|
53
|
+
software without specific prior written permission.
|
|
54
|
+
|
|
55
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
|
|
56
|
+
IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
|
57
|
+
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
58
|
+
PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
|
|
59
|
+
CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
|
|
60
|
+
EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
|
|
61
|
+
PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
|
|
62
|
+
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
|
|
63
|
+
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
|
|
64
|
+
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
|
65
|
+
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
66
|
+
|
|
67
|
+
--------------------------------------------------------------------------------
|
|
68
|
+
2. VarByte — variable-byte / delta coder (src/lib/var_byte.{hpp,cpp})
|
|
69
|
+
--------------------------------------------------------------------------------
|
|
70
|
+
|
|
71
|
+
Copyright (c) 2009 Daisuke Okanohara
|
|
72
|
+
Licensed under the MIT License (same terms as above).
|
smbt-0.1.0/MANIFEST.in
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# Files needed to build and test the extension from an sdist.
|
|
2
|
+
include LICENSE README.md pyproject.toml setup.py
|
|
3
|
+
|
|
4
|
+
# C++ sources and headers (the extension compiles a fixed subset; ship all so
|
|
5
|
+
# transitive includes resolve). The vendored rsdic lives under src/rsdic/.
|
|
6
|
+
recursive-include src *.hpp *.cpp *.h
|
|
7
|
+
recursive-include bindings *.cpp
|
|
8
|
+
|
|
9
|
+
# Python package and test suite.
|
|
10
|
+
graft python
|
|
11
|
+
graft tests
|
|
12
|
+
|
|
13
|
+
# Large sample data and the shell regression harness are kept in git only;
|
|
14
|
+
# the golden pytest skips gracefully when they are absent.
|
|
15
|
+
prune dat
|
|
16
|
+
prune test
|
|
17
|
+
prune prog
|
|
18
|
+
|
|
19
|
+
global-exclude *.o *.so *~ __pycache__/* *.pyc
|
smbt-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: smbt
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Succinct Multibit Tree: fast Jaccard/Tanimoto similarity search over chemical fingerprints
|
|
5
|
+
Author-email: Yasuo Tabei <yasuo.tabei@gmail.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/tb-yasu/smbt
|
|
8
|
+
Project-URL: Repository, https://github.com/tb-yasu/smbt
|
|
9
|
+
Project-URL: Paper (WABI 2012), https://doi.org/10.1007/978-3-642-33122-0_16
|
|
10
|
+
Keywords: cheminformatics,fingerprint,similarity-search,tanimoto,jaccard,succinct-data-structure
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Science/Research
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Programming Language :: C++
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Topic :: Scientific/Engineering :: Chemistry
|
|
17
|
+
Classifier: Topic :: Scientific/Engineering :: Bio-Informatics
|
|
18
|
+
Classifier: Operating System :: MacOS
|
|
19
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
20
|
+
Requires-Python: >=3.9
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
License-File: LICENSE
|
|
23
|
+
Dynamic: license-file
|
|
24
|
+
|
|
25
|
+
# smbt — Succinct Multibit Tree
|
|
26
|
+
|
|
27
|
+
Fast Jaccard/Tanimoto similarity search over chemical fingerprints, backed by
|
|
28
|
+
succinct data structures. `smbt` indexes large fingerprint databases in little
|
|
29
|
+
memory and, given a query fingerprint, returns every database entry whose
|
|
30
|
+
Jaccard (Tanimoto) similarity is at or above a threshold.
|
|
31
|
+
|
|
32
|
+
It is a Python binding around the original C++ implementation from
|
|
33
|
+
[Tabei, WABI 2012](https://doi.org/10.1007/978-3-642-33122-0_16) (see
|
|
34
|
+
[Citation](#citation)).
|
|
35
|
+
|
|
36
|
+
## Installation
|
|
37
|
+
|
|
38
|
+
```sh
|
|
39
|
+
pip install smbt
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Wheels are provided for macOS (x86_64/arm64) and Linux (x86_64/aarch64) on
|
|
43
|
+
CPython 3.9+. Other platforms build from the source distribution and need a
|
|
44
|
+
C++14 compiler. Windows is not supported yet.
|
|
45
|
+
|
|
46
|
+
## Quick start
|
|
47
|
+
|
|
48
|
+
```python
|
|
49
|
+
import smbt
|
|
50
|
+
|
|
51
|
+
# Each fingerprint is a set of uint32 item ids (e.g. "on" bits of a
|
|
52
|
+
# structural fingerprint). Duplicate ids within a fingerprint are ignored.
|
|
53
|
+
data = [
|
|
54
|
+
[1, 5, 9],
|
|
55
|
+
[2, 5, 8],
|
|
56
|
+
[1, 5, 9, 12],
|
|
57
|
+
]
|
|
58
|
+
|
|
59
|
+
idx = smbt.build(data, mode=1, minsup=10)
|
|
60
|
+
|
|
61
|
+
# Every database fingerprint at similarity >= 0.5 to the query,
|
|
62
|
+
# as (id, similarity) sorted by similarity descending then id ascending.
|
|
63
|
+
idx.search([1, 5, 9], similarity=0.5)
|
|
64
|
+
# -> [(0, 1.0), (2, 0.75)]
|
|
65
|
+
|
|
66
|
+
idx.save("db.smbt")
|
|
67
|
+
smbt.load("db.smbt").search([1, 5, 9], similarity=0.9)
|
|
68
|
+
# -> [(0, 1.0)]
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
`id` is the 0-based position of the fingerprint in the input. `similarity` is
|
|
72
|
+
the exact Jaccard/Tanimoto coefficient.
|
|
73
|
+
|
|
74
|
+
Build straight from a file (one whitespace-separated fingerprint per line):
|
|
75
|
+
|
|
76
|
+
```python
|
|
77
|
+
idx = smbt.build_file("fingerprints.dat", mode=1, minsup=10)
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## API
|
|
81
|
+
|
|
82
|
+
| Call | Description |
|
|
83
|
+
|------|-------------|
|
|
84
|
+
| `smbt.build(data, mode=1, minsup=10, verbose=False)` | Build from a list of fingerprints (each a list of ints). |
|
|
85
|
+
| `smbt.build_file(path, mode=1, minsup=10, verbose=False)` | Build from a fingerprint file. |
|
|
86
|
+
| `smbt.load(path)` | Load an index written by `Index.save`. |
|
|
87
|
+
| `Index.search(query, similarity=0.9)` | `[(id, similarity), ...]` above the threshold. |
|
|
88
|
+
| `Index.save(path)` | Persist the index. |
|
|
89
|
+
| `Index.mode` / `Index.size_in_bytes()` | Mode (1/2/3) and approximate index size. |
|
|
90
|
+
|
|
91
|
+
`mode` selects the representation; **all three return identical results**:
|
|
92
|
+
|
|
93
|
+
- **1** — succinct multibit tree + succinct trie (smallest index)
|
|
94
|
+
- **2** — succinct multibit tree + variable-length array
|
|
95
|
+
- **3** — pointer-based multibit tree (uncompressed baseline)
|
|
96
|
+
|
|
97
|
+
`minsup` is the minimum number of fingerprints per leaf during construction; it
|
|
98
|
+
affects index size and speed, not the result set.
|
|
99
|
+
|
|
100
|
+
`search()` releases the GIL and is safe to call concurrently from multiple
|
|
101
|
+
threads on the same index. `build`, `save`, and `load` are not thread-safe.
|
|
102
|
+
|
|
103
|
+
Errors map to Python exceptions: invalid `mode`/`similarity`, an empty dataset,
|
|
104
|
+
an empty fingerprint, or an empty query raise `ValueError`; an unreadable,
|
|
105
|
+
truncated, or unrecognized index raises `smbt.Error`.
|
|
106
|
+
|
|
107
|
+
## Command-line tools
|
|
108
|
+
|
|
109
|
+
The original CLI is also available:
|
|
110
|
+
|
|
111
|
+
```sh
|
|
112
|
+
smbt-build -mode 1 -minsup 10 fingerprints.dat db.smbt
|
|
113
|
+
smbt-search -similarity 0.9 db.smbt queries.dat
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
Options must precede the positional arguments. Input format: one fingerprint
|
|
117
|
+
per line, whitespace-separated uint32 item ids (treated as a set; blank lines
|
|
118
|
+
are skipped).
|
|
119
|
+
|
|
120
|
+
## Constraints
|
|
121
|
+
|
|
122
|
+
- **64-bit only.** Wheels are built for 64-bit platforms (x86_64, arm64); 32-bit
|
|
123
|
+
is not supported (the format and the algorithm assume a 64-bit `size_t`).
|
|
124
|
+
- **Index files are not portable across architectures.** They are a raw binary
|
|
125
|
+
dump assuming 64-bit, same-endianness, same `size_t` width. Build and read an
|
|
126
|
+
index on the same kind of machine (x86_64 and arm64 are both little-endian, so
|
|
127
|
+
this is rarely an issue in practice).
|
|
128
|
+
- **Item ids are treated as a dense dimension.** Very large ids (e.g. 2^31)
|
|
129
|
+
inflate memory; map your fingerprint bits to a compact id range first.
|
|
130
|
+
- Indexes written by pre-0.1 builds (format v1) are not readable; rebuild them.
|
|
131
|
+
- In-memory `build` treats an empty fingerprint as an error, whereas the file
|
|
132
|
+
path skips blank lines. A blank-line-free file and the equivalent in-memory
|
|
133
|
+
data produce a byte-identical index.
|
|
134
|
+
|
|
135
|
+
## How it works
|
|
136
|
+
|
|
137
|
+
Fingerprints are grouped by cardinality; each group is indexed by one binary
|
|
138
|
+
tree built by recursively splitting on the item that maximizes entropy. Internal
|
|
139
|
+
nodes record the columns that are all-ones / all-zeros within their subtree;
|
|
140
|
+
accumulating these gives an admissible Jaccard upper bound for branch-and-bound
|
|
141
|
+
pruning, and leaves compute the exact Jaccard. Because pruning is admissible and
|
|
142
|
+
leaves are exact, the result set is independent of the tree shape — which is why
|
|
143
|
+
all three modes agree. Modes 1 and 2 encode the trees and the fingerprint store
|
|
144
|
+
with succinct data structures (a vendored rank/select dictionary) to shrink the
|
|
145
|
+
index; mode 3 is the uncompressed pointer-based baseline.
|
|
146
|
+
|
|
147
|
+
## Citation
|
|
148
|
+
|
|
149
|
+
If you use this software in academic work, please cite:
|
|
150
|
+
|
|
151
|
+
> Yasuo Tabei. "Succinct Multibit Tree: Compact Representation of Multibit Trees
|
|
152
|
+
> by Using Succinct Data Structures in Chemical Fingerprint Searches." In
|
|
153
|
+
> *Algorithms in Bioinformatics — 12th International Workshop (WABI 2012)*,
|
|
154
|
+
> Lecture Notes in Computer Science, vol. 7534, pp. 201–213. Springer, 2012.
|
|
155
|
+
> [doi:10.1007/978-3-642-33122-0_16](https://doi.org/10.1007/978-3-642-33122-0_16)
|
|
156
|
+
|
|
157
|
+
```bibtex
|
|
158
|
+
@inproceedings{tabei2012succinct,
|
|
159
|
+
author = {Yasuo Tabei},
|
|
160
|
+
title = {Succinct Multibit Tree: Compact Representation of Multibit Trees
|
|
161
|
+
by Using Succinct Data Structures in Chemical Fingerprint
|
|
162
|
+
Searches},
|
|
163
|
+
booktitle = {Algorithms in Bioinformatics -- 12th International Workshop,
|
|
164
|
+
{WABI} 2012, Ljubljana, Slovenia},
|
|
165
|
+
series = {Lecture Notes in Computer Science},
|
|
166
|
+
volume = {7534},
|
|
167
|
+
pages = {201--213},
|
|
168
|
+
publisher = {Springer},
|
|
169
|
+
year = {2012},
|
|
170
|
+
doi = {10.1007/978-3-642-33122-0_16}
|
|
171
|
+
}
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
## License
|
|
175
|
+
|
|
176
|
+
MIT (Yasuo Tabei). Bundles the vendored **rsdic** rank/select library
|
|
177
|
+
(BSD-3-Clause, © 2012 Daisuke Okanohara) and a variable-byte coder (MIT, © 2009
|
|
178
|
+
Daisuke Okanohara). See [LICENSE](LICENSE) for full third-party notices.
|
smbt-0.1.0/README.md
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
# smbt — Succinct Multibit Tree
|
|
2
|
+
|
|
3
|
+
Fast Jaccard/Tanimoto similarity search over chemical fingerprints, backed by
|
|
4
|
+
succinct data structures. `smbt` indexes large fingerprint databases in little
|
|
5
|
+
memory and, given a query fingerprint, returns every database entry whose
|
|
6
|
+
Jaccard (Tanimoto) similarity is at or above a threshold.
|
|
7
|
+
|
|
8
|
+
It is a Python binding around the original C++ implementation from
|
|
9
|
+
[Tabei, WABI 2012](https://doi.org/10.1007/978-3-642-33122-0_16) (see
|
|
10
|
+
[Citation](#citation)).
|
|
11
|
+
|
|
12
|
+
## Installation
|
|
13
|
+
|
|
14
|
+
```sh
|
|
15
|
+
pip install smbt
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Wheels are provided for macOS (x86_64/arm64) and Linux (x86_64/aarch64) on
|
|
19
|
+
CPython 3.9+. Other platforms build from the source distribution and need a
|
|
20
|
+
C++14 compiler. Windows is not supported yet.
|
|
21
|
+
|
|
22
|
+
## Quick start
|
|
23
|
+
|
|
24
|
+
```python
|
|
25
|
+
import smbt
|
|
26
|
+
|
|
27
|
+
# Each fingerprint is a set of uint32 item ids (e.g. "on" bits of a
|
|
28
|
+
# structural fingerprint). Duplicate ids within a fingerprint are ignored.
|
|
29
|
+
data = [
|
|
30
|
+
[1, 5, 9],
|
|
31
|
+
[2, 5, 8],
|
|
32
|
+
[1, 5, 9, 12],
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
idx = smbt.build(data, mode=1, minsup=10)
|
|
36
|
+
|
|
37
|
+
# Every database fingerprint at similarity >= 0.5 to the query,
|
|
38
|
+
# as (id, similarity) sorted by similarity descending then id ascending.
|
|
39
|
+
idx.search([1, 5, 9], similarity=0.5)
|
|
40
|
+
# -> [(0, 1.0), (2, 0.75)]
|
|
41
|
+
|
|
42
|
+
idx.save("db.smbt")
|
|
43
|
+
smbt.load("db.smbt").search([1, 5, 9], similarity=0.9)
|
|
44
|
+
# -> [(0, 1.0)]
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
`id` is the 0-based position of the fingerprint in the input. `similarity` is
|
|
48
|
+
the exact Jaccard/Tanimoto coefficient.
|
|
49
|
+
|
|
50
|
+
Build straight from a file (one whitespace-separated fingerprint per line):
|
|
51
|
+
|
|
52
|
+
```python
|
|
53
|
+
idx = smbt.build_file("fingerprints.dat", mode=1, minsup=10)
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## API
|
|
57
|
+
|
|
58
|
+
| Call | Description |
|
|
59
|
+
|------|-------------|
|
|
60
|
+
| `smbt.build(data, mode=1, minsup=10, verbose=False)` | Build from a list of fingerprints (each a list of ints). |
|
|
61
|
+
| `smbt.build_file(path, mode=1, minsup=10, verbose=False)` | Build from a fingerprint file. |
|
|
62
|
+
| `smbt.load(path)` | Load an index written by `Index.save`. |
|
|
63
|
+
| `Index.search(query, similarity=0.9)` | `[(id, similarity), ...]` above the threshold. |
|
|
64
|
+
| `Index.save(path)` | Persist the index. |
|
|
65
|
+
| `Index.mode` / `Index.size_in_bytes()` | Mode (1/2/3) and approximate index size. |
|
|
66
|
+
|
|
67
|
+
`mode` selects the representation; **all three return identical results**:
|
|
68
|
+
|
|
69
|
+
- **1** — succinct multibit tree + succinct trie (smallest index)
|
|
70
|
+
- **2** — succinct multibit tree + variable-length array
|
|
71
|
+
- **3** — pointer-based multibit tree (uncompressed baseline)
|
|
72
|
+
|
|
73
|
+
`minsup` is the minimum number of fingerprints per leaf during construction; it
|
|
74
|
+
affects index size and speed, not the result set.
|
|
75
|
+
|
|
76
|
+
`search()` releases the GIL and is safe to call concurrently from multiple
|
|
77
|
+
threads on the same index. `build`, `save`, and `load` are not thread-safe.
|
|
78
|
+
|
|
79
|
+
Errors map to Python exceptions: invalid `mode`/`similarity`, an empty dataset,
|
|
80
|
+
an empty fingerprint, or an empty query raise `ValueError`; an unreadable,
|
|
81
|
+
truncated, or unrecognized index raises `smbt.Error`.
|
|
82
|
+
|
|
83
|
+
## Command-line tools
|
|
84
|
+
|
|
85
|
+
The original CLI is also available:
|
|
86
|
+
|
|
87
|
+
```sh
|
|
88
|
+
smbt-build -mode 1 -minsup 10 fingerprints.dat db.smbt
|
|
89
|
+
smbt-search -similarity 0.9 db.smbt queries.dat
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Options must precede the positional arguments. Input format: one fingerprint
|
|
93
|
+
per line, whitespace-separated uint32 item ids (treated as a set; blank lines
|
|
94
|
+
are skipped).
|
|
95
|
+
|
|
96
|
+
## Constraints
|
|
97
|
+
|
|
98
|
+
- **64-bit only.** Wheels are built for 64-bit platforms (x86_64, arm64); 32-bit
|
|
99
|
+
is not supported (the format and the algorithm assume a 64-bit `size_t`).
|
|
100
|
+
- **Index files are not portable across architectures.** They are a raw binary
|
|
101
|
+
dump assuming 64-bit, same-endianness, same `size_t` width. Build and read an
|
|
102
|
+
index on the same kind of machine (x86_64 and arm64 are both little-endian, so
|
|
103
|
+
this is rarely an issue in practice).
|
|
104
|
+
- **Item ids are treated as a dense dimension.** Very large ids (e.g. 2^31)
|
|
105
|
+
inflate memory; map your fingerprint bits to a compact id range first.
|
|
106
|
+
- Indexes written by pre-0.1 builds (format v1) are not readable; rebuild them.
|
|
107
|
+
- In-memory `build` treats an empty fingerprint as an error, whereas the file
|
|
108
|
+
path skips blank lines. A blank-line-free file and the equivalent in-memory
|
|
109
|
+
data produce a byte-identical index.
|
|
110
|
+
|
|
111
|
+
## How it works
|
|
112
|
+
|
|
113
|
+
Fingerprints are grouped by cardinality; each group is indexed by one binary
|
|
114
|
+
tree built by recursively splitting on the item that maximizes entropy. Internal
|
|
115
|
+
nodes record the columns that are all-ones / all-zeros within their subtree;
|
|
116
|
+
accumulating these gives an admissible Jaccard upper bound for branch-and-bound
|
|
117
|
+
pruning, and leaves compute the exact Jaccard. Because pruning is admissible and
|
|
118
|
+
leaves are exact, the result set is independent of the tree shape — which is why
|
|
119
|
+
all three modes agree. Modes 1 and 2 encode the trees and the fingerprint store
|
|
120
|
+
with succinct data structures (a vendored rank/select dictionary) to shrink the
|
|
121
|
+
index; mode 3 is the uncompressed pointer-based baseline.
|
|
122
|
+
|
|
123
|
+
## Citation
|
|
124
|
+
|
|
125
|
+
If you use this software in academic work, please cite:
|
|
126
|
+
|
|
127
|
+
> Yasuo Tabei. "Succinct Multibit Tree: Compact Representation of Multibit Trees
|
|
128
|
+
> by Using Succinct Data Structures in Chemical Fingerprint Searches." In
|
|
129
|
+
> *Algorithms in Bioinformatics — 12th International Workshop (WABI 2012)*,
|
|
130
|
+
> Lecture Notes in Computer Science, vol. 7534, pp. 201–213. Springer, 2012.
|
|
131
|
+
> [doi:10.1007/978-3-642-33122-0_16](https://doi.org/10.1007/978-3-642-33122-0_16)
|
|
132
|
+
|
|
133
|
+
```bibtex
|
|
134
|
+
@inproceedings{tabei2012succinct,
|
|
135
|
+
author = {Yasuo Tabei},
|
|
136
|
+
title = {Succinct Multibit Tree: Compact Representation of Multibit Trees
|
|
137
|
+
by Using Succinct Data Structures in Chemical Fingerprint
|
|
138
|
+
Searches},
|
|
139
|
+
booktitle = {Algorithms in Bioinformatics -- 12th International Workshop,
|
|
140
|
+
{WABI} 2012, Ljubljana, Slovenia},
|
|
141
|
+
series = {Lecture Notes in Computer Science},
|
|
142
|
+
volume = {7534},
|
|
143
|
+
pages = {201--213},
|
|
144
|
+
publisher = {Springer},
|
|
145
|
+
year = {2012},
|
|
146
|
+
doi = {10.1007/978-3-642-33122-0_16}
|
|
147
|
+
}
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
## License
|
|
151
|
+
|
|
152
|
+
MIT (Yasuo Tabei). Bundles the vendored **rsdic** rank/select library
|
|
153
|
+
(BSD-3-Clause, © 2012 Daisuke Okanohara) and a variable-byte coder (MIT, © 2009
|
|
154
|
+
Daisuke Okanohara). See [LICENSE](LICENSE) for full third-party notices.
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* smbt_core.cpp — pybind11 bindings for the Succinct Multibit Tree.
|
|
3
|
+
* Copyright (c) 2013-2026 Yasuo Tabei. MIT License (see LICENSE).
|
|
4
|
+
*
|
|
5
|
+
* Exposes a single Python type, smbt._core.Index, that wraps the three C++
|
|
6
|
+
* implementations (mode 1 = trie, 2 = vla, 3 = mbt) behind one interface.
|
|
7
|
+
* The 8-byte format tag (mode + 10) that the CLI writes/reads outside the
|
|
8
|
+
* classes is handled here in save()/load(), mirroring build.cpp/search.cpp.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
#include <pybind11/pybind11.h>
|
|
12
|
+
#include <pybind11/stl.h>
|
|
13
|
+
|
|
14
|
+
#include <algorithm>
|
|
15
|
+
#include <fstream>
|
|
16
|
+
#include <memory>
|
|
17
|
+
#include <sstream>
|
|
18
|
+
#include <stdexcept>
|
|
19
|
+
#include <string>
|
|
20
|
+
#include <utility>
|
|
21
|
+
#include <vector>
|
|
22
|
+
|
|
23
|
+
#include "lib/error.hpp"
|
|
24
|
+
#include "smbt_trie/succinct_multibit_tree_trie.hpp"
|
|
25
|
+
#include "smbt_vla/succinct_multibit_tree_vla.hpp"
|
|
26
|
+
#include "mbt/multibit_tree.hpp"
|
|
27
|
+
|
|
28
|
+
namespace py = pybind11;
|
|
29
|
+
|
|
30
|
+
namespace {
|
|
31
|
+
|
|
32
|
+
typedef std::vector<std::vector<uint32_t> > FpData;
|
|
33
|
+
typedef std::vector<std::pair<uint64_t, double> > Hits; // (db id, similarity)
|
|
34
|
+
|
|
35
|
+
void validate_mode(int mode) {
|
|
36
|
+
if (mode != 1 && mode != 2 && mode != 3)
|
|
37
|
+
throw std::invalid_argument("mode must be 1, 2 or 3");
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
void validate_similarity(double similarity) {
|
|
41
|
+
if (similarity < 0.0 || similarity > 1.0)
|
|
42
|
+
throw std::invalid_argument("similarity must be in [0, 1]");
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Coerce any path-like (str, os.PathLike, pathlib.Path) to a std::string.
|
|
46
|
+
// Must be called with the GIL held (it invokes os.fspath).
|
|
47
|
+
std::string to_path(const py::object &pathobj) {
|
|
48
|
+
py::object s = py::module_::import("os").attr("fspath")(pathobj);
|
|
49
|
+
return s.cast<std::string>();
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Sort hits into a deterministic order: similarity descending, ties by id
|
|
53
|
+
// ascending. Jaccard here is a ratio of integer counts, so it is bit-stable
|
|
54
|
+
// across the three modes and the resulting order is identical for all.
|
|
55
|
+
void sort_hits(Hits &hits) {
|
|
56
|
+
struct Cmp {
|
|
57
|
+
bool operator()(const std::pair<uint64_t, double> &a,
|
|
58
|
+
const std::pair<uint64_t, double> &b) const {
|
|
59
|
+
if (a.second != b.second) return a.second > b.second;
|
|
60
|
+
return a.first < b.first;
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
std::sort(hits.begin(), hits.end(), Cmp());
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
class Index {
|
|
67
|
+
public:
|
|
68
|
+
// --- factories -----------------------------------------------------------
|
|
69
|
+
static Index *build(const FpData &data, int mode, uint64_t minsup, bool verbose) {
|
|
70
|
+
validate_mode(mode);
|
|
71
|
+
std::unique_ptr<Index> self(new Index(mode));
|
|
72
|
+
if (mode == 1) { self->trie_->set_verbose(verbose); self->trie_->build_from_data(data, minsup); }
|
|
73
|
+
else if (mode == 2) { self->vla_->set_verbose(verbose); self->vla_->build_from_data(data, minsup); }
|
|
74
|
+
else { self->mbt_->set_verbose(verbose); self->mbt_->build_from_data(data, minsup); }
|
|
75
|
+
return self.release();
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
static Index *build_file(const py::object &pathobj, int mode, uint64_t minsup, bool verbose) {
|
|
79
|
+
validate_mode(mode);
|
|
80
|
+
const std::string path = to_path(pathobj);
|
|
81
|
+
py::gil_scoped_release nogil;
|
|
82
|
+
std::unique_ptr<Index> self(new Index(mode));
|
|
83
|
+
if (mode == 1) { self->trie_->set_verbose(verbose); self->trie_->build(path.c_str(), minsup); }
|
|
84
|
+
else if (mode == 2) { self->vla_->set_verbose(verbose); self->vla_->build(path.c_str(), minsup); }
|
|
85
|
+
else { self->mbt_->set_verbose(verbose); self->mbt_->build(path.c_str(), minsup); }
|
|
86
|
+
return self.release();
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
static Index *load(const py::object &pathobj) {
|
|
90
|
+
const std::string path = to_path(pathobj);
|
|
91
|
+
py::gil_scoped_release nogil;
|
|
92
|
+
std::ifstream ifs(path.c_str(), std::ios::in | std::ios::binary);
|
|
93
|
+
if (!ifs)
|
|
94
|
+
throw smbt::Error("cannot open: " + path);
|
|
95
|
+
uint64_t tag = 0;
|
|
96
|
+
ifs.read((char *)&tag, sizeof(tag));
|
|
97
|
+
if (!ifs)
|
|
98
|
+
throw smbt::Error("error: corrupt or truncated index file: " + path);
|
|
99
|
+
|
|
100
|
+
int mode;
|
|
101
|
+
if (tag == 11) mode = 1;
|
|
102
|
+
else if (tag == 12) mode = 2;
|
|
103
|
+
else if (tag == 13) mode = 3;
|
|
104
|
+
else if (tag == 1 || tag == 2 || tag == 3)
|
|
105
|
+
throw smbt::Error("error: " + path +
|
|
106
|
+
" is an old index format (v1) — rebuild the index with smbt-build.");
|
|
107
|
+
else {
|
|
108
|
+
std::ostringstream oss;
|
|
109
|
+
oss << "error: " << path << " is not a smbt index file (unknown format tag " << tag << ").";
|
|
110
|
+
throw smbt::Error(oss.str());
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
std::unique_ptr<Index> self(new Index(mode));
|
|
114
|
+
if (mode == 1) self->trie_->load(ifs);
|
|
115
|
+
else if (mode == 2) self->vla_->load(ifs);
|
|
116
|
+
else self->mbt_->load(ifs);
|
|
117
|
+
return self.release();
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// --- persistence ---------------------------------------------------------
|
|
121
|
+
void save(const py::object &pathobj) const {
|
|
122
|
+
const std::string path = to_path(pathobj);
|
|
123
|
+
py::gil_scoped_release nogil;
|
|
124
|
+
std::ofstream ofs(path.c_str(), std::ios::out | std::ios::binary | std::ios::trunc);
|
|
125
|
+
if (!ofs)
|
|
126
|
+
throw smbt::Error("cannot open: " + path);
|
|
127
|
+
const uint64_t tag = (uint64_t)mode_ + 10;
|
|
128
|
+
ofs.write((const char *)&tag, sizeof(tag));
|
|
129
|
+
if (mode_ == 1) trie_->save(ofs);
|
|
130
|
+
else if (mode_ == 2) vla_->save(ofs);
|
|
131
|
+
else mbt_->save(ofs);
|
|
132
|
+
ofs.flush();
|
|
133
|
+
if (!ofs)
|
|
134
|
+
throw smbt::Error("error writing index file: " + path);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// --- query ---------------------------------------------------------------
|
|
138
|
+
// Not const: the underlying search_fv/search_query are not marked const,
|
|
139
|
+
// but they only read the succinct structures, so releasing the GIL and
|
|
140
|
+
// running searches concurrently is data-race-free.
|
|
141
|
+
Hits search(const std::vector<uint32_t> &query, double similarity) {
|
|
142
|
+
validate_similarity(similarity);
|
|
143
|
+
Hits hits;
|
|
144
|
+
if (mode_ == 1) {
|
|
145
|
+
std::vector<std::pair<float, uint32_t> > res;
|
|
146
|
+
trie_->search_fv(query, (float)similarity, res);
|
|
147
|
+
for (size_t i = 0; i < res.size(); ++i)
|
|
148
|
+
hits.push_back(std::make_pair((uint64_t)res[i].second, (double)res[i].first));
|
|
149
|
+
} else if (mode_ == 2) {
|
|
150
|
+
std::vector<std::pair<float, uint32_t> > res;
|
|
151
|
+
vla_->search_fv(query, (float)similarity, res);
|
|
152
|
+
for (size_t i = 0; i < res.size(); ++i)
|
|
153
|
+
hits.push_back(std::make_pair((uint64_t)res[i].second, (double)res[i].first));
|
|
154
|
+
} else {
|
|
155
|
+
std::vector<std::pair<float, uint64_t> > res;
|
|
156
|
+
mbt_->search_fv(query, (float)similarity, res);
|
|
157
|
+
for (size_t i = 0; i < res.size(); ++i)
|
|
158
|
+
hits.push_back(std::make_pair(res[i].second, (double)res[i].first));
|
|
159
|
+
}
|
|
160
|
+
sort_hits(hits);
|
|
161
|
+
return hits;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
int mode() const { return mode_; }
|
|
165
|
+
|
|
166
|
+
uint64_t size_in_bytes() {
|
|
167
|
+
if (mode_ == 1) return trie_->size_in_bytes();
|
|
168
|
+
if (mode_ == 2) return vla_->size_in_bytes();
|
|
169
|
+
return mbt_->size_in_bytes();
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
private:
|
|
173
|
+
explicit Index(int mode) : mode_(mode) {
|
|
174
|
+
if (mode_ == 1) trie_.reset(new smbt::trie::SuccinctMultibitTreeTRIE());
|
|
175
|
+
else if (mode_ == 2) vla_.reset(new smbt::vla::SuccinctMultibitTreeVLA());
|
|
176
|
+
else mbt_.reset(new smbt::mbt::MultibitTree());
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
int mode_;
|
|
180
|
+
std::unique_ptr<smbt::trie::SuccinctMultibitTreeTRIE> trie_;
|
|
181
|
+
std::unique_ptr<smbt::vla::SuccinctMultibitTreeVLA> vla_;
|
|
182
|
+
std::unique_ptr<smbt::mbt::MultibitTree> mbt_;
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
} // namespace
|
|
186
|
+
|
|
187
|
+
PYBIND11_MODULE(_core, m) {
|
|
188
|
+
m.doc() = "Succinct Multibit Tree: Jaccard/Tanimoto similarity search over fingerprints.";
|
|
189
|
+
|
|
190
|
+
py::register_exception<smbt::Error>(m, "Error", PyExc_RuntimeError);
|
|
191
|
+
|
|
192
|
+
py::class_<Index>(m, "Index",
|
|
193
|
+
"A built (or loaded) similarity-search index. Build with "
|
|
194
|
+
"Index(fingerprints, mode=..., minsup=...), Index.from_file(path, ...), "
|
|
195
|
+
"or Index.load(path). Not thread-safe for build/save/load, but "
|
|
196
|
+
"search() releases the GIL and is safe to call concurrently.")
|
|
197
|
+
.def(py::init(&Index::build),
|
|
198
|
+
py::arg("fingerprints"),
|
|
199
|
+
py::arg("mode") = 1,
|
|
200
|
+
py::arg("minsup") = 10,
|
|
201
|
+
py::arg("verbose") = false,
|
|
202
|
+
py::call_guard<py::gil_scoped_release>(),
|
|
203
|
+
"Build an index from a list of fingerprints (each a list of uint32 "
|
|
204
|
+
"item ids). An empty fingerprint raises ValueError.")
|
|
205
|
+
.def_static("from_file", &Index::build_file,
|
|
206
|
+
py::arg("path"),
|
|
207
|
+
py::arg("mode") = 1,
|
|
208
|
+
py::arg("minsup") = 10,
|
|
209
|
+
py::arg("verbose") = false,
|
|
210
|
+
"Build an index from a fingerprint file (one whitespace-separated "
|
|
211
|
+
"line per fingerprint; blank lines are skipped).")
|
|
212
|
+
.def_static("load", &Index::load,
|
|
213
|
+
py::arg("path"),
|
|
214
|
+
"Load an index previously written by save(). Raises smbt.Error for "
|
|
215
|
+
"a v1 index (rebuild required) or an unrecognized file.")
|
|
216
|
+
.def("save", &Index::save,
|
|
217
|
+
py::arg("path"),
|
|
218
|
+
"Write the index to a file, tagged with its mode.")
|
|
219
|
+
.def("search", &Index::search,
|
|
220
|
+
py::arg("query"),
|
|
221
|
+
py::arg("similarity") = 0.9,
|
|
222
|
+
py::call_guard<py::gil_scoped_release>(),
|
|
223
|
+
"Return [(db_id, similarity), ...] for every database fingerprint whose "
|
|
224
|
+
"Jaccard/Tanimoto similarity to the query is >= similarity, sorted by "
|
|
225
|
+
"similarity descending then id ascending. db_id is the 0-based input "
|
|
226
|
+
"index of the fingerprint. An empty query raises ValueError.")
|
|
227
|
+
.def_property_readonly("mode", &Index::mode, "The index mode (1, 2 or 3).")
|
|
228
|
+
.def("size_in_bytes", &Index::size_in_bytes,
|
|
229
|
+
"Approximate size of the succinct multibit tree(s), in bytes.")
|
|
230
|
+
.def("__repr__", [](Index &self) {
|
|
231
|
+
std::ostringstream oss;
|
|
232
|
+
oss << "<smbt.Index mode=" << self.mode() << ">";
|
|
233
|
+
return oss.str();
|
|
234
|
+
});
|
|
235
|
+
}
|