"""
点群の生成元とそれらの語表現を計算するスクリプト。
詳細説明:
このスクリプトは、Pymatgenライブラリを用いて特定の点群(H–M記法)の対称操作を取得し、
その操作集合から最小限の生成元セットを特定します。
さらに、点群内の各操作を、見つけられた生成元とそれらのべき乗の積(語表現)として表現します。
数値的な丸め誤差による問題を避けるため、対称操作の直交化やスナップ処理が組み込まれています。
また、生成元には優先順位を付けて抽出され、ユーザーが任意のラベルを付けることも可能です。
計算結果は、標準出力またはJSON形式で出力されます。
:doc:`pg_generators_usage`
"""
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import sys
import json
import argparse
import numpy as np
from collections import deque
from pymatgen.symmetry.groups import PointGroup
# 32種(H–M)
PG_HM = [
"1", "-1", "2", "m", "2/m", "222", "mm2", "mmm",
"4", "-4", "4/m", "422", "4mm", "-42m", "4/mmm",
"3", "-3", "32", "3m", "-3m",
"6", "-6", "6/m", "622", "6mm", "-6m2", "6/mmm",
"23", "m-3", "432", "-43m", "m-3m"
]
EPS = 1e-12
Z = np.array([0.0, 0.0, 1.0])
X = np.array([1.0, 0.0, 0.0])
Y = np.array([0.0, 1.0, 0.0])
# ---------- 数値安定化(det 符号保存) ----------
[ドキュメント]
def orthogonalize_preserving_det(R):
"""
行列を直交化し、元の行列の行列式の符号を保存します。
詳細説明:
特異値分解(SVD)を用いて行列Rを直交化します。直交化後の行列の行列式の符号が
元のRの符号と異なる場合、最後の特異ベクトルに-1を乗算して符号を一致させます。
これは回転行列や鏡映行列の分類において、行列式の符号が重要であるためです。
:param R: 直交化する3x3行列。
:type R: numpy.ndarray
:returns: 行列式の符号を保存して直交化された3x3行列。
:rtype: numpy.ndarray
"""
U, _, Vt = np.linalg.svd(R)
R_sv = U @ Vt
det_target = np.sign(np.linalg.det(R)) or 1.0
det_sv = np.sign(np.linalg.det(R_sv)) or 1.0
if det_sv != det_target:
U[:, -1] *= -1
R_sv = U @ Vt
return R_sv
[ドキュメント]
def canon(R, ndigits=12):
"""
行列を正規化し、比較可能なタプル形式に変換します。
詳細説明:
まず `orthogonalize_preserving_det` を用いて行列を直交化し、
その結果の要素を丸めて1次元のタプルに変換します。
これにより、浮動小数点数の誤差による比較の問題を避け、行列の同一性を
効率的に判定できるようになります。
:param R: 変換する3x3行列。
:type R: numpy.ndarray
:param ndigits: 丸める小数点以下の桁数。デフォルトは12。
:type ndigits: int
:returns: 丸められ、直列化された行列の要素を含むタプル。
:rtype: tuple
"""
R_ = orthogonalize_preserving_det(R)
return tuple(np.round(R_.reshape(-1), ndigits))
# ---------- 分類&幾何 ----------
[ドキュメント]
def rotation_order(R):
"""
回転行列の回転次数を計算します。
詳細説明:
3x3回転行列の回転次数(最小の正整数nでR^n = Iとなるn)を計算します。
行列のトレースから回転角を求め、2πを回転角で割ることで次数を推定します。
微小な回転角(ほぼ恒等変換)の場合は1を返します。
:param R: 回転行列。
:type R: numpy.ndarray
:returns: 回転次数。
:rtype: int
"""
R = orthogonalize_preserving_det(R)
cos_th = np.clip((np.trace(R) - 1) / 2, -1, 1)
th = np.arccos(cos_th)
if th < 1e-8:
return 1
return int(np.round(2 * np.pi / th))
[ドキュメント]
def eigvec_for_value(R, val, atol=1e-6):
"""
指定された固有値に対応する実固有ベクトルを抽出します。
詳細説明:
与えられた行列の固有値と固有ベクトルを計算し、指定された固有値に
数値的に近い実部を持つ固有値に対応する固有ベクトルを返します。
見つからない場合はNoneを返します。主に回転軸(固有値1)や鏡映面法線
(固有値-1)を求めるのに使用されます。
:param R: 固有ベクトルを計算する3x3行列。
:type R: numpy.ndarray
:param val: 探し出す固有値。
:type val: float
:param atol: 固有値の実部を比較する際の許容誤差。デフォルトは1e-6。
:type atol: float
:returns: 正規化された実固有ベクトル、または見つからなかった場合はNone。
:rtype: numpy.ndarray or None
"""
vals, vecs = np.linalg.eig(R)
idx = np.where(np.isclose(vals.real, val, atol=atol))[0]
if idx.size == 0:
return None
v = vecs[:, idx[0]].real
n = np.linalg.norm(v)
return v / n if n > EPS else None
[ドキュメント]
def classify_and_geometry(R):
"""
3x3対称操作行列の種類と幾何学的情報を分類します。
詳細説明:
与えられた対称操作行列を恒等操作(I)、反転操作(Inv)、純粋回転(Rot)、
鏡映操作(Mir)、回反操作(Rinv)のいずれかに分類し、それぞれの幾何学的情報
(回転次数、回転軸、面法線)を返します。
行列式の符号や行列のべき乗の振る舞いを検査することで分類を行います。
:param R: 分類する3x3対称操作行列。
:type R: numpy.ndarray
:returns: (種類 (str), 次数 (int), 軸または法線 (numpy.ndarray or None))。
`kind` は 'I', 'Inv', 'Rot', 'Mir', 'Rinv', 'Unknown' のいずれか。
`order` は RotまたはRinvの場合の回転次数。
`axis_or_normal` は Rotの場合は回転軸、Mirの場合は面法線ベクトル。
:rtype: tuple
"""
R = orthogonalize_preserving_det(R)
det = np.linalg.det(R)
if np.allclose(R, np.eye(3), atol=1e-7): return 'I', 0, None
if np.allclose(R, -np.eye(3), atol=1e-7): return 'Inv', 0, None
if np.isclose(det, 1.0, atol=1e-7):
n = rotation_order(R)
if n == 1: return 'I', 0, None
axis = eigvec_for_value(R, 1.0)
return 'Rot', n, axis
if np.isclose(det, -1.0, atol=1e-7):
if np.allclose(R @ R, np.eye(3), atol=1e-6):
nrm = eigvec_for_value(R, -1.0)
return 'Mir', 0, nrm
n = rotation_order(-R)
axis = eigvec_for_value(-R, 1.0)
return 'Rinv', n, axis
return 'Unknown', 0, None
[ドキュメント]
def axis_label(v):
"""
ベクトルを軸ラベル(x, y, z)または座標文字列に変換します。
詳細説明:
与えられたベクトルが主要な結晶学的軸(x, y, z)のいずれかに
ほぼ平行である場合、対応する文字ラベルを返します。
そうでない場合は、ベクトルの丸められた座標値を文字列として返します。
:param v: ラベル化するベクトル。
:type v: numpy.ndarray or None
:returns: 軸ラベルまたは座標文字列。
:rtype: str
"""
if v is None: return "?"
v = v / (np.linalg.norm(v) + 1e-15)
for name, b in [('z', Z), ('x', X), ('y', Y)]:
if np.allclose(np.abs(np.dot(v, b)), 1.0, atol=1e-3):
return name
return f"{np.round(v[0],2)},{np.round(v[1],2)},{np.round(v[2],2)}"
[ドキュメント]
def default_label_of(R):
"""
対称操作行列のデフォルトの記号ラベルを生成します。
詳細説明:
`classify_and_geometry` 関数で分類された情報に基づいて、
対称操作の標準的な記号ラベル(例: "E", "i", "C3(z)", "m(⊥x)", "S6")を生成します。
:param R: ラベルを生成する3x3対称操作行列。
:type R: numpy.ndarray
:returns: 対称操作の記号ラベル。
:rtype: str
"""
kind, n, v = classify_and_geometry(R)
if kind == 'I': return "E"
if kind == 'Inv': return "i"
if kind == 'Rot': return f"C{n}({axis_label(v)})" if v is not None else f"C{n}"
if kind == 'Mir': return f"m(⊥{axis_label(v)})" if v is not None else "m"
if kind == 'Rinv':return f"S{n}({axis_label(v)})" if v is not None else f"S{n}"
return "?"
# ---------- “対称性優先”スコア(ジェネレータ抽出用) ----------
[ドキュメント]
def symmetry_priority_score(R):
"""
対称操作の「対称性優先」スコアを計算します。
詳細説明:
生成元を抽出する際に、特定の種類の対称操作(例: z軸回転、xy面鏡映)を
優先的に選択するためのスコアを割り当てます。
これは、結晶学で一般的に使用される慣習的な生成元の選択を反映するための
ヒューリスティックなスコアです。回転次数や軸の向きによってスコアが異なります。
:param R: スコアを計算する3x3対称操作行列。
:type R: numpy.ndarray
:returns: 対称操作の優先度スコア。
:rtype: float
"""
kind, n, v = classify_and_geometry(R)
score = 0.0
if kind == 'Rot' and v is not None:
az, ax, ay = abs(np.dot(v, Z)), abs(np.dot(v, X)), abs(np.dot(v, Y))
if az > 0.999: # z軸回転(xy面に垂直)
score = 1000 + 10 * n # 高次数ほど優先
elif ax > 0.999 or ay > 0.999:
score = 600 + 2 * n
else:
score = 200 + n
elif kind == 'Mir' and v is not None:
if abs(np.dot(v, Z)) > 0.999: # xy面鏡映
score = 900
elif abs(np.dot(v, X)) > 0.999 or abs(np.dot(v, Y)) > 0.999:
score = 550
else:
score = 150
elif kind == 'Rinv' and v is not None:
score = 120 + n
elif kind == 'Inv':
score = 50
elif kind == 'I':
score = 0
else:
score = 10
score += 0.001 * np.trace(orthogonalize_preserving_det(R)) # タイブレーク
return score
# ---------- “既知要素へのスナップ”(常に最近傍に写す) ----------
[ドキュメント]
def build_snapper(known_ops):
"""
既知の対称操作集合に任意の操作を「スナップ」させる関数を構築します。
詳細説明:
浮動小数点数演算に起因する微小な誤差を吸収するため、
与えられた既知の操作集合の中で、最も近い操作に任意の行列を「スナップ」させる
クロージャ関数を返します。これにより、グループ閉包の計算中に
同一の操作が異なる浮動小数点値で表現されることを防ぎます。
:param known_ops: 既知の対称操作行列のリスト。
:type known_ops: list[numpy.ndarray]
:returns: `snap(R) -> (numpy.ndarray, tuple)` というシグネチャを持つ関数。
`R` はスナップ対象の行列、戻り値はスナップされた行列と、その正規化されたタプル形式のキー。
:rtype: callable
"""
known = [orthogonalize_preserving_det(R) for R in known_ops]
known_keys = [canon(R) for R in known]
known_arr = np.stack(known, axis=0)
def snap(R):
Rn = orthogonalize_preserving_det(R)
diff = known_arr - Rn[None, :, :]
dists = np.linalg.norm(diff.reshape(len(known), -1), axis=1)
k = int(np.argmin(dists))
return known[k], known_keys[k]
return snap
# ---------- BFS 閉包(語つき、スナップ使用) ----------
[ドキュメント]
def closure_with_words(generators, target_size, snap):
"""
生成元からグループの閉包を計算し、各要素の語表現を追跡します。
詳細説明:
幅優先探索(BFS)を使用して、与えられた生成元セットから到達可能な
すべての対称操作(グループの閉包)を計算します。
各到達可能な操作について、それが恒等元からどのように生成されたかの「語」
(生成元のインデックス列)を記録します。`snap` 関数を使用して
数値的な安定性を確保します。
:param generators: 対称操作の生成元となる行列のリスト。
:type generators: list[numpy.ndarray]
:param target_size: 閉包がこのサイズに達したときに探索を停止する目安のサイズ。
:type target_size: int
:param snap: `build_snapper` で構築された、行列を既知の集合にスナップする関数。
:type snap: callable
:returns: (mats, words_idx, key_to_idx)。
`mats`: 閉包内のすべてのユニークな対称操作行列のリスト。
`words_idx`: `mats` の各操作に対応する生成元のインデックスの語表現(右掛け順)。
`key_to_idx`: 正規化された行列キーから `mats` リストのインデックスへのマッピング。
:rtype: tuple[list[numpy.ndarray], list[list[int]], dict[tuple, int]]
"""
I = np.eye(3)
I_snap, I_key = snap(I)
mats = [I_snap]
words_idx = [[]] # 恒等元は空語
key_to_idx = {I_key: 0}
q = deque([0])
gens_snap = [snap(G)[0] for G in generators]
while q:
i = q.popleft()
A = mats[i]; wA = words_idx[i]
for j, Gj in enumerate(gens_snap):
B_raw = A @ Gj
B, k = snap(B_raw)
if k not in key_to_idx:
key_to_idx[k] = len(mats)
mats.append(B)
words_idx.append(wA + [j]) # 右掛けでジェネレータ番号を追加
q.append(len(mats) - 1)
if len(mats) >= target_size:
return mats, words_idx, key_to_idx
return mats, words_idx, key_to_idx
[ドキュメント]
def compare_sets(setA, setB):
"""
2つの対称操作行列のセットが同一であるかを比較します。
詳細説明:
浮動小数点数の誤差を考慮して、2つの行列のリストが本質的に同じ要素の
セットを構成しているかを判断します。
各行列を `canon` 関数で正規化されたタプルに変換し、集合として比較します。
:param setA: 比較する対称操作行列のリストA。
:type setA: list[numpy.ndarray]
:param setB: 比較する対称操作行列のリストB。
:type setB: list[numpy.ndarray]
:returns: 両方のセットが同じ要素を含んでいればTrue、そうでなければFalse。
:rtype: bool
"""
return {canon(M) for M in setA} == {canon(M) for M in setB}
# ---------- generator 抽出 ----------
[ドキュメント]
def find_generators(all_ops):
"""
与えられたすべての操作から、点群の最小生成元セットを特定します。
詳細説明:
`symmetry_priority_score` に基づいて操作をソートし、貪欲法を用いて
生成元のセットを構築します。
まず、操作を優先順位で並べ、空の生成元セットから始めて、
まだ生成されていない操作を一つずつ追加していきます。
追加した生成元で構成される閉包が、以前の閉包よりも多くのユニークな操作を
生成する場合にのみ、その操作を生成元に採用します。
最後に、冗長な生成元を削除し、慣習的な順序に並べ替えます。
:param all_ops: 点群内のすべてのユニークな対称操作行列のリスト。
:type all_ops: list[numpy.ndarray]
:returns: (gens, snap)。
`gens`: 選択された生成元行列のリスト。
`snap`: この点群の操作集合にスナップする関数。
:rtype: tuple[list[numpy.ndarray], callable]
"""
ops_sorted = sorted(all_ops, key=lambda R: -symmetry_priority_score(R))
snap = build_snapper(all_ops)
gens = []
mats, _, _ = closure_with_words(gens, target_size=len(all_ops), snap=snap)
have = {canon(M) for M in mats}
target = {canon(R) for R in all_ops}
for R in ops_sorted:
trial_gens = gens + [R]
mats_t, _, _ = closure_with_words(trial_gens, target_size=len(all_ops), snap=snap)
if len({canon(M) for M in mats_t}) > len(have):
gens = trial_gens
mats, _, _ = closure_with_words(gens, target_size=len(all_ops), snap=snap)
have = {canon(M) for M in mats}
if have == target:
break
# 冗長削除
i = 0
while i < len(gens):
trial_g = gens[:i] + gens[i+1:]
mats_t, _, _ = closure_with_words(trial_g, target_size=len(all_ops), snap=snap)
if {canon(M) for M in mats_t} == target:
gens = trial_g
else:
i += 1
# 好みの順序:z軸回転, xy鏡映 を先頭へ
def prefer_key(R):
kind, n, v = classify_and_geometry(R)
zrot = (kind == 'Rot' and v is not None and abs(np.dot(v, Z)) > 0.999)
xym = (kind == 'Mir' and v is not None and abs(np.dot(v, Z)) > 0.999)
return (0 if zrot else 1 if xym else 2, -n, -symmetry_priority_score(R))
order = sorted(range(len(gens)), key=lambda i: prefer_key(gens[i]))
gens = [gens[i] for i in order]
return gens, snap
# ---------- 位数の推定(各 generator ごと) ----------
[ドキュメント]
def estimate_orders(generators, snap):
"""
各生成元の位数を推定します。
詳細説明:
各生成元について、その操作を繰り返し適用し、恒等操作に戻るまでの
最小回数(位数)を計算します。
`snap` 関数を使用して、累積的な浮動小数点誤差による判定のずれを防ぎます。
結晶点群の文脈では、位数は通常小さいため、最大12回までの繰り返しで探索します。
:param generators: 位数を推定する生成元行列のリスト。
:type generators: list[numpy.ndarray]
:param snap: 行列を既知の集合にスナップする関数。
:type snap: callable
:returns: 各生成元に対応する位数のリスト。見つからなかった場合はNone。
:rtype: list[int or None]
"""
orders = []
I = snap(np.eye(3))[0]
for G in generators:
A = snap(G)[0]
cur = I
for k in range(1, 13):
cur = snap(cur @ A)[0]
if canon(cur) == canon(I):
orders.append(k)
break
else:
orders.append(None) # 見つからない(鏡映など位数2以外はほぼ無いはず)
return orders
# ---------- 語の文字列化(指数表記、正の指数優先) ----------
# ---------- メイン処理 ----------
[ドキュメント]
def compute_generators_with_words(symbol, user_gen_labels=None):
"""
指定された点群の生成元とその語表現を計算します。
詳細説明:
Pymatgenを使用して指定された点群のすべての対称操作を取得し、重複を排除します。
次に、`find_generators` を呼び出して生成元を見つけ、`estimate_orders` で位数を推定します。
最後に、`closure_with_words` で各操作の語表現を計算し、`format_word` で整形します。
ユーザーが生成元にカスタムラベルを割り当てることも可能です。
:param symbol: 計算対象の点群のH–M記号(例: "6mm", "-3m")。
:type symbol: str
:param user_gen_labels: 生成元のインデックスをキーとし、カスタムラベルを値とする辞書。
:type user_gen_labels: dict[int, str] or None
:returns: (gens, gen_labels, gen_orders, all_ops, elems, ok)。
`gens`: 抽出された生成元行列のリスト。
`gen_labels`: 各生成元のラベル(自動生成またはユーザー指定)。
`gen_orders`: 各生成元の位数のリスト。
`all_ops`: 点群のすべてのユニークな対称操作行列のリスト。
`elems`: (操作行列, 語表現文字列, 分類ラベル) のタプルのリスト(恒等元を除く)。
`ok`: 生成元からの閉包が点群全体と一致したかを示すブール値。
:rtype: tuple[list[numpy.ndarray], list[str], list[int or None], list[numpy.ndarray], list[tuple[numpy.ndarray, str, str]], bool]
"""
if symbol not in PG_HM:
raise ValueError(f"Unknown point group: {symbol}")
pg = PointGroup(symbol)
all_ops_raw = [orthogonalize_preserving_det(op.rotation_matrix) for op in pg.symmetry_ops]
# 重複除去
seen, all_ops = set(), []
for R in all_ops_raw:
k = canon(R)
if k not in seen:
seen.add(k)
all_ops.append(R)
gens, snap = find_generators(all_ops)
# デフォルトラベル(自動推定)
auto_labels = [default_label_of(G) for G in gens]
# ユーザ指定があれば上書き
gen_labels = auto_labels
if user_gen_labels:
gen_labels = []
for i, G in enumerate(gens):
gen_labels.append(user_gen_labels.get(i, auto_labels[i]))
# 位数推定(指数正規化に使用)
gen_orders = estimate_orders(gens, snap)
# 語付き閉包
mats, words_idx, key_to_idx = closure_with_words(gens, target_size=len(all_ops), snap=snap)
ok = compare_sets(mats, all_ops)
# 各要素(恒等元を除く)を整形
elems = []
for R in all_ops:
k = canon(R)
idx = key_to_idx.get(k, None)
if idx is None:
continue
if len(words_idx[idx]) == 0:
continue # E はスキップ
word_str = format_word(words_idx[idx], gen_labels, gen_orders)
elems.append((R, word_str, default_label_of(R)))
return gens, gen_labels, gen_orders, all_ops, elems, ok
[ドキュメント]
def to_jsonable(R):
"""
NumPy行列をJSONシリアライズ可能な形式に変換します。
詳細説明:
NumPyの3x3行列の要素を、指定された精度(小数点以下10桁)で丸められた
浮動小数点数のリストのリストに変換します。
これにより、JSON出力に適した形式になります。
:param R: 変換する3x3行列。
:type R: numpy.ndarray
:returns: JSON形式で表現された行列。
:rtype: list[list[float]]
"""
return [[float(f"{v:.10f}") for v in row] for row in R]
[ドキュメント]
def parse_gen_labels(s):
"""
コマンドライン引数から生成元ラベルの辞書を解析します。
詳細説明:
"g1=C6(z),g2=m(⊥z)" のような文字列を受け取り、
生成元のインデックス(0から始まる)をキー、指定されたラベルを値とする辞書に変換します。
これにより、ユーザーがコマンドラインから生成元にカスタムラベルを適用できるようになります。
:param s: 解析するラベル文字列。
:type s: str
:returns: 生成元のインデックスとラベルのマッピング辞書。
:rtype: dict[int, str]
"""
mapping = {}
if not s:
return mapping
items = [x.strip() for x in s.split(',') if x.strip()]
for it in items:
if '=' not in it:
continue
k, v = it.split('=', 1)
k = k.strip().lower()
if k.startswith('g'):
try:
idx = int(k[1:]) - 1
except Exception:
continue
mapping[idx] = v.strip()
return mapping
[ドキュメント]
def main():
"""
スクリプトのエントリポイント。コマンドライン引数を解析し、点群の生成元と要素を出力します。
詳細説明:
`argparse` を使用してコマンドライン引数(点群の記号、カスタム生成元ラベル、JSON出力オプション)を解析します。
`compute_generators_with_words` を呼び出して点群情報を計算し、その結果を整形して標準出力に表示します。
JSONオプションが指定されている場合は、追加でJSON形式の出力も行います。
:param: なし
:returns: なし。スクリプトの実行結果を標準出力に出力します。
:rtype: None
"""
ap = argparse.ArgumentParser(
description=(
"Point-group generators with words using positive exponents (e.g., C3 = C6^2, C6^5). "
"You can predefine generator symbols like 'g1=C6(z),g2=m(⊥z)'."
),
formatter_class=argparse.RawTextHelpFormatter
)
ap.add_argument("--pg", "-p", required=True, help="Point group symbol (H–M), e.g. 6, 4mm, 6mm, -3m")
ap.add_argument("--gen-labels", type=str, default="",
help="Comma-separated labels, e.g. 'g1=C6(z),g2=m(⊥z)'")
ap.add_argument("--json", action="store_true", help="Also output JSON")
args = ap.parse_args()
user_labels = parse_gen_labels(args.gen_labels)
try:
gens, gen_labels, gen_orders, all_ops, elems, ok = compute_generators_with_words(
args.pg, user_gen_labels=user_labels
)
except Exception as e:
print(f"Error: {e}")
sys.exit(1)
print(f"--- Point Group {args.pg} ---")
print(f"Total operations: {len(all_ops)}")
print(f"Verification (⟨gens⟩ == group): {'OK' if ok else 'NG'}\n")
# Generators
print("Generators (preferred order):")
for i, (lbl, G, n) in enumerate(zip(gen_labels, gens, gen_orders), 1):
ord_txt = f" (order={n})" if n is not None else ""
print(f" g{i} = {lbl}{ord_txt}")
print(np.array2string(G, formatter={'float_kind': lambda x: f"{x:9.6f}"}))
print()
# Elements (exclude identity)
print("Generated elements (excluding identity):")
for i, (R, word, cls_lbl) in enumerate(elems, 1):
print(f" O{i:02d}: {word} => {cls_lbl}")
print(np.array2string(R, formatter={'float_kind': lambda x: f"{x:9.6f}"}))
print()
if args.json:
out = {
"point_group": args.pg,
"total_ops": len(all_ops),
"verified": bool(ok),
"generators": [
{"name": f"g{i+1}", "label": lbl, "order": gen_orders[i], "matrix": to_jsonable(G)}
for i, (lbl, G) in enumerate(zip(gen_labels, gens))
],
"elements": [
{"name": f"O{i+1}", "word": word, "class": cls_lbl, "matrix": to_jsonable(R)}
for i, (R, word, cls_lbl) in enumerate(elems)
],
}
print(json.dumps(out, ensure_ascii=False, indent=2))
if __name__ == "__main__":
main()
input("\nPress ENTER to terminate>>\n")