"""
CIFファイルを管理するためのパッケージ。
CIF (Crystallographic Information File) 形式のファイルを読み込み、解析し、
結晶構造データとして操作するためのクラスと関数を提供します。
ファイルからのデータの抽出、キーワードと値のペアの解析、ループデータの処理などをサポートします。
関連リンク:
:doc:`tkcif_usage`
"""
import sys
import shlex
import re
from pprint import pprint
from tklib.tkobject import tkObject
from tklib.tkcrystal.tkcifobject import tkCIFObject, tkCIFData
from tklib.tkcrystal.tkcrystal import tkCrystal
from tklib.tkutils import SplitFilePath, split_quoted_args
from tklib.tkfile import tkFile
import tklib.tkre as tkre
#=====================================
# CIF manager class
#=====================================
end_of_cifdata = 0
header = 1
comment = 2
singlekey = 3
multikeys = 4
loopkeys = 5
other = 999
[ドキュメント]
class tkCIF(tkCIFObject):
"""
CIFファイルを管理するためのクラス。
CIFファイルの読み込み、解析、およびデータ抽出機能を提供します。
tkCIFObjectを継承し、ファイルI/OおよびCIFデータ構造の処理を拡張します。
"""
def __init__(self, path = None, mode = 'r', **args):
"""
tkCIFオブジェクトを初期化します。
CIFファイルのパスとオープンモードを設定し、内部変数を初期化します。
パスが指定されている場合、ファイルをオープンします。
:param path: str, optional CIFファイルのパス。デフォルトはNone。
:param mode: str, optional ファイルのオープンモード(例: 'r'、'w')。デフォルトは'r'。
:param args: dict その他のキーワード引数。
"""
super(tkCIF, self).__init__(path, mode, **args)
self.debug = 0
self.cifdata_list = []
self.fp = None
self.path = None
self.mode = 'r'
self.path = self.IfYes(path is not None, path, self.path)
self.mode = self.IfYes(mode is not None, mode, self.mode)
self.update(**args)
if self.path is not None:
self.Open(self.path, self.mode)
# print("open [{}]:".format(self.path), self.fp)
def __del__(self):
"""
tkCIFオブジェクトのデストラクタ。
オープンされているファイルを閉じ、親クラスのデストラクタを呼び出します。
"""
self.Close()
super(tkCIF, self).__del__()
def __str__(self):
"""
オブジェクトの文字列表現を返します。
このオブジェクトのクラスパスを返します。
:returns: str このオブジェクトのクラスパス。
"""
return self.ClassPath()
[ドキュメント]
def splitline(self, lines, i):
"""
与えられた行リストの指定されたインデックスの行をキーワードと値に分割します。
行をスペースで区切られたキーワードと値に分割します。
キーワードは非空白部分、値は残りの部分からヘッダーと末尾のスペースを除いたものです。
:param lines: list[str] 分割対象の行のリスト。
:param i: int 分割する行のインデックス。
:returns: tuple[str, str, str] 行、キーワード、値のタプル。
行が存在しない場合は (None, '', '') を返します。
"""
if i >= len(lines):
return None, '', ''
match = re.match(r"\s*(\S.*?)\s+(\S.*?)\s*$", lines[i])
if match:
return lines[i], match.group(1), match.group(2)
return lines[i], lines[i].strip(), ''
[ドキュメント]
def GetNextValuesOne(self, lines, nrequired, i):
"""
単一の行からCIF値を抽出します。
`lines[i]` からCIFデータを抽出し、指定された数の値が要求された場合に、
それらをリストとして返します。マルチラインの値(セミコロンで開始・終了)も処理します。
:param lines: list[str] 解析対象の行のリスト。
:param nrequired: int 必要とされる値の数。
:param i: int 解析を開始する行のインデックス。
:returns: tuple[int, int, str, list[str]]
現在の行インデックス、ステータス、関連する行(文字列)、値のリスト。
ステータスは `end_of_cifdata`、`singlekey`、`multikeys` のいずれかです。
"""
l, key, val = self.splitline(lines, i)
# data end
if l is None:
# print("dataend")
return i, end_of_cifdata, '', []
# for single key, single line value
l0 = l.strip()
list = tkre.Match(r';\s*(.*)\s*', l0)
if not list:
if nrequired == 1:
self.dprint(1, "GNVOne(single): cA={} nreq={} l={}, key={}".format(i, nrequired, l0, key))
return i+1, singlekey, l0, [l0]
else:
self.dprint(1, "GNVOne(mult nreq): cB={} nreq={} l={}, key={}".format(i, nrequired, l0, key))
kws = split_quoted_args(l.strip())
# try:
# kws = shlex.split(l.strip())
# except:
# kws = l.strip()
# self.dprint(1, "kws=", kws)
if len(kws) == nrequired:
return i+1, multikeys, l0, kws
return i+1, multikeys, l0, kws
# for single key, multi line value
if len(list) >= 2:
s = list[1]
else:
s = ''
# try:
# s = list[1]
# except:
# s = ''
while 1:
i += 1
l, key, val = self.splitline(lines, i)
self.dprint(1, "GNVOne(mult): cC={} l={}, key={}".format(i, l0, key))
l0 = l.strip()
# data end
if l is None:
s0 = s.strip()
if s0 == '':
# print("single multi line1")
return i, singlekey, s, []
else:
# print("single multi line2")
return i, singlekey, s, [s0]
# multi line data end
list = tkre.Match(';', l0)
if list:
s0 = s.strip()
if s0 == '':
# print("single multi line ; 1")
return i+1, singlekey, s, []
else:
# print("single multi line ; 2")
return i+1, singlekey, s, [s0]
s += l
[ドキュメント]
def GetNextValues(self, lines, nrequired, i):
"""
複数行からCIF値を抽出します。
`lines[i]` から始まる複数行からCIF値を抽出し、指定された数の値が要求されるまで読み込みを続けます。
主に `_keyword` に続く値や `loop_` データブロック内の値を処理するために使用されます。
:param lines: list[str] 解析対象の行のリスト。
:param nrequired: int 必要とされる値の数。
:param i: int 解析を開始する行のインデックス。
:returns: tuple[int, int, str, list[str]]
現在の行インデックス、ステータス、関連する結合行(文字列)、値のリスト。
ステータスは `multikeys` または `end_of_cifdata` のいずれかです。
"""
count = 0
retval = []
retline = ''
c = i
while 1:
c, type2, l2, values = self.GetNextValuesOne(lines, nrequired, c)
nv = len(values)
count += nv
retval += values
retline += l2
self.dprint(1, "GNV: c={} count={} type2={} values={}".format(c, count, type2, values))
if count >= nrequired:
return c, multikeys, retline, retval
if type2 == end_of_cifdata:
if nv == 0:
return c, end_of_cifdata, retline, retval
else:
return c, multikeys, retline, retval
[ドキュメント]
def GetNextKey(self, lines, i):
"""
指定された行からCIFキーワードと対応する値を抽出します。
CIFファイルの行を解析し、`data_`ヘッダー、コメント、単一キーワード、
または `loop_` ブロックのキーワードと値を識別します。
:param lines: list[str] 解析対象の行のリスト。
:param i: int 解析を開始する行のインデックス。
:returns: tuple[int, int, list]
現在の行インデックス、ステータス、および抽出されたキーワードと値のリスト。
ステータスは `end_of_cifdata`、`header`、`comment`、`singlekey`、
`multikeys` (実際には`loopkeys`が返される)、`loopkeys`、`other` のいずれかです。
`keyval` の形式はステータスに依存します。
- `header`: `['header', 'data_name']`
- `comment`: `['# comment text', '']`
- `singlekey`: `['keyword', 'value']`
- `loopkeys`: `[[['kw1', 'val1a'], ['kw2', 'val2a']], [['kw1', 'val1b'], ['kw2', 'val2b']], ...]`
"""
# data end
nlines = len(lines)
if i >= nlines:
# print("end_of_cifdata: i >= nlines ({} > {})".format(i, nlines))
return i, end_of_cifdata, []
keyinf = []
c = i
nlines = len(lines)
while 1:
l, key, val = self.splitline(lines, c)
if l is None:
break
self.dprint(1, "GNK: c={}, nlines={}: key=[{}] val=[{}]".format(c, len(lines), key, val))
l0 = l.strip()
# null value
if l0 == '':
c += 1
continue
# end of cif
if re.match(r'#End of', l0):
# print("end_of_cifdata: #End of")
return c+1, end_of_cifdata, []
# if re.match(r'data_', l0):
# return c, end_of_cifdata, []
# comment
if re.match('#', l0):
return c+1, comment, [l0, '']
# first header line
if re.match('data_', key):
return c+1, header, ['header', l0]
# for single keyword starting from '_'
if key == '':
continue
elif re.match('_', key):
# self.dprint(1, "singlekw")
# for single key, multiple line value
if val == '':
c += 1
c, type2, l2, values = self.GetNextValues(lines, 1, c)
self.dprint(1, "GNK: c3={} type={} key={} val={}".format(c, key, type2, values[0]))
return c, singlekey, [key, values[0]]
else:
return c+1, singlekey, [key, val]
# for keyword 'loop_'
elif key == 'loop_':
self.dprint(1, "\n")
self.dprint(1, "======================= loop")
self.dprint(1, " ** Read keywords")
# collect keywords starting from '_'
keywords = []
c += 1
while 1:
if c >= nlines:
break
l, key, val = self.splitline(lines, c)
if l is None:
break
self.dprint(1, "GNK: c5={} key={} val={}".format(c, key, val))
l0 = l.strip()
if l0 == '':
c += 1
continue
# if re.match(r"\s*#End of", l) or re.match(r"\s*#=====", l):
if re.match(r"\s*#End of", l):
c += 1
break
# if re.match(r"\s*data_", l):
# break
if re.match(r"\s*#", l):
c += 1
continue
if re.match(r"\s*_", key):
keywords.append(key)
c += 1
continue
else:
c -= 1
break
# collect values untile a new '_' or 'loop_' is found
c += 1
nrequired = len(keywords)
keyvalues = []
nkeyvalues = 0
self.dprint(1, "\n")
self.dprint(1, " ** Read values (c={} nlines={} nrequired={})"
.format(c, nlines, nrequired))
while 1:
if c >= nlines:
break
self.dprint(1, "**check c={} {}".format(c, lines[c]))
l0 = lines[c].strip()
# print("l0=", l0)
if l0 == '':
c += 1
continue
# if re.match(r"\s*#End of", lines[c]) or re.match(r"\s*#=====", lines[c]):
if re.match(r"\s*#End of", lines[c]):
c += 1
break
# if re.match(r"\s*data_", lines[c]):
# break
if re.match(r"\s*#", lines[c]):
c += 1
continue
if re.match("\s*$", lines[c]):
c += 1
continue
if re.match("_", lines[c]) or re.match("loop_", lines[c]):
break
self.dprint(1, "GNK: for GetNextValues: nreq=", nrequired)
c, type2, l, values = self.GetNextValues(lines, nrequired, c)
self.dprint(1, " c4={} type2={} key={} val={} l={}"
.format(c, type2, key, values, l.strip()))
if type2 == end_of_cifdata:
break
# if re.match("_", key) or key == 'loop_':
# break
nval = len(values)
self.dprint(1, "\n")
self.dprint(1, "GNK: nrequired={} nvalues={} nkeyvalues={}"
.format(nrequired, nval, nkeyvalues))
self.dprint(1, " values=", values)
keyval = []
for i in range(len(keywords)):
self.dprint(1, "GNK: build keyval: [{}]::[{}]"
.format(keywords[i], values[i]))
keyval.append([keywords[i], values[i]])
keyvalues.append(keyval)
nkeyvalues += 1
self.dprint(1, "\n*****************GNK: loop_values break")
self.dprint(1, "keyvalues:")
self.dpprint(1, keyvalues)
self.dprint(1, "")
return c, loopkeys, keyvalues
else:
print(f"Error in tkCIF.GetNextKey(): Invalid key [{l}]")
return c+1, other, [l, '']
continue
return c, end_of_cifdata, []
[ドキュメント]
def StringsToCIF(self, lines):
"""
文字列のリストからCIFの内容を抽出して辞書形式に変換します。
`GetNextKey` メソッドを使用して行のリストを解析し、
キーワードと値のペアを格納する辞書を構築します。
ループデータは `keyword[i]` の形式でインデックス付きエントリとして格納されます。
:param lines: list[str] CIFデータの行のリスト。
:returns: dict CIF情報を含む辞書。
キーはCIFキーワード(ループデータの場合は `_keyword[index]` 形式)、
値は対応するデータです。
"""
cifinf = {}
i = 0
icomment = 0
while 1:
self.dprint(1, "S2CIF: i next=", i, " len(lines)=", len(lines))
i_next, type, keyinf = self.GetNextKey(lines, i)
self.dprint(1, "S2CIF: i={} => {} type=[{}] key=[{}]\n".format(i, i_next, type, keyinf))
if type == end_of_cifdata or len(keyinf) == 0:
return cifinf
self.dprint(1, "keyinf:")
self.dpprint(1, keyinf)
self.dprint(1, "")
if type == end_of_cifdata:
break
elif type == comment:
self.dprint(1, "comment {} [{}]".format(icomment, keyinf[0]))
cifinf["comment[{}]".format(icomment+1)] = keyinf[0]
icomment += 1
elif type == loopkeys:
for idata in range(len(keyinf)):
for j in range(len(keyinf[idata])):
cifinf[keyinf[idata][j][0] + "[{}]".format(idata)] = keyinf[idata][j][1]
else:
cifinf[keyinf[0]] = keyinf[1]
i = i_next
return cifinf
[ドキュメント]
def ReadNextCIF(self, path = None, print_level = 0):
"""
CIFファイルの次のデータブロックを読み込み、行のリストとして返します。
`data_` で始まる次のCIFデータブロックを見つけ、`#End` または次の `data_`
ブロックの手前までを読み込みます。
パスが指定された場合、そのファイルを開いてから読み込みます。
それ以外の場合は、現在のファイルポインタ (`self.fp`) から読み込みます。
:param path: str, optional 読み込むCIFファイルのパス。指定しない場合、`self.path` を使用します。
:param print_level: int, optional デバッグ出力レベル。デフォルトは0。
:returns: list[str] CIFデータの行のリスト。読み込むデータブロックがない場合はNone。
"""
if path:
self.path = path
if self.Open(self.path) == 0:
return None
lines = []
IsSkip = 1
ret = None
iheader = 0
while 1:
pos = self.Tell()
line = self.ReadLine()
# self.dprint(1, "tkcrystal.tkcif.ReadNextCIF: line: [{}]".format(line.strip()))
if not line:
if len(lines) == 0:
break
ret = lines
break
if IsSkip and re.match(r'[\r\n]$', line):
continue
IsSkip = 0
if re.match("#End", line):
# self.dprint(1, "tkcrystal.tkcif.ReadNextCIF: match to end")
lines.append(line)
ret = lines
break
if re.match("data_", line):
iheader += 1
if iheader > 1:
self.Seek(pos)
ret = lines
break
lines.append(line)
if path:
self.Close()
return ret
def __ReadCIF1(self, path = None, print_level = 1):
"""
CIFファイルを読み込むためのヘルパー関数。
`ReadCIF` メソッドからのみ呼び出され、単一のCIFデータブロックを `tkCIFData` オブジェクトとして読み込みます。
ファイルポインタが `io.StringIO` オブジェクトである可能性も考慮します。
:param path: str or io.StringIO, optional 読み込むCIFファイルのパス、またはStringIOオブジェクト。
指定しない場合、`self.path` または `self.fp` を使用します。
:param print_level: int, optional デバッグ出力レベル。デフォルトは1。
:returns: tkCIFData 読み込まれたCIFデータを含む `tkCIFData` オブジェクト。
読み込みに失敗した場合は空のリスト `[]` を返します。
"""
# pathが文字列型でない場合、io.StringIOと判断してファイルポインタとする
if path and type(path) is not str:
self.fp = path
path = None
if path:
if self.Open(path) == None:
return []
elif self.fp:
self.Rewind()
else:
if self.Open(self.path) == None:
return []
lines = self.ReadNextCIF()
if path:
self.Close()
dic = self.StringsToCIF(lines)
cif = tkCIFData()
cif.path = self.path
cif.data = dic
cif.lines = lines
self.cifdata_list = [cif]
if not cif.CheckCIF():
exit()
return self.cifdata_list[0]
[ドキュメント]
def read_cif(self, path = None, find_valid_structure = 1, print_level = 1):
"""
CIFファイルを読み込み、`tkCIFData` オブジェクトのリスト (`self.cifdata_list`) を更新します。
単一のCIFデータブロック、または複数のブロックを読み込むことができます。
`find_valid_structure` が真の場合、有効な結晶構造データ(`_cell_length_a` > 0)を
持つ最初のデータブロックを返します。
:param path: str, optional 読み込むCIFファイルのパス。指定しない場合、`self.path` を使用します。
:param find_valid_structure: int, optional
1の場合、有効な構造データが見つかるまでセクションを読み込みます。
0の場合、最初のセクションのみを読み込みます。デフォルトは1。
:param print_level: int, optional デバッグ出力レベル。デフォルトは1。
:returns: tkCIFData 有効な結晶構造データを持つ `tkCIFData` オブジェクト。
`find_valid_structure` が有効で適切なデータが見つからない場合はNone。
"""
if find_valid_structure:
cifdatas = self.ReadCIFs(path, print_level = print_level)
for cifdata in cifdatas:
lata = cifdata.geterrorf('_cell_length_a')
if lata is not None and lata > 0:
return cifdata
else:
return None
return self.__ReadCIF1(path, print_level = print_level)
[ドキュメント]
def ReadCIF(self, path = None, find_valid_structure = 1, print_level = 1):
"""
CIFファイルを読み込み、`tkCIFData` オブジェクトを返します。
`read_cif` メソッドのエイリアスです。
有効な結晶構造データを持つ最初のデータブロックを返します。
:param path: str, optional 読み込むCIFファイルのパス。指定しない場合、`self.path` を使用します。
:param find_valid_structure: int, optional
1の場合、有効な構造データが見つかるまでセクションを読み込みます。
0の場合、最初のセクションのみを読み込みます。デフォルトは1。
:param print_level: int, optional デバッグ出力レベル。デフォルトは1。
:returns: tkCIFData 有効な結晶構造データを持つ `tkCIFData` オブジェクト。
`find_valid_structure` が有効で適切なデータが見つからない場合はNone。
"""
return self.read_cif(path = path, find_valid_structure = find_valid_structure, print_level = print_level)
[ドキュメント]
def ReadCIFs(self, path = None, print_level = 1):
"""
CIFファイルを読み込み、その中の全てのデータブロックを `tkCIFData` オブジェクトのリストとして返します。
`data_` で始まる各セクションを個別の `tkCIFData` オブジェクトとして解析し、リストに格納します。
ファイルポインタが `io.StringIO` オブジェクトである可能性も考慮します。
:param path: str or io.StringIO, optional 読み込むCIFファイルのパス、またはStringIOオブジェクト。
指定しない場合、`self.path` または `self.fp` を使用します。
:param print_level: int, optional デバッグ出力レベル。デフォルトは1。
:returns: list[tkCIFData] 読み込まれた全てのCIFデータを含む `tkCIFData` オブジェクトのリスト。
"""
# pathが文字列型でない場合、io.StringIOと判断してファイルポインタとする
if path and type(path) is not str:
self.fp = path
path = None
if path:
if self.Open(path) == None:
return []
elif self.fp:
self.Rewind()
else:
if self.Open(path) == None:
return []
ciflist = []
while 1:
lines = self.ReadNextCIF(print_level = print_level)
if lines == None:
break
dic = self.StringsToCIF(lines)
cif = tkCIFData()
cif.path = self.path
cif.data = dic
cif.lines = lines
ciflist.append(cif)
self.cifdata_list = ciflist
if path:
self.Close()
return self.cifdata_list
infile = 'test.cif'
#infile = 'test1.cif'
#narg = len(sys.argv)
#if narg >= 2:
# infile = sys.argv[1]
#debug = 0
#if narg >= 3:
# debug = int(sys.argv[2])
[ドキュメント]
def main():
cif = tkCIF(infile)
cif.debug = debug
single = 1
# atom = tkAtomType()
# inf = atom.GetAtomInformation('Au')
# for key in inf:
# print("{}: {}".format(key, inf[key]))
# print("")
# exit()
if single:
cifdata = cif.ReadCIF()
cifdata.Print()
cry = cifdata.GetCrystal()
print("")
print("==============================================")
cry.PrintInf()
print("")
print("==============================================")
outfile = 'a.cif'
cifdata.WriteSimpleCIFFile(outfile)
outfile = 'b.cif'
cifdata.CreateCIFFileFromCCrystal(cry, outfile)
# pos0 = [0.3, 0.2, 0.1]
# isym = 1
# pos1 = cry.DoSymmetryOperation(pos0, isym)
# print("pos0 ", pos0, " => sym %d" % (isym), pos1)
else:
cifdatas = cif.ReadCIFs()
for cifdata in cifdatas:
cifdata.Print()
cif.Close()
if __name__ == "__main__":
main()