search_cif_db_tkcif.py ダウンロード/コピー
search_cif_db_tkcif.py をダウンロード
search_cif_db_tkcif.py
search_cif_db_tkcif.py
1#!/usr/bin/env python
2# -*- coding: utf-8 -*-
3"""
4概要:
5 CIFファイルを再帰的に検索し、その組成情報をSQLite3データベースに格納するツールです。
6 格納されたデータベースを利用して、組成式からCIFデータを検索することができます。
7詳細説明:
8 このスクリプトは、指定されたルートディレクトリ以下のCIFファイルを読み込み、
9 pymatgenとtkcifライブラリを使用して構造と組成情報を解析します。
10 解析された情報はSQLite3データベースに保存され、後で高速に検索できるようになります。
11 特に、組成式、還元組成式、元素のリスト、空間群、格子定数などのメタデータが格納されます。
12
13 主な機能:
14 - indexモード: 指定されたディレクトリからCIFファイルをスキャンし、データベースを構築または更新します。
15 コマンド例:
16 python search_cif_db_tkcif.py --mode index --root ./COD --db cif_index.sqlite
17 - searchモード: データベースに対して、組成式に基づいてCIFデータを検索します。
18 検索モードには、exact (還元組成式が完全に一致)、elements (構成元素が完全に一致)、
19 contains (指定元素が部分集合として含まれる) の3種類のマッチング方式があります。
20 コマンド例:
21 python search_cif_db_tkcif.py --mode search --db cif_index.sqlite --formula BaTiO3
22 python search_cif_db_tkcif.py --mode search --db cif_index.sqlite --formula TiBaO3 --match exact
23 python search_cif_db_tkcif.py --mode search --db cif_index.sqlite --formula BaTiO3 --match elements
24 python search_cif_db_tkcif.py --mode search --db cif_index.sqlite --formula BaTiO3 --match contains
25 - infoモード: データベースの統計情報を表示します。
26 コマンド例:
27 python search_cif_db_tkcif.py --mode info --db cif_index.sqlite
28
29 データベースのカラムは、ソースデータベース名、マテリアルID、CIFファイルのパス、組成式、
30 還元組成式、匿名組成式、JSON形式の組成情報、元素リスト、元素数、サイト数、
31 空間群記号、空間群番号、体積、原子あたりの体積、密度、格子定数 (a, b, c, alpha, beta, gamma)、
32 使用されたバックエンド、正規化の有無、処理ステータス、エラーメッセージを含みます。
33
34関連リンク:
35 search_cif_db_tkcif_usage
36"""
37
38from __future__ import annotations
39
40import argparse
41import json
42import re
43import sqlite3
44import sys
45import traceback
46import warnings
47from pathlib import Path
48from typing import Any
49
50try:
51 from pymatgen.core import Composition
52except Exception:
53 print("ERROR: failed to import pymatgen Composition")
54 traceback.print_exc()
55 print("\nInstall example:")
56 print(" pip install pymatgen")
57 input("\nPress ENTER to terminate>>\n")
58 sys.exit(1)
59
60try:
61 from tkcif.tkcif_reader import read_structure
62except Exception:
63 print("ERROR: failed to import tkcif.tkcif_reader")
64 traceback.print_exc()
65 input("\nPress ENTER to terminate>>\n")
66 sys.exit(1)
67
68# 大量 CIF 処理では pymatgen/ASE の warning が多すぎるので標準では抑制する。
69warnings.simplefilter("ignore")
70
71
72SCHEMA_COLUMNS: dict[str, str] = {
73 "source_db": "TEXT",
74 "material_id": "TEXT",
75 "cif_path": "TEXT UNIQUE",
76 "formula": "TEXT",
77 "reduced_formula": "TEXT",
78 "anonymous_formula": "TEXT",
79 "composition_json": "TEXT",
80 "elements": "TEXT",
81 "nelements": "INTEGER",
82 "nsites": "INTEGER",
83 "sg_symbol": "TEXT",
84 "sg_number": "INTEGER",
85 "volume": "REAL",
86 "volume_per_atom": "REAL",
87 "density": "REAL",
88 "a": "REAL",
89 "b": "REAL",
90 "c": "REAL",
91 "alpha": "REAL",
92 "beta": "REAL",
93 "gamma": "REAL",
94 "backend": "TEXT",
95 "normalized": "INTEGER",
96 "status": "TEXT",
97 "error": "TEXT",
98}
99
100
101DEFAULT_ERROR_RECORD_VALUES: dict[str, Any] = {
102 "formula": "",
103 "reduced_formula": "",
104 "anonymous_formula": "",
105 "composition_json": "{}",
106 "elements": "",
107 "nelements": -1,
108 "nsites": -1,
109 "sg_symbol": "",
110 "sg_number": -1,
111 "volume": -1.0,
112 "volume_per_atom": -1.0,
113 "density": -1.0,
114 "a": -1.0,
115 "b": -1.0,
116 "c": -1.0,
117 "alpha": -1.0,
118 "beta": -1.0,
119 "gamma": -1.0,
120 "backend": "",
121 "normalized": 0,
122}
123
124
125def guess_db_name(path: Path) -> str:
126 """
127 概要:
128 ファイルのパスからデータベース名を推定します。
129 詳細説明:
130 パスの要素を小文字に変換し、"pcod", "tcod", "cod" のいずれかが含まれる場合、
131 その名前をデータベース名として返します。
132 これらの名前が見つからない場合は "unknown" を返します。
133 引数:
134 :param path: CIFファイルのパス。
135 :type path: pathlib.Path
136 戻り値:
137 :returns: 推定されたデータベース名 (例: "cod", "tcod", "pcod", "unknown")。
138 :rtype: str
139 """
140 parts = [p.lower() for p in path.parts]
141 for name in ("pcod", "tcod", "cod"):
142 if name in parts:
143 return name
144 return "unknown"
145
146
147def guess_material_id(path: Path) -> str:
148 """
149 概要:
150 CIFファイルのファイル名からマテリアルIDを推定します。
151 詳細説明:
152 ファイル名から最初に見つかる数字列をマテリアルIDとして抽出します。
153 例えば、"1234567.cif" から "1234567" を返します。
154 数字が見つからない場合は、ファイル名全体 (拡張子なし) を返します。
155 引数:
156 :param path: CIFファイルのパス。
157 :type path: pathlib.Path
158 戻り値:
159 :returns: 推定されたマテリアルID。
160 :rtype: str
161 """
162 m = re.search(r"(\d+)", path.stem)
163 return m.group(1) if m else path.stem
164
165
166def composition_to_sorted_json(comp: Composition) -> str:
167 """
168 概要:
169 pymatgen.core.Composition オブジェクトを、元素名でソートされたJSON文字列に変換します。
170 詳細説明:
171 Composition オブジェクトの各元素と量の情報を取得し、元素のシンボル (文字列) でソートします。
172 その後、JSON形式の文字列として出力します。
173 pymatgen Composition.as_dict() のキーは Element オブジェクトではなく文字列として扱われます。
174 引数:
175 :param comp: 変換する組成オブジェクト。
176 :type comp: pymatgen.core.Composition
177 戻り値:
178 :returns: 元素名でソートされた組成情報のJSON文字列。
179 :rtype: str
180 """
181 d = {str(el): float(amount) for el, amount in comp.as_dict().items()}
182 return json.dumps(dict(sorted(d.items())), ensure_ascii=False, sort_keys=True)
183
184
185def normalize_formula(formula: str) -> str:
186 """
187 概要:
188 与えられた組成式をpymatgen.core.Compositionを使用して標準化(還元)します。
189 詳細説明:
190 例えば、"BaTiO3" や "Ba Ti O3" のような式を pymatgen が認識する標準形式に変換し、
191 さらに組成が最も簡単な整数比になるように還元します。
192 引数:
193 :param formula: 標準化する組成式文字列。
194 :type formula: str
195 戻り値:
196 :returns: 標準化された還元組成式文字列。
197 :rtype: str
198 """
199 comp = Composition(formula)
200 return comp.reduced_formula
201
202
203def composition_info_from_structure(path: Path) -> dict[str, Any]:
204 """
205 概要:
206 CIFファイルから構造情報を読み込み、検索用の組成および構造メタデータを作成します。
207 詳細説明:
208 指定されたCIFファイルを tkcif.tkcif_reader.read_structure を使って読み込み、
209 pymatgen.core.Structure オブジェクトを生成します。
210 そこから組成式、還元組成式、匿名組成式、元素リスト、サイト数、空間群情報、
211 格子定数、体積、原子あたりの体積、密度などを抽出し、辞書形式で返します。
212 空間群情報の取得に失敗した場合は、空文字列や -1 を返します。
213 引数:
214 :param path: CIFファイルのパス。
215 :type path: pathlib.Path
216 戻り値:
217 :returns: 検索用のメタデータを含む辞書。
218 :rtype: dict
219 """
220 structure, read_info = read_structure(path, return_info=True)
221
222 comp = structure.composition
223 red_comp = comp.reduced_composition
224
225 elements = sorted([el.symbol for el in red_comp.elements])
226 element_set = ",".join(elements)
227
228 try:
229 sg_symbol, sg_number_raw = structure.get_space_group_info()
230 sg_number = int(sg_number_raw)
231 except Exception:
232 sg_symbol = ""
233 sg_number = -1
234
235 nsites = len(structure)
236 volume = float(structure.volume)
237 volume_per_atom = volume / nsites if nsites > 0 else -1.0
238
239 try:
240 density = float(structure.density)
241 except Exception:
242 density = -1.0
243
244 return {
245 "formula": comp.formula,
246 "reduced_formula": red_comp.reduced_formula,
247 "anonymous_formula": red_comp.anonymized_formula,
248 "composition_json": composition_to_sorted_json(red_comp),
249 "elements": element_set,
250 "nelements": len(elements),
251 "nsites": nsites,
252 "sg_symbol": sg_symbol,
253 "sg_number": sg_number,
254 "volume": volume,
255 "volume_per_atom": volume_per_atom,
256 "density": density,
257 "a": float(structure.lattice.a),
258 "b": float(structure.lattice.b),
259 "c": float(structure.lattice.c),
260 "alpha": float(structure.lattice.alpha),
261 "beta": float(structure.lattice.beta),
262 "gamma": float(structure.lattice.gamma),
263 "backend": getattr(read_info, "backend", ""),
264 "normalized": int(bool(getattr(read_info, "normalized", False))),
265 }
266
267
268def get_existing_columns(conn: sqlite3.Connection, table_name: str) -> set[str]:
269 """
270 概要:
271 指定されたSQLiteテーブルに存在するカラム名を取得します。
272 詳細説明:
273 PRAGMA table_info SQLコマンドを実行し、テーブルのスキーマ情報を取得します。
274 結果からカラム名のみを抽出し、集合として返します。
275 引数:
276 :param conn: SQLiteデータベース接続オブジェクト。
277 :type conn: sqlite3.Connection
278 :param table_name: カラム情報を取得するテーブルの名前。
279 :type table_name: str
280 戻り値:
281 :returns: 既存のカラム名の集合。
282 :rtype: set
283 """
284 rows = conn.execute(f"PRAGMA table_info({table_name})").fetchall()
285 return {str(row[1]) for row in rows}
286
287
288def create_schema(conn: sqlite3.Connection) -> None:
289 """
290 概要:
291 SQLiteデータベースのスキーマを新規作成または既存のスキーマを更新します。
292 詳細説明:
293 cif_index テーブルが存在しない場合は作成し、必要なカラムとインデックスを追加します。
294 テーブルが既に存在する場合は、SCHEMA_COLUMNS に定義されているがまだ存在しないカラムがあれば追加します。
295 これにより、新しいバージョンのスキーマに既存のデータベースをアップグレードできます。
296 その後、検索パフォーマンス向上のために複数のインデックスを作成します。
297 引数:
298 :param conn: SQLiteデータベース接続オブジェクト。
299 :type conn: sqlite3.Connection
300 戻り値:
301 :returns: なし
302 :rtype: None
303 """
304 column_defs = ",\n ".join(
305 f"{name} {dtype}" for name, dtype in SCHEMA_COLUMNS.items()
306 )
307 conn.execute(f"""
308 CREATE TABLE IF NOT EXISTS cif_index (
309 id INTEGER PRIMARY KEY AUTOINCREMENT,
310 {column_defs}
311 )
312 """)
313
314 existing = get_existing_columns(conn, "cif_index")
315 for name, dtype in SCHEMA_COLUMNS.items():
316 if name not in existing:
317 conn.execute(f"ALTER TABLE cif_index ADD COLUMN {name} {dtype}")
318
319 conn.execute("CREATE INDEX IF NOT EXISTS idx_formula ON cif_index(reduced_formula)")
320 conn.execute("CREATE INDEX IF NOT EXISTS idx_elements ON cif_index(elements)")
321 conn.execute("CREATE INDEX IF NOT EXISTS idx_source_db ON cif_index(source_db)")
322 conn.execute("CREATE INDEX IF NOT EXISTS idx_backend ON cif_index(backend)")
323 conn.execute("CREATE INDEX IF NOT EXISTS idx_sg_number ON cif_index(sg_number)")
324 conn.execute("CREATE INDEX IF NOT EXISTS idx_volume_per_atom ON cif_index(volume_per_atom)")
325 conn.commit()
326
327
328def upsert_record(conn: sqlite3.Connection, rec: dict[str, Any]) -> None:
329 """
330 概要:
331 データベースにレコードを挿入または更新(UPSERT)します。
332 詳細説明:
333 指定されたレコード辞書recの内容に基づいて、cif_indexテーブルにデータを挿入します。
334 cif_path カラムは UNIQUE 制約を持つため、同じパスのレコードが既に存在する場合は
335 既存のレコードが新しいデータで置き換えられます(INSERT OR REPLACE)。
336 引数:
337 :param conn: SQLiteデータベース接続オブジェクト。
338 :type conn: sqlite3.Connection
339 :param rec: データベースに挿入または更新するレコードデータを含む辞書。
340 :type rec: dict
341 戻り値:
342 :returns: なし
343 :rtype: None
344 """
345 cols = list(SCHEMA_COLUMNS.keys())
346 col_sql = ", ".join(cols)
347 val_sql = ", ".join(f":{c}" for c in cols)
348 conn.execute(
349 f"""
350 INSERT OR REPLACE INTO cif_index ({col_sql})
351 VALUES ({val_sql})
352 """,
353 rec,
354 )
355
356
357def build_index(
358 root: Path,
359 db_path: Path,
360 pattern: str = "*.cif",
361 store_errors: int = 1,
362 commit_interval: int = 200,
363) -> None:
364 """
365 概要:
366 指定されたルートディレクトリ以下のCIFファイルをスキャンし、データベースインデックスを構築します。
367 詳細説明:
368 再帰的にCIFファイルを検索し、各ファイルから組成および構造情報を抽出します。
369 抽出された情報はSQLiteデータベースに格納され、検索可能なインデックスが作成されます。
370 ファイル処理中にエラーが発生した場合、store_errors が1であればエラー情報もデータベースに記録されます。
371 commit_interval ごとにトランザクションがコミットされ、進行状況が出力されます。
372 最後に、処理の要約と、使用されたバックエンドの統計情報が表示されます。
373 引数:
374 :param root: CIFファイルを検索するルートディレクトリのパス。
375 :type root: pathlib.Path
376 :param db_path: SQLiteデータベースファイルのパス。
377 :type db_path: pathlib.Path
378 :param pattern: 検索するCIFファイルのパターン (例: "*.cif")。
379 :type pattern: str
380 :param store_errors: エラーが発生したレコードをデータベースに保存するかどうか (0: 保存しない, 1: 保存する)。
381 :type store_errors: int
382 :param commit_interval: データベースにコミットする間隔 (処理されたファイル数)。
383 :type commit_interval: int
384 戻り値:
385 :returns: なし
386 :rtype: None
387 """
388 conn = sqlite3.connect(str(db_path))
389 create_schema(conn)
390
391 files = list(root.rglob(pattern))
392 print(f"Found CIF files: {len(files)}")
393
394 n_ok = 0
395 n_err = 0
396 backend_counts: dict[str, int] = {}
397
398 for i, path in enumerate(files, start=1):
399 path = path.resolve()
400
401 rec_base: dict[str, Any] = {
402 "source_db": guess_db_name(path),
403 "material_id": guess_material_id(path),
404 "cif_path": str(path),
405 }
406
407 try:
408 info = composition_info_from_structure(path)
409 rec = {
410 **rec_base,
411 **info,
412 "status": "ok",
413 "error": "",
414 }
415 upsert_record(conn, rec)
416 n_ok += 1
417 backend = str(rec.get("backend", "")) or "unknown"
418 backend_counts[backend] = backend_counts.get(backend, 0) + 1
419
420 except Exception as exc:
421 n_err += 1
422 print(f"[ERROR] {path}: {exc}")
423
424 if store_errors:
425 rec = {
426 **rec_base,
427 **DEFAULT_ERROR_RECORD_VALUES,
428 "status": "error",
429 "error": str(exc),
430 }
431 upsert_record(conn, rec)
432
433 if i % commit_interval == 0:
434 conn.commit()
435 print(f"Indexed {i}/{len(files)} ok={n_ok} error={n_err}")
436
437 conn.commit()
438 conn.close()
439
440 print(f"Done. ok={n_ok}, error={n_err}")
441 if backend_counts:
442 print("\nBackend summary:")
443 for backend, count in sorted(backend_counts.items(), key=lambda x: (-x[1], x[0])):
444 print(f" {backend:24s} {count}")
445 print(f"\nDB: {db_path}")
446
447
448def make_subset_condition_for_contains(target_elements: list[str]) -> tuple[str, list[Any]]:
449 """
450 概要:
451 データベースの elements カラムが指定された元素リストの部分集合である条件を生成します。
452 詳細説明:
453 この関数は、SQLのLIKE演算子を使用して、ある元素集合 (target_elements) の中に
454 データベースレコードの元素集合が含まれるかを判定するためのWHERE句とパラメータを生成します。
455 elementsカラムは、DB作成時にソートされたカンマ区切りの文字列として格納されているため、
456 例として、targetが Ba,O,Ti の場合、DBレコードの elements が Ba,O や Ti,O などであればヒットします。
457 要素の境界を明確にするために、カンマで囲まれた文字列に対する LIKE 検索を行います。
458 引数:
459 :param target_elements: 検索対象となる元素のソート済みリスト。
460 :type target_elements: list
461 戻り値:
462 :returns: WHERE句の条件文字列と、その条件にバインドするパラメータのリストのタプル。
463 :rtype: tuple
464 """
465 target_string = "," + ",".join(target_elements) + ","
466 return "? LIKE '%,' || elements || ',%'", [target_string]
467
468
469def search_by_formula(
470 db_path: Path,
471 formula: str,
472 source_db: str = "",
473 match: str = "exact",
474 limit: int = 100,
475 output_json: int = 0,
476) -> None:
477 """
478 概要:
479 指定された組成式とマッチングモードに基づいて、データベースからCIFデータを検索します。
480 詳細説明:
481 pymatgen.core.Composition を用いて検索対象の組成式を正規化し、
482 指定されたマッチングモード (exact, elements, contains) に従ってデータベースをクエリします。
483 検索結果は、source_db、material_id、reduced_formula、elements、空間群、格子定数などの情報を含みます。
484 結果は、元素数、ソースデータベース、還元組成式、マテリアルIDの順にソートされ、指定された件数に制限されます。
485 output_json が1の場合、結果はJSON形式で出力されます。それ以外の場合は、人間が読みやすい形式で出力されます。
486 引数:
487 :param db_path: SQLiteデータベースファイルのパス。
488 :type db_path: pathlib.Path
489 :param formula: 検索する組成式。
490 :type formula: str
491 :param source_db: 検索対象のソースデータベース名 (例: "cod", "tcod")。空文字列の場合はすべてのデータベースを検索します。
492 :type source_db: str
493 :param match: マッチングモード。"exact" (還元組成式が完全に一致), "elements" (構成元素が完全に一致),
494 "contains" (指定元素集合が部分集合として含まれる) のいずれかを指定します。
495 :type match: str
496 :param limit: 検索結果の最大件数。
497 :type limit: int
498 :param output_json: 結果をJSON形式で出力するかどうか (0: 通常出力, 1: JSON出力)。
499 :type output_json: int
500 戻り値:
501 :returns: なし
502 :rtype: None
503 例外:
504 :raises ValueError: 未知のマッチングモードが指定された場合に発生します。
505 """
506 target_comp = Composition(formula).reduced_composition
507 target_formula = target_comp.reduced_formula
508 target_elements = sorted([el.symbol for el in target_comp.elements])
509 target_element_set = ",".join(target_elements)
510
511 conn = sqlite3.connect(str(db_path))
512 conn.row_factory = sqlite3.Row
513
514 params: list[Any] = []
515 where = ["status = 'ok'"]
516
517 if source_db:
518 where.append("source_db = ?")
519 params.append(source_db)
520
521 if match == "exact":
522 # BaTiO3 と TiBaO3 はどちらも BaTiO3 に正規化される。
523 where.append("reduced_formula = ?")
524 params.append(target_formula)
525
526 elif match == "elements":
527 # 元素集合が完全一致:Ba-Ti-O系のみ。
528 where.append("elements = ?")
529 params.append(target_element_set)
530
531 elif match == "contains":
532 # 指定元素集合の部分集合を許す。
533 # BaTiO3検索で Ba, Ti, O, BaO, TiO2, BaTiO3 などがヒット。
534 cond, cond_params = make_subset_condition_for_contains(target_elements)
535 where.append(cond)
536 params.extend(cond_params)
537
538 else:
539 raise ValueError(f"Unknown match mode: {match}")
540
541 sql = f"""
542 SELECT
543 source_db, material_id, reduced_formula, formula,
544 composition_json, elements, sg_symbol, sg_number, nsites,
545 a, b, c, alpha, beta, gamma, volume, volume_per_atom, density,
546 backend, normalized, cif_path
547 FROM cif_index
548 WHERE {' AND '.join(where)}
549 ORDER BY nelements DESC, source_db, reduced_formula, material_id
550 LIMIT ?
551 """
552 params.append(limit)
553
554 rows = [dict(r) for r in conn.execute(sql, params).fetchall()]
555 conn.close()
556
557 if output_json:
558 print(json.dumps(rows, ensure_ascii=False, indent=2))
559 return
560
561 print(f"query formula : {formula}")
562 print(f"reduced formula : {target_formula}")
563 print(f"elements : {target_element_set}")
564 print(f"match : {match}")
565 print(f"hits : {len(rows)}")
566 print()
567
568 for r in rows:
569 print(
570 f"[{r['source_db']}] {r['material_id']} "
571 f"{r['reduced_formula']} "
572 f"elements={r['elements']} "
573 f"SG={r['sg_symbol']}({r['sg_number']}) "
574 f"nsites={r['nsites']} "
575 f"backend={r.get('backend', '')}"
576 )
577 print(f" a,b,c = {r['a']:.6g}, {r['b']:.6g}, {r['c']:.6g}")
578 print(f" V/atom = {r.get('volume_per_atom', -1):.6g} A^3, density = {r.get('density', -1):.6g} g/cm^3")
579 print(f" path = {r['cif_path']}")
580 print()
581
582
583def show_info(db_path: Path) -> None:
584 """
585 概要:
586 指定されたデータベースの統計情報を表示します。
587 詳細説明:
588 データベースに格納されているCIFレコードの総数、ステータスごとの内訳 (成功/エラー)、
589 ソースデータベースごとの内訳、使用されたバックエンドごとの内訳、
590 および正規化の有無ごとの内訳を出力します。
591 これらの情報は、データベースの状態やインデックス作成の品質を把握するのに役立ちます。
592 引数:
593 :param db_path: SQLiteデータベースファイルのパス。
594 :type db_path: pathlib.Path
595 戻り値:
596 :returns: なし
597 :rtype: None
598 """
599 conn = sqlite3.connect(str(db_path))
600 cur = conn.cursor()
601
602 print("Total:")
603 for row in cur.execute("""
604 SELECT status, COUNT(*) FROM cif_index GROUP BY status
605 """):
606 print(f" {row[0]}: {row[1]}")
607
608 print("\nBy source_db:")
609 for row in cur.execute("""
610 SELECT source_db, status, COUNT(*)
611 FROM cif_index
612 GROUP BY source_db, status
613 ORDER BY source_db, status
614 """):
615 print(f" {row[0]:8s} {row[1]:8s} {row[2]}")
616
617 existing_cols = get_existing_columns(conn, "cif_index")
618 if "backend" in existing_cols:
619 print("\nBy backend:")
620 for row in cur.execute("""
621 SELECT backend, status, COUNT(*)
622 FROM cif_index
623 GROUP BY backend, status
624 ORDER BY status, backend
625 """):
626 backend = row[0] if row[0] else "(none)"
627 print(f" {backend:24s} {row[1]:8s} {row[2]}")
628
629 if "normalized" in existing_cols:
630 print("\nNormalized:")
631 for row in cur.execute("""
632 SELECT normalized, status, COUNT(*)
633 FROM cif_index
634 GROUP BY normalized, status
635 ORDER BY normalized, status
636 """):
637 print(f" normalized={row[0]} {row[1]:8s} {row[2]}")
638
639 conn.close()
640
641
642def main() -> None:
643 """
644 概要:
645 スクリプトのエントリポイント関数です。
646 詳細説明:
647 コマンドライン引数を解析し、指定されたモード (index, search, info) に応じて
648 適切な処理関数を呼び出します。
649 --mode index はCIFファイルのインデックスを構築し、
650 --mode search はデータベースから組成式で検索し、
651 --mode info はデータベースの統計情報を表示します。
652 検索モード (--mode search) の場合、--formula 引数が必須です。
653 引数:
654 なし
655 戻り値:
656 :returns: なし
657 :rtype: None
658 """
659 parser = argparse.ArgumentParser()
660
661 parser.add_argument("--mode", type=str, default="search",
662 choices=["index", "search", "info"])
663
664 parser.add_argument("--root", type=str, default=".")
665 parser.add_argument("--db", type=str, default="cif_index.sqlite")
666 parser.add_argument("--pattern", type=str, default="*.cif")
667
668 parser.add_argument("--formula", type=str, default="")
669 parser.add_argument("--match", type=str, default="exact",
670 choices=["exact", "elements", "contains"],
671 help=(
672 "exact: 還元組成一致, "
673 "elements: 元素集合一致, "
674 "contains: 指定元素集合の部分集合も許す"
675 ))
676 parser.add_argument("--source-db", type=str, default="",
677 help="cod, tcod, pcod など。空なら全DB検索。")
678
679 parser.add_argument("--store-errors", type=int, default=1, choices=[0, 1])
680 parser.add_argument("--json", type=int, default=0, choices=[0, 1])
681 parser.add_argument("--limit", type=int, default=100)
682
683 args = parser.parse_args()
684
685 db_path = Path(args.db)
686
687 if args.mode == "index":
688 build_index(
689 root=Path(args.root),
690 db_path=db_path,
691 pattern=args.pattern,
692 store_errors=args.store_errors,
693 )
694
695 elif args.mode == "search":
696 if not args.formula:
697 print("ERROR: --formula is required for --mode search")
698 sys.exit(1)
699
700 search_by_formula(
701 db_path=db_path,
702 formula=args.formula,
703 source_db=args.source_db,
704 match=args.match,
705 limit=args.limit,
706 output_json=args.json,
707 )
708
709 elif args.mode == "info":
710 show_info(db_path)
711
712
713if __name__ == "__main__":
714 main()