quantui 0.5.1__py3-none-any.whl
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.
- quantui/__init__.py +311 -0
- quantui/analytics.py +609 -0
- quantui/app.py +5650 -0
- quantui/app_analysis.py +662 -0
- quantui/app_builders.py +2465 -0
- quantui/app_exports.py +194 -0
- quantui/app_formatters.py +493 -0
- quantui/app_history.py +624 -0
- quantui/app_runflow.py +1544 -0
- quantui/app_visualization.py +2620 -0
- quantui/ase_bridge.py +236 -0
- quantui/benchmarks.py +1543 -0
- quantui/c_stderr.py +124 -0
- quantui/cactus.py +88 -0
- quantui/calc_log.py +1116 -0
- quantui/calculator.py +204 -0
- quantui/cancellation.py +88 -0
- quantui/cli.py +288 -0
- quantui/comparison.py +306 -0
- quantui/config.py +725 -0
- quantui/data/js/3Dmol-min.js +2 -0
- quantui/data/js/3Dmol-min.js.LICENSE.txt +5 -0
- quantui/data/library/library.sqlite +0 -0
- quantui/data/manifests/bulk_qm9.json +1 -0
- quantui/data/manifests/curated.json +15482 -0
- quantui/data/manifests/presets.json +816 -0
- quantui/descriptor_cards.py +186 -0
- quantui/freq_calc.py +712 -0
- quantui/freq_ir_workers.py +229 -0
- quantui/gpu_offload.py +278 -0
- quantui/help_content.py +474 -0
- quantui/ir_plot.py +130 -0
- quantui/issue_tracker.py +170 -0
- quantui/live_log.py +387 -0
- quantui/log_utils.py +492 -0
- quantui/molecule.py +577 -0
- quantui/molecule_library.py +433 -0
- quantui/nmr_calc.py +437 -0
- quantui/optimizer.py +670 -0
- quantui/orbital_visualization.py +1102 -0
- quantui/pes_scan.py +420 -0
- quantui/preopt.py +355 -0
- quantui/progress.py +111 -0
- quantui/pubchem.py +1157 -0
- quantui/reorganization_energy.py +435 -0
- quantui/results_storage.py +902 -0
- quantui/security.py +14 -0
- quantui/session_calc.py +622 -0
- quantui/structure_providers.py +277 -0
- quantui/tddft_calc.py +307 -0
- quantui/user_settings.py +238 -0
- quantui/utils.py +287 -0
- quantui/vib_cache.py +247 -0
- quantui/visualization_py3dmol.py +593 -0
- quantui/viz_assets.py +101 -0
- quantui/viz_backend_router.py +243 -0
- quantui-0.5.1.dist-info/METADATA +533 -0
- quantui-0.5.1.dist-info/RECORD +62 -0
- quantui-0.5.1.dist-info/WHEEL +5 -0
- quantui-0.5.1.dist-info/entry_points.txt +2 -0
- quantui-0.5.1.dist-info/licenses/LICENSE +21 -0
- quantui-0.5.1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,433 @@
|
|
|
1
|
+
"""Bundled molecule library — indexed package-data store + lazy loader.
|
|
2
|
+
|
|
3
|
+
The library lives in ``quantui/data/`` as two committed artifacts:
|
|
4
|
+
|
|
5
|
+
- ``manifests/presets.json`` — the human-readable, reviewable source of record.
|
|
6
|
+
- ``library/library.sqlite`` — an indexed, lazily-queried store generated from
|
|
7
|
+
the manifest(s). Coordinates are packed as ``int16`` @ 0.001 Å (7–8 bytes per
|
|
8
|
+
atom) so the store stays compact enough to hold the ~10 MB hybrid library
|
|
9
|
+
without bloating import time.
|
|
10
|
+
|
|
11
|
+
Design goals:
|
|
12
|
+
|
|
13
|
+
- **Lazy + indexed** — search/get touch only the rows they need, so the store
|
|
14
|
+
scales to tens of thousands of bulk entries without loading them all.
|
|
15
|
+
- **Back-compat** — :func:`get_preset_dict` returns the exact legacy
|
|
16
|
+
``MOLECULE_LIBRARY`` shape, so ``config.MOLECULE_LIBRARY`` and every existing
|
|
17
|
+
consumer keep working unchanged.
|
|
18
|
+
- **Robust** — if the SQLite artifact is missing or unreadable, every entry
|
|
19
|
+
point transparently falls back to the JSON manifest, so ``import quantui``
|
|
20
|
+
never fails on a fresh/odd checkout.
|
|
21
|
+
|
|
22
|
+
Adding curated or bulk content only adds manifests + rebuilds the store;
|
|
23
|
+
nothing in this module's public API changes.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
import json
|
|
27
|
+
import logging
|
|
28
|
+
import sqlite3
|
|
29
|
+
import struct
|
|
30
|
+
from functools import lru_cache
|
|
31
|
+
from importlib.resources import files
|
|
32
|
+
from pathlib import Path
|
|
33
|
+
from typing import Any, Dict, List, Optional, Tuple
|
|
34
|
+
|
|
35
|
+
logger = logging.getLogger(__name__)
|
|
36
|
+
|
|
37
|
+
# ── Coordinate codec ─────────────────────────────────────────────────────────
|
|
38
|
+
# Per-atom record: 2 bytes element symbol (ascii, 2nd byte 0 if 1-char) +
|
|
39
|
+
# 3× int16 little-endian coordinate in milli-ångström. 8 bytes/atom.
|
|
40
|
+
_COORD_SCALE = 1000 # 0.001 Å resolution; ±32.767 Å range (int16)
|
|
41
|
+
_ATOM_RECORD = "<BBhhh"
|
|
42
|
+
_ATOM_RECORD_SIZE = struct.calcsize(_ATOM_RECORD) # 8
|
|
43
|
+
|
|
44
|
+
# Categories that are NOT part of the browsable preset dict (reached via search
|
|
45
|
+
# only). Bulk QM9 entries carry "bulk-qm9".
|
|
46
|
+
_BULK_CATEGORIES = frozenset({"bulk-qm9"})
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def encode_coords(atoms: List[str], coords: List[List[float]]) -> bytes:
|
|
50
|
+
"""Pack ``(atoms, coords)`` into the compact per-atom binary record."""
|
|
51
|
+
buf = bytearray()
|
|
52
|
+
for sym, (x, y, z) in zip(atoms, coords):
|
|
53
|
+
s = sym.encode("ascii")
|
|
54
|
+
if not 1 <= len(s) <= 2:
|
|
55
|
+
raise ValueError(f"Unsupported element symbol: {sym!r}")
|
|
56
|
+
c0 = s[0]
|
|
57
|
+
c1 = s[1] if len(s) > 1 else 0
|
|
58
|
+
buf += struct.pack(
|
|
59
|
+
_ATOM_RECORD,
|
|
60
|
+
c0,
|
|
61
|
+
c1,
|
|
62
|
+
round(float(x) * _COORD_SCALE),
|
|
63
|
+
round(float(y) * _COORD_SCALE),
|
|
64
|
+
round(float(z) * _COORD_SCALE),
|
|
65
|
+
)
|
|
66
|
+
return bytes(buf)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def decode_coords(blob: bytes) -> Tuple[List[str], List[List[float]]]:
|
|
70
|
+
"""Unpack a coordinate blob back into ``(atoms, coords)``."""
|
|
71
|
+
atoms: List[str] = []
|
|
72
|
+
coords: List[List[float]] = []
|
|
73
|
+
for offset in range(0, len(blob), _ATOM_RECORD_SIZE):
|
|
74
|
+
c0, c1, xi, yi, zi = struct.unpack(
|
|
75
|
+
_ATOM_RECORD, blob[offset : offset + _ATOM_RECORD_SIZE]
|
|
76
|
+
)
|
|
77
|
+
sym = chr(c0) + (chr(c1) if c1 else "")
|
|
78
|
+
atoms.append(sym)
|
|
79
|
+
coords.append([xi / _COORD_SCALE, yi / _COORD_SCALE, zi / _COORD_SCALE])
|
|
80
|
+
return atoms, coords
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
# ── Paths ────────────────────────────────────────────────────────────────────
|
|
84
|
+
def _data_dir() -> Path:
|
|
85
|
+
return Path(str(files("quantui"))) / "data"
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def db_path() -> Path:
|
|
89
|
+
return _data_dir() / "library" / "library.sqlite"
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def manifest_paths() -> List[Path]:
|
|
93
|
+
"""All manifest JSON files that seed the store.
|
|
94
|
+
|
|
95
|
+
``presets.json`` is ordered first so the original teaching molecules lead
|
|
96
|
+
the (interim) flat dropdown; remaining manifests follow alphabetically.
|
|
97
|
+
"""
|
|
98
|
+
manifest_dir = _data_dir() / "manifests"
|
|
99
|
+
if not manifest_dir.is_dir():
|
|
100
|
+
return []
|
|
101
|
+
return sorted(
|
|
102
|
+
manifest_dir.glob("*.json"),
|
|
103
|
+
key=lambda p: (p.name != "presets.json", p.name),
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
# ── Manifest loading (always-available fallback source) ──────────────────────
|
|
108
|
+
@lru_cache(maxsize=1)
|
|
109
|
+
def _manifest_entries() -> Tuple[Dict[str, Any], ...]:
|
|
110
|
+
"""Load every manifest entry. Cached. The JSON is the source of record."""
|
|
111
|
+
entries: List[Dict[str, Any]] = []
|
|
112
|
+
for path in manifest_paths():
|
|
113
|
+
try:
|
|
114
|
+
entries.extend(json.loads(path.read_text(encoding="utf-8")))
|
|
115
|
+
except Exception as exc: # pragma: no cover - corrupt manifest
|
|
116
|
+
logger.error(f"Failed to read library manifest {path}: {exc}")
|
|
117
|
+
return tuple(entries)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def _normalize_entry(raw: Dict[str, Any]) -> Dict[str, Any]:
|
|
121
|
+
"""Fill derived fields (id/formula/n_atoms/...) for a manifest entry."""
|
|
122
|
+
atoms = raw["atoms"]
|
|
123
|
+
formula = raw.get("formula") or raw.get("id") or raw["name"]
|
|
124
|
+
return {
|
|
125
|
+
"id": raw.get("id", formula),
|
|
126
|
+
"name": raw.get("name", formula),
|
|
127
|
+
"formula": formula,
|
|
128
|
+
"category": raw.get("category", "preset"),
|
|
129
|
+
"n_atoms": len(atoms),
|
|
130
|
+
"n_heavy": sum(1 for a in atoms if a != "H"),
|
|
131
|
+
"charge": int(raw.get("charge", 0)),
|
|
132
|
+
"multiplicity": int(raw.get("multiplicity", 1)),
|
|
133
|
+
"source": raw.get("source", "preset"),
|
|
134
|
+
"smiles": raw.get("smiles"),
|
|
135
|
+
"inchikey": raw.get("inchikey"),
|
|
136
|
+
"synonyms": raw.get("synonyms", ""),
|
|
137
|
+
"description": raw.get("description", ""),
|
|
138
|
+
"atoms": atoms,
|
|
139
|
+
"coordinates": raw["coordinates"],
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
# ── Store build (run by scripts/build_library.py or on-demand) ───────────────
|
|
144
|
+
_SCHEMA = """
|
|
145
|
+
CREATE TABLE molecule (
|
|
146
|
+
id TEXT PRIMARY KEY,
|
|
147
|
+
name TEXT,
|
|
148
|
+
formula TEXT NOT NULL,
|
|
149
|
+
category TEXT NOT NULL,
|
|
150
|
+
n_heavy INTEGER NOT NULL,
|
|
151
|
+
n_atoms INTEGER NOT NULL,
|
|
152
|
+
charge INTEGER NOT NULL DEFAULT 0,
|
|
153
|
+
multiplicity INTEGER NOT NULL DEFAULT 1,
|
|
154
|
+
source TEXT NOT NULL,
|
|
155
|
+
smiles TEXT,
|
|
156
|
+
inchikey TEXT,
|
|
157
|
+
synonyms TEXT,
|
|
158
|
+
description TEXT,
|
|
159
|
+
coords BLOB NOT NULL
|
|
160
|
+
);
|
|
161
|
+
CREATE INDEX idx_name ON molecule(name);
|
|
162
|
+
CREATE INDEX idx_formula ON molecule(formula);
|
|
163
|
+
CREATE INDEX idx_category ON molecule(category);
|
|
164
|
+
CREATE INDEX idx_inchikey ON molecule(inchikey);
|
|
165
|
+
"""
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def build_store(entries: List[Dict[str, Any]], target: Path) -> Path:
|
|
169
|
+
"""(Re)build the SQLite store from a list of (raw) manifest entries.
|
|
170
|
+
|
|
171
|
+
Deterministic: entries are inserted in the order given, so a rebuild from
|
|
172
|
+
the same manifest yields a stable file. Overwrites ``target``.
|
|
173
|
+
"""
|
|
174
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
175
|
+
if target.exists():
|
|
176
|
+
target.unlink()
|
|
177
|
+
con = sqlite3.connect(target)
|
|
178
|
+
try:
|
|
179
|
+
con.executescript(_SCHEMA)
|
|
180
|
+
rows = []
|
|
181
|
+
for raw in entries:
|
|
182
|
+
e = _normalize_entry(raw)
|
|
183
|
+
rows.append(
|
|
184
|
+
(
|
|
185
|
+
e["id"],
|
|
186
|
+
e["name"],
|
|
187
|
+
e["formula"],
|
|
188
|
+
e["category"],
|
|
189
|
+
e["n_heavy"],
|
|
190
|
+
e["n_atoms"],
|
|
191
|
+
e["charge"],
|
|
192
|
+
e["multiplicity"],
|
|
193
|
+
e["source"],
|
|
194
|
+
e["smiles"],
|
|
195
|
+
e["inchikey"],
|
|
196
|
+
e["synonyms"],
|
|
197
|
+
e["description"],
|
|
198
|
+
encode_coords(e["atoms"], e["coordinates"]),
|
|
199
|
+
)
|
|
200
|
+
)
|
|
201
|
+
con.executemany(
|
|
202
|
+
"INSERT INTO molecule (id, name, formula, category, n_heavy, n_atoms, "
|
|
203
|
+
"charge, multiplicity, source, smiles, inchikey, synonyms, description, "
|
|
204
|
+
"coords) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?)",
|
|
205
|
+
rows,
|
|
206
|
+
)
|
|
207
|
+
con.commit()
|
|
208
|
+
finally:
|
|
209
|
+
con.close()
|
|
210
|
+
logger.info(f"Built molecule library store: {target} ({len(entries)} entries)")
|
|
211
|
+
return target
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def build_from_manifests(target: Optional[Path] = None) -> Path:
|
|
215
|
+
"""Build the store from all committed manifests. Returns the store path."""
|
|
216
|
+
target = target or db_path()
|
|
217
|
+
return build_store([dict(e) for e in _manifest_entries()], target)
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def _ensure_store() -> Optional[Path]:
|
|
221
|
+
"""Best-effort: ensure the SQLite store exists. Returns its path or None.
|
|
222
|
+
|
|
223
|
+
Never raises — if the package dir is read-only (installed wheel) and the
|
|
224
|
+
store is somehow absent, callers fall back to the JSON manifest.
|
|
225
|
+
"""
|
|
226
|
+
path = db_path()
|
|
227
|
+
if path.exists():
|
|
228
|
+
return path
|
|
229
|
+
try:
|
|
230
|
+
if _manifest_entries():
|
|
231
|
+
return build_from_manifests(path)
|
|
232
|
+
except Exception as exc: # pragma: no cover - read-only install w/o store
|
|
233
|
+
logger.warning(f"Could not build library store on demand: {exc}")
|
|
234
|
+
return None
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
# ── Query API ────────────────────────────────────────────────────────────────
|
|
238
|
+
def _connect_ro() -> Optional[sqlite3.Connection]:
|
|
239
|
+
"""Open a fresh read-only connection (thread-safe per-call), or None."""
|
|
240
|
+
path = _ensure_store()
|
|
241
|
+
if path is None or not path.exists():
|
|
242
|
+
return None
|
|
243
|
+
con = sqlite3.connect(f"file:{path}?mode=ro", uri=True)
|
|
244
|
+
con.row_factory = sqlite3.Row
|
|
245
|
+
return con
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def _row_to_entry(row: sqlite3.Row) -> Dict[str, Any]:
|
|
249
|
+
atoms, coords = decode_coords(row["coords"])
|
|
250
|
+
return {
|
|
251
|
+
"id": row["id"],
|
|
252
|
+
"name": row["name"],
|
|
253
|
+
"formula": row["formula"],
|
|
254
|
+
"category": row["category"],
|
|
255
|
+
"n_heavy": row["n_heavy"],
|
|
256
|
+
"n_atoms": row["n_atoms"],
|
|
257
|
+
"charge": row["charge"],
|
|
258
|
+
"multiplicity": row["multiplicity"],
|
|
259
|
+
"source": row["source"],
|
|
260
|
+
"smiles": row["smiles"],
|
|
261
|
+
"inchikey": row["inchikey"],
|
|
262
|
+
"synonyms": row["synonyms"],
|
|
263
|
+
"description": row["description"],
|
|
264
|
+
"atoms": atoms,
|
|
265
|
+
"coordinates": coords,
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
@lru_cache(maxsize=1)
|
|
270
|
+
def get_preset_dict() -> Dict[str, Dict[str, Any]]:
|
|
271
|
+
"""Return the curated/preset entries in the legacy ``MOLECULE_LIBRARY`` shape.
|
|
272
|
+
|
|
273
|
+
Keyed by entry id (the formula key, e.g. ``"H2O"``); each value carries the
|
|
274
|
+
legacy ``atoms`` / ``coordinates`` / ``charge`` / ``multiplicity`` /
|
|
275
|
+
``description`` fields. Bulk categories are excluded (they are reached via
|
|
276
|
+
:func:`search`, never the browse dropdown). Preserves manifest order.
|
|
277
|
+
|
|
278
|
+
Prefers the SQLite store; falls back to the JSON manifest if the store is
|
|
279
|
+
unavailable, so this never hard-fails.
|
|
280
|
+
"""
|
|
281
|
+
out: Dict[str, Dict[str, Any]] = {}
|
|
282
|
+
con = _connect_ro()
|
|
283
|
+
if con is not None:
|
|
284
|
+
try:
|
|
285
|
+
placeholders = ",".join("?" * len(_BULK_CATEGORIES))
|
|
286
|
+
rows = con.execute(
|
|
287
|
+
f"SELECT * FROM molecule WHERE category NOT IN ({placeholders}) "
|
|
288
|
+
"ORDER BY rowid",
|
|
289
|
+
tuple(_BULK_CATEGORIES),
|
|
290
|
+
).fetchall()
|
|
291
|
+
for row in rows:
|
|
292
|
+
e = _row_to_entry(row)
|
|
293
|
+
out[e["id"]] = _legacy_view(e)
|
|
294
|
+
return out
|
|
295
|
+
except Exception as exc: # pragma: no cover - corrupt store
|
|
296
|
+
logger.warning(f"Library store unreadable, using manifest: {exc}")
|
|
297
|
+
finally:
|
|
298
|
+
con.close()
|
|
299
|
+
# JSON fallback.
|
|
300
|
+
for raw in _manifest_entries():
|
|
301
|
+
e = _normalize_entry(raw)
|
|
302
|
+
if e["category"] in _BULK_CATEGORIES:
|
|
303
|
+
continue
|
|
304
|
+
out[e["id"]] = _legacy_view(e)
|
|
305
|
+
return out
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
def _legacy_view(entry: Dict[str, Any]) -> Dict[str, Any]:
|
|
309
|
+
"""Project a full entry down to the legacy MOLECULE_LIBRARY value shape."""
|
|
310
|
+
return {
|
|
311
|
+
"atoms": entry["atoms"],
|
|
312
|
+
"coordinates": entry["coordinates"],
|
|
313
|
+
"charge": entry["charge"],
|
|
314
|
+
"multiplicity": entry["multiplicity"],
|
|
315
|
+
"description": entry["description"],
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
def get(entry_id: str) -> Optional[Dict[str, Any]]:
|
|
320
|
+
"""Fetch one full entry (incl. decoded coordinates) by id, or None."""
|
|
321
|
+
con = _connect_ro()
|
|
322
|
+
if con is not None:
|
|
323
|
+
try:
|
|
324
|
+
row = con.execute(
|
|
325
|
+
"SELECT * FROM molecule WHERE id = ?", (entry_id,)
|
|
326
|
+
).fetchone()
|
|
327
|
+
return _row_to_entry(row) if row else None
|
|
328
|
+
finally:
|
|
329
|
+
con.close()
|
|
330
|
+
for raw in _manifest_entries():
|
|
331
|
+
e = _normalize_entry(raw)
|
|
332
|
+
if e["id"] == entry_id:
|
|
333
|
+
return e
|
|
334
|
+
return None
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
def search(
|
|
338
|
+
query: str = "", *, category: Optional[str] = None, limit: int = 50
|
|
339
|
+
) -> List[Dict[str, Any]]:
|
|
340
|
+
"""Search the library by name / formula / synonyms (substring, case-insensitive).
|
|
341
|
+
|
|
342
|
+
Returns lightweight rows (no coordinates) for listing; call :func:`get` to
|
|
343
|
+
materialize the chosen entry. Empty ``query`` lists entries (optionally
|
|
344
|
+
filtered by ``category``).
|
|
345
|
+
"""
|
|
346
|
+
q = f"%{query.strip().lower()}%"
|
|
347
|
+
con = _connect_ro()
|
|
348
|
+
if con is not None:
|
|
349
|
+
try:
|
|
350
|
+
sql = (
|
|
351
|
+
"SELECT id, name, formula, category, n_heavy, n_atoms, charge, "
|
|
352
|
+
"multiplicity, source FROM molecule WHERE "
|
|
353
|
+
"(lower(name) LIKE ? OR lower(formula) LIKE ? OR lower(synonyms) LIKE ?)"
|
|
354
|
+
)
|
|
355
|
+
params: List[Any] = [q, q, q]
|
|
356
|
+
if category:
|
|
357
|
+
sql += " AND category = ?"
|
|
358
|
+
params.append(category)
|
|
359
|
+
sql += " ORDER BY n_atoms, id LIMIT ?"
|
|
360
|
+
params.append(limit)
|
|
361
|
+
return [dict(r) for r in con.execute(sql, params).fetchall()]
|
|
362
|
+
finally:
|
|
363
|
+
con.close()
|
|
364
|
+
# JSON fallback (linear scan).
|
|
365
|
+
needle = query.strip().lower()
|
|
366
|
+
results = []
|
|
367
|
+
for raw in _manifest_entries():
|
|
368
|
+
e = _normalize_entry(raw)
|
|
369
|
+
hay = f"{e['name']} {e['formula']} {e['synonyms']}".lower()
|
|
370
|
+
if needle in hay and (category is None or e["category"] == category):
|
|
371
|
+
results.append(
|
|
372
|
+
{
|
|
373
|
+
k: e[k]
|
|
374
|
+
for k in (
|
|
375
|
+
"id",
|
|
376
|
+
"name",
|
|
377
|
+
"formula",
|
|
378
|
+
"category",
|
|
379
|
+
"n_heavy",
|
|
380
|
+
"n_atoms",
|
|
381
|
+
"charge",
|
|
382
|
+
"multiplicity",
|
|
383
|
+
"source",
|
|
384
|
+
)
|
|
385
|
+
}
|
|
386
|
+
)
|
|
387
|
+
results.sort(key=lambda r: (r["n_atoms"], r["id"]))
|
|
388
|
+
return results[:limit]
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
def categories() -> List[str]:
|
|
392
|
+
"""Distinct category labels present in the library."""
|
|
393
|
+
con = _connect_ro()
|
|
394
|
+
if con is not None:
|
|
395
|
+
try:
|
|
396
|
+
return [
|
|
397
|
+
r[0]
|
|
398
|
+
for r in con.execute(
|
|
399
|
+
"SELECT DISTINCT category FROM molecule ORDER BY category"
|
|
400
|
+
).fetchall()
|
|
401
|
+
]
|
|
402
|
+
finally:
|
|
403
|
+
con.close()
|
|
404
|
+
return sorted({_normalize_entry(e)["category"] for e in _manifest_entries()})
|
|
405
|
+
|
|
406
|
+
|
|
407
|
+
def count() -> int:
|
|
408
|
+
"""Total number of entries in the library."""
|
|
409
|
+
con = _connect_ro()
|
|
410
|
+
if con is not None:
|
|
411
|
+
try:
|
|
412
|
+
return int(con.execute("SELECT COUNT(*) FROM molecule").fetchone()[0])
|
|
413
|
+
finally:
|
|
414
|
+
con.close()
|
|
415
|
+
return len(_manifest_entries())
|
|
416
|
+
|
|
417
|
+
|
|
418
|
+
def iter_entries():
|
|
419
|
+
"""Yield every entry (full, with decoded coordinates) in store order.
|
|
420
|
+
|
|
421
|
+
Single connection — efficient for whole-library governance/round-trip
|
|
422
|
+
checks. Falls back to the JSON manifest if the store is absent.
|
|
423
|
+
"""
|
|
424
|
+
con = _connect_ro()
|
|
425
|
+
if con is None:
|
|
426
|
+
for raw in _manifest_entries():
|
|
427
|
+
yield _normalize_entry(raw)
|
|
428
|
+
return
|
|
429
|
+
try:
|
|
430
|
+
for row in con.execute("SELECT * FROM molecule ORDER BY rowid"):
|
|
431
|
+
yield _row_to_entry(row)
|
|
432
|
+
finally:
|
|
433
|
+
con.close()
|