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.
Files changed (53) hide show
  1. smbt-0.1.0/LICENSE +72 -0
  2. smbt-0.1.0/MANIFEST.in +19 -0
  3. smbt-0.1.0/PKG-INFO +178 -0
  4. smbt-0.1.0/README.md +154 -0
  5. smbt-0.1.0/bindings/smbt_core.cpp +235 -0
  6. smbt-0.1.0/pyproject.toml +49 -0
  7. smbt-0.1.0/python/smbt/__init__.py +47 -0
  8. smbt-0.1.0/python/smbt.egg-info/PKG-INFO +178 -0
  9. smbt-0.1.0/python/smbt.egg-info/SOURCES.txt +51 -0
  10. smbt-0.1.0/python/smbt.egg-info/dependency_links.txt +1 -0
  11. smbt-0.1.0/python/smbt.egg-info/top_level.txt +1 -0
  12. smbt-0.1.0/setup.cfg +4 -0
  13. smbt-0.1.0/setup.py +40 -0
  14. smbt-0.1.0/src/build.cpp +165 -0
  15. smbt-0.1.0/src/lib/bit_array.cpp +164 -0
  16. smbt-0.1.0/src/lib/bit_array.hpp +62 -0
  17. smbt-0.1.0/src/lib/checked_read.hpp +30 -0
  18. smbt-0.1.0/src/lib/error.hpp +27 -0
  19. smbt-0.1.0/src/lib/suc_array.cpp +101 -0
  20. smbt-0.1.0/src/lib/suc_array.hpp +68 -0
  21. smbt-0.1.0/src/lib/var_byte.cpp +94 -0
  22. smbt-0.1.0/src/lib/var_byte.hpp +58 -0
  23. smbt-0.1.0/src/mbt/multibit_tree.cpp +434 -0
  24. smbt-0.1.0/src/mbt/multibit_tree.hpp +240 -0
  25. smbt-0.1.0/src/rsdic/lib/Const.hpp +36 -0
  26. smbt-0.1.0/src/rsdic/lib/EnumCoder.cpp +264 -0
  27. smbt-0.1.0/src/rsdic/lib/EnumCoder.hpp +53 -0
  28. smbt-0.1.0/src/rsdic/lib/EnumCoderTest.cpp +46 -0
  29. smbt-0.1.0/src/rsdic/lib/RSDic.cpp +182 -0
  30. smbt-0.1.0/src/rsdic/lib/RSDic.hpp +87 -0
  31. smbt-0.1.0/src/rsdic/lib/RSDicBuilder.cpp +111 -0
  32. smbt-0.1.0/src/rsdic/lib/RSDicBuilder.hpp +58 -0
  33. smbt-0.1.0/src/rsdic/lib/RSDicBuilderTest.cpp +76 -0
  34. smbt-0.1.0/src/rsdic/lib/RSDicTest.cpp +157 -0
  35. smbt-0.1.0/src/rsdic/lib/Type.hpp +30 -0
  36. smbt-0.1.0/src/rsdic/lib/Util.hpp +69 -0
  37. smbt-0.1.0/src/rsdic/lib/UtilTest.cpp +55 -0
  38. smbt-0.1.0/src/rsdic/tool/Combination.hpp +35 -0
  39. smbt-0.1.0/src/rsdic/tool/CombinationGenerator.cpp +47 -0
  40. smbt-0.1.0/src/rsdic/tool/CombinationTest.cpp +16 -0
  41. smbt-0.1.0/src/rsdic/tool/PerformanceTest.cpp +102 -0
  42. smbt-0.1.0/src/search.cpp +140 -0
  43. smbt-0.1.0/src/smbt_trie/succinct_multibit_tree_trie.cpp +709 -0
  44. smbt-0.1.0/src/smbt_trie/succinct_multibit_tree_trie.hpp +333 -0
  45. smbt-0.1.0/src/smbt_vla/succinct_multibit_tree_vla.cpp +489 -0
  46. smbt-0.1.0/src/smbt_vla/succinct_multibit_tree_vla.hpp +291 -0
  47. smbt-0.1.0/tests/conftest.py +68 -0
  48. smbt-0.1.0/tests/test_errors.py +74 -0
  49. smbt-0.1.0/tests/test_golden.py +53 -0
  50. smbt-0.1.0/tests/test_inmemory_vs_file.py +32 -0
  51. smbt-0.1.0/tests/test_modes_agree.py +72 -0
  52. smbt-0.1.0/tests/test_save_load.py +25 -0
  53. 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
+ }