xrd_fit_xrayutilities.py ダウンロード/コピー

xrd_fit_xrayutilities.py をダウンロード

xrd_fit_xrayutilities.py
xrd_fit_xrayutilities.py
   1r"""
   2xrd_fit.py: X線回折(XRD)シミュレーションおよびフィッティングツール
   3
   4このモジュールは、xrayutilitiesライブラリを使用して動的理論に基づいたXRDシミュレーションを実行し、
   5実験データに対して構造パラメータ(組成、緩和度、膜厚)を最適化します。
   6また、フリンジ解析による膜厚の自動推定機能も備えています。
   7
   8関連リンク: :doc:`xrd_fit_usage`
   9"""
  10
  11import argparse
  12import csv
  13import os
  14import random
  15import sys
  16import traceback
  17
  18import numpy as np
  19import matplotlib.pyplot as plt
  20import xrayutilities as xu
  21from scipy.optimize import minimize
  22from scipy.signal import savgol_filter, find_peaks
  23
  24
  25# ============================================================
  26# 定数 / デフォルト値
  27# ============================================================
  28DEFAULT_MODE = "sim"
  29DEFAULT_METHOD = "pso"
  30DEFAULT_INFILE = ""
  31DEFAULT_FIX = ""
  32
  33DEFAULT_SUBSTRATE_FILE = "GaN.cif"
  34DEFAULT_FILM_FILE = "AlN.cif"
  35
  36DEFAULT_NMAXITER = 1000
  37DEFAULT_TOL = 1e-7
  38DEFAULT_YSCALE = "log"
  39DEFAULT_RESIDUAL_SCALE = "log"
  40
  41TARGET_X = 0.2
  42TARGET_RELAX = 0.0
  43TARGET_THICK = 1500.0
  44
  45TRY_X = 0.0
  46TRY_RELAX = 1.0
  47TRY_THICK = 1500.0
  48
  49TWOTHETA_MIN = 32.0
  50TWOTHETA_MAX = 38.0
  51NPOINTS = 200
  52
  53RESOLUTION_WIDTH = 0.0001
  54HKL = (0, 0, 2)
  55
  56LIMIT = 0.01
  57MAX_STALL = 5000
  58
  59NINTERVAL_SAVE = 100
  60NINTERVAL_PRINT = 10
  61NINTERVAL_PLOT = 10
  62
  63XRANGE = 1.0
  64RRANGE = 1.0
  65TRANGE = 10.0
  66
  67PARAMETER_NAMES = ["x", "relax", "thick"]
  68DEFAULT_PRIMARY_SET = "best0"
  69DEFAULT_PSO_PREFIX = "pso"
  70
  71DEFAULT_GUESS_LOW_DEG = 16.0
  72DEFAULT_GUESS_HIGH_DEG = 17.3
  73DEFAULT_NSMOOTH_POINTS = 31
  74DEFAULT_NSMOOTH_ORDER = 3
  75DEFAULT_NQ_UNIFORM = 4096
  76DEFAULT_NGUESS_KEEP = 5
  77
  78DEFAULT_PSO_NPARTICLES = 12
  79DEFAULT_PSO_W = 0.72
  80DEFAULT_PSO_C1 = 1.49
  81DEFAULT_PSO_C2 = 1.49
  82
  83DEFAULT_CLUSTER_GAP_FACTOR = 1.6
  84
  85
  86# ============================================================
  87# argparse
  88# ============================================================
  89def build_parser():
  90    """
  91    コマンドライン引数を解析するためのパーサーを構築します。
  92
  93    詳細説明:
  94        この関数はargparseモジュールを使用して、XRDシミュレーション、フィッティング、
  95        および膜厚推定ツールが受け入れるコマンドライン引数を定義します。
  96        各引数にはデフォルト値、選択肢、ヘルプメッセージが含まれます。
  97
  98    Returns:
  99        :returns: argparse.ArgumentParser: 設定済みのパーサーオブジェクト。
 100    """
 101    parser = argparse.ArgumentParser(
 102        description="動的理論に基づくXRDシミュレーション、フィッティング、および膜厚推定"
 103    )
 104    parser.add_argument(
 105        "--mode",
 106        default=DEFAULT_MODE,
 107        choices=["sim", "read", "fit", "guess"],
 108        help="実行モード: sim (シミュレーション), read (読込), fit (最適化), guess (膜厚推定)",
 109    )
 110    parser.add_argument(
 111        "--method",
 112        default=DEFAULT_METHOD,
 113        choices=["random", "nelder-mead", "bfgs", "cg", "pso"],
 114        help="最適化手法。fitモード時に有効",
 115    )
 116    parser.add_argument(
 117        "--infile",
 118        default=DEFAULT_INFILE,
 119        help='2theta-intensityデータを含むテキストファイルへのパス',
 120    )
 121    parser.add_argument(
 122        "--substrate_file",
 123        default=DEFAULT_SUBSTRATE_FILE,
 124        help=f'基板のCIFファイルパス (デフォルト: {DEFAULT_SUBSTRATE_FILE})',
 125    )
 126    parser.add_argument(
 127        "--film_file",
 128        default=DEFAULT_FILM_FILE,
 129        help=f'膜のCIFファイルパス (デフォルト: {DEFAULT_FILM_FILE})',
 130    )
 131    parser.add_argument(
 132        "--fix",
 133        default=DEFAULT_FIX,
 134        help='固定するパラメータ(カンマ区切り、例: "x,relax")',
 135    )
 136    parser.add_argument(
 137        "--nmaxiter",
 138        type=int,
 139        default=DEFAULT_NMAXITER,
 140        help=f"最大反復回数 (デフォルト: {DEFAULT_NMAXITER})",
 141    )
 142    parser.add_argument(
 143        "--tol",
 144        type=float,
 145        default=DEFAULT_TOL,
 146        help=f"収束判定の許容誤差 (デフォルト: {DEFAULT_TOL})",
 147    )
 148    parser.add_argument(
 149        "--yscale",
 150        default=DEFAULT_YSCALE,
 151        choices=["linear", "log"],
 152        help=f'プロットのY軸スケール (デフォルト: "{DEFAULT_YSCALE}")',
 153    )
 154    parser.add_argument(
 155        "--residual_scale",
 156        default=DEFAULT_RESIDUAL_SCALE,
 157        choices=["linear", "log"],
 158        help=f'残差計算に使用するスケール (デフォルト: "{DEFAULT_RESIDUAL_SCALE}")',
 159    )
 160    parser.add_argument(
 161        "--guess_low",
 162        type=float,
 163        default=DEFAULT_GUESS_LOW_DEG,
 164        help="guessモードでの解析開始角度 [deg]",
 165    )
 166    parser.add_argument(
 167        "--guess_high",
 168        type=float,
 169        default=DEFAULT_GUESS_HIGH_DEG,
 170        help="guessモードでの解析終了角度 [deg]",
 171    )
 172    parser.add_argument(
 173        "--nsmooth_points",
 174        type=int,
 175        default=DEFAULT_NSMOOTH_POINTS,
 176        help="guessモードでの平滑化ウィンドウ点数",
 177    )
 178    parser.add_argument(
 179        "--nsmooth_order",
 180        type=int,
 181        default=DEFAULT_NSMOOTH_ORDER,
 182        help="guessモードでの平滑化多項式次数",
 183    )
 184    parser.add_argument(
 185        "--nguess_keep",
 186        type=int,
 187        default=DEFAULT_NGUESS_KEEP,
 188        help="guessモードで保持する膜厚候補の数",
 189    )
 190    parser.add_argument(
 191        "--cluster_gap_factor",
 192        type=float,
 193        default=DEFAULT_CLUSTER_GAP_FACTOR,
 194        help=f"フリンジクラスター判定用のギャップ係数 (デフォルト: {DEFAULT_CLUSTER_GAP_FACTOR})",
 195    )
 196    parser.add_argument(
 197        "--pso_nparticles",
 198        type=int,
 199        default=DEFAULT_PSO_NPARTICLES,
 200        help=f"PSOの粒子数 (デフォルト: {DEFAULT_PSO_NPARTICLES})",
 201    )
 202    parser.add_argument(
 203        "--pso_w",
 204        type=float,
 205        default=DEFAULT_PSO_W,
 206        help=f"PSOの慣性重み (デフォルト: {DEFAULT_PSO_W})",
 207    )
 208    parser.add_argument(
 209        "--pso_c1",
 210        type=float,
 211        default=DEFAULT_PSO_C1,
 212        help=f"PSOの自己学習係数 (デフォルト: {DEFAULT_PSO_C1})",
 213    )
 214    parser.add_argument(
 215        "--pso_c2",
 216        type=float,
 217        default=DEFAULT_PSO_C2,
 218        help=f"PSOの社会学習係数 (デフォルト: {DEFAULT_PSO_C2})",
 219    )
 220    parser.add_argument(
 221        "--pso_stall_max",
 222        type=int,
 223        default=15,
 224        help="改善がない場合にPSOを停止する最大反復数",
 225    )
 226    parser.add_argument(
 227        "--pso_spread_rtol",
 228        type=float,
 229        default=0.10,
 230        help="粒子の分散に基づく停止条件の相対許容誤差",
 231    )
 232    return parser
 233
 234
 235def initialize(parser):
 236    """
 237    コマンドライン引数を解析し、結果を返します。
 238
 239    Parameters:
 240        :param parser: argparse.ArgumentParser: argparse.ArgumentParserオブジェクト。
 241    Returns:
 242        :returns: argparse.Namespace: 解析された引数を含むオブジェクト。
 243    """
 244    return parser.parse_args()
 245
 246
 247# ============================================================
 248# ファイル名ヘルパー
 249# ============================================================
 250def get_parameter_filename(infile):
 251    """
 252    入力ファイル名に基づいてパラメータ保存用のCSVファイル名を生成します。
 253
 254    詳細説明:
 255        入力ファイル名が存在する場合はそのファイル名から拡張子を除いた名前に "-parameters.csv" を追加します。
 256        入力ファイル名が空の場合は "parameters.csv" を返します。
 257
 258    Parameters:
 259        :param infile: str: 入力データファイル名。
 260
 261    Returns:
 262        :returns: str: パラメータCSVファイル名。
 263    """
 264    if infile == "":
 265        return "parameters.csv"
 266    filebody, _ = os.path.splitext(infile)
 267    return f"{filebody}-parameters.csv"
 268
 269
 270def get_guess_peaks_filename(infile):
 271    """
 272    入力ファイル名に基づいて膜厚推定モードで検出したピークの保存用CSVファイル名を生成します。
 273
 274    詳細説明:
 275        入力ファイル名が存在する場合はそのファイル名から拡張子を除いた名前に "-guess-peaks.csv" を追加します。
 276        入力ファイル名が空の場合は "guess-peaks.csv" を返します。
 277
 278    Parameters:
 279        :param infile: str: 入力データファイル名。
 280
 281    Returns:
 282        :returns: str: ピークデータCSVファイル名。
 283    """
 284    if infile == "":
 285        return "guess-peaks.csv"
 286    filebody, _ = os.path.splitext(infile)
 287    return f"{filebody}-guess-peaks.csv"
 288
 289
 290def get_guess_clusters_filename(infile):
 291    """
 292    入力ファイル名に基づいて膜厚推定モードで解析したクラスターの保存用CSVファイル名を生成します。
 293
 294    詳細説明:
 295        入力ファイル名が存在する場合はそのファイル名から拡張子を除いた名前に "-guess-clusters.csv" を追加します。
 296        入力ファイル名が空の場合は "guess-clusters.csv" を返します。
 297
 298    Parameters:
 299        :param infile: str: 入力データファイル名。
 300
 301    Returns:
 302        :returns: str: クラスターデータCSVファイル名。
 303    """
 304    if infile == "":
 305        return "guess-clusters.csv"
 306    filebody, _ = os.path.splitext(infile)
 307    return f"{filebody}-guess-clusters.csv"
 308
 309
 310def parse_fix_list(fix_str):
 311    """
 312    固定パラメータの文字列をリストに変換します。
 313
 314    詳細説明:
 315        カンマ区切りの文字列を受け取り、各パラメータ名を小文字に変換し、
 316        空白を除去したリストとして返します。空の文字列は空のリストを返します。
 317
 318    Parameters:
 319        :param fix_str: str: カンマ区切りのパラメータ名文字列(例: "x,relax")。
 320
 321    Returns:
 322        :returns: list[str]: 小文字に統一された固定するパラメータ名のリスト。
 323    """
 324    if fix_str.strip() == "":
 325        return []
 326    return [s.strip().lower() for s in fix_str.split(",") if s.strip() != ""]
 327
 328
 329# ============================================================
 330# パラメータユーティリティ
 331# ============================================================
 332def make_default_parameter_set():
 333    """
 334    単一のパラメータセット(組成、緩和度、膜厚)の初期辞書を作成します。
 335
 336    詳細説明:
 337        各パラメータには 'value' (現在の値) と 'optid' (最適化対象フラグ: 1=対象, 0=固定) が含まれます。
 338        初期値は定数 `TRY_X`, `TRY_RELAX`, `TRY_THICK` から取得され、
 339        全て最適化対象として設定されます。
 340
 341    Returns:
 342        :returns: dict[str, dict[str, float or int]]: 初期値と最適化フラグを含むパラメータ辞書。
 343            例: `{"x": {"value": 0.0, "optid": 1}, ...}`
 344    """
 345    return {
 346        "x": {"value": TRY_X, "optid": 1},
 347        "relax": {"value": TRY_RELAX, "optid": 1},
 348        "thick": {"value": TRY_THICK, "optid": 1},
 349    }
 350
 351
 352def clone_parameter_set(param_set):
 353    """
 354    指定されたパラメータセットを深くコピーします。
 355
 356    詳細説明:
 357        元のパラメータセットの内容を変更せずに新しい独立した辞書を作成するために使用されます。
 358        これにより、最適化プロセス中にパラメータが不意に変更されるのを防ぎます。
 359
 360    Parameters:
 361        :param param_set: dict: コピーするパラメータセット。
 362
 363    Returns:
 364        :returns: dict: コピーされたパラメータセット。
 365    """
 366    out = {}
 367    for name in param_set:
 368        out[name] = {
 369            "value": float(param_set[name]["value"]),
 370            "optid": int(param_set[name]["optid"]),
 371        }
 372    return out
 373
 374
 375def make_default_parameter_sets():
 376    """
 377    デフォルトのプライマリパラメータセットを含む、複数のパラメータセットを管理する辞書を作成します。
 378
 379    詳細説明:
 380        `DEFAULT_PRIMARY_SET` (通常 'best0') というキーで、
 381        `make_default_parameter_set()` で生成された初期パラメータセットを保持します。
 382        これは、複数の最適化粒子や候補を管理する際に基盤となる構造を提供します。
 383
 384    Returns:
 385        :returns: dict[str, dict]: デフォルトのプライマリセットを含むパラメータ辞書全体の構造。
 386    """
 387    return {DEFAULT_PRIMARY_SET: make_default_parameter_set()}
 388
 389
 390def apply_fix_list(param_sets, fix_list):
 391    """
 392    指定されたパラメータの最適化フラグをオフ(0)に設定します。
 393
 394    詳細説明:
 395        `param_sets` 内の全てのパラメータセットに対して、`fix_list` に含まれるパラメータの
 396        `optid` (最適化対象フラグ) を0に設定し、最適化から除外します。
 397
 398    Parameters:
 399        :param param_sets: dict: 変更対象のパラメータ辞書 (複数のセットを含む)。
 400        :param fix_list: list[str]: 固定するパラメータ名のリスト。
 401    """
 402    for set_name in param_sets:
 403        for varname in fix_list:
 404            if varname in param_sets[set_name]:
 405                param_sets[set_name][varname]["optid"] = 0
 406
 407
 408def read_parameters(parameter_file):
 409    """
 410    CSVファイルからパラメータ設定を読み込みます。ファイルが存在しない場合はデフォルト値を返します。
 411
 412    詳細説明:
 413        指定されたCSVファイルからパラメータ名、値、最適化IDを読み込み、
 414        `make_default_parameter_sets` の形式に沿った辞書構造を構築します。
 415        CSVファイルのヘッダーが不正な場合や、ファイルが空の場合は、警告を出力し
 416        デフォルトのパラメータセットを返します。
 417
 418    Parameters:
 419        :param parameter_file: str: パラメータCSVファイルへのパス。
 420
 421    Returns:
 422        :returns: dict[str, dict]: 読み込まれたパラメータセットの辞書。
 423            ファイルが存在しない場合や無効な場合は、デフォルトのパラメータセットが返されます。
 424    """
 425    param_sets = make_default_parameter_sets()
 426
 427    if not os.path.isfile(parameter_file):
 428        return param_sets
 429
 430    rows = []
 431    with open(parameter_file, "r", newline="", encoding="utf-8") as f:
 432        reader = csv.reader(f)
 433        for row in reader:
 434            rows.append(row)
 435
 436    if len(rows) == 0:
 437        return param_sets
 438
 439    header = rows[0]
 440    if len(header) < 3 or header[0] != "varname" or header[1] != "value" or header[2] != "optid":
 441        print(f"Warning: invalid parameter CSV header in {parameter_file}. Use defaults.")
 442        return param_sets
 443
 444    loaded = {}
 445    for row in rows[1:]:
 446        if len(row) < 3:
 447            continue
 448
 449        varname_raw = row[0].strip()
 450        value_str = row[1].strip()
 451        optid_str = row[2].strip()
 452
 453        if varname_raw == "":
 454            continue
 455
 456        if ":" in varname_raw:
 457            set_name, varname = varname_raw.split(":", 1)
 458            set_name = set_name.strip()
 459            varname = varname.strip()
 460        else:
 461            set_name = DEFAULT_PRIMARY_SET
 462            varname = varname_raw
 463
 464        if set_name == "" or varname == "":
 465            continue
 466        if varname not in PARAMETER_NAMES:
 467            continue
 468
 469        try:
 470            value = float(value_str)
 471            optid = int(optid_str)
 472        except ValueError:
 473            continue
 474
 475        if set_name not in loaded:
 476            loaded[set_name] = make_default_parameter_set()
 477
 478        loaded[set_name][varname]["value"] = value
 479        loaded[set_name][varname]["optid"] = 1 if optid != 0 else 0
 480
 481    if len(loaded) > 0:
 482        param_sets = loaded
 483
 484    if DEFAULT_PRIMARY_SET not in param_sets:
 485        param_sets[DEFAULT_PRIMARY_SET] = make_default_parameter_set()
 486
 487    return param_sets
 488
 489
 490def save_parameters(parameter_file, param_sets):
 491    """
 492    現在のパラメータセットをCSVファイルに書き出します。
 493
 494    詳細説明:
 495        `param_sets` 辞書に格納されている全てのパラメータ(複数のセットを含む)を、
 496        "varname", "value", "optid" のヘッダーを持つCSV形式で指定されたファイルに保存します。
 497        各パラメータ名は `set_name:varname` の形式で保存されます。
 498
 499    Parameters:
 500        :param parameter_file: str: パラメータを保存するファイルパス。
 501        :param param_sets: dict[str, dict]: 保存するパラメータデータ。
 502    """
 503    rows = [["varname", "value", "optid"]]
 504
 505    set_names = sorted(param_sets.keys())
 506    for set_name in set_names:
 507        for varname in PARAMETER_NAMES:
 508            entry = param_sets[set_name][varname]
 509            rows.append(
 510                [
 511                    f"{set_name}:{varname}",
 512                    f"{entry['value']:.16g}",
 513                    str(int(entry["optid"])),
 514                ]
 515            )
 516
 517    with open(parameter_file, "w", newline="", encoding="utf-8") as f:
 518        writer = csv.writer(f)
 519        writer.writerows(rows)
 520
 521
 522def save_guess_peak_csv(outfile, peak_rows):
 523    """
 524    膜厚推定モードで検出したピーク情報をCSVファイルに保存します。
 525
 526    Parameters:
 527        :param outfile: str: 保存先ファイルパス。
 528        :param peak_rows: list[dict]: ピーク情報を含む辞書のリスト。
 529            各辞書は `cluster_id`, `n`, `q`, `twotheta`, `used_stage1`, `used_stage2`,
 530            `resid_stage1`, `resid_stage2` のキーを持つと期待されます。
 531    """
 532    rows = [["cluster_id", "n", "q", "2theta", "used_stage1", "used_stage2", "resid_stage1", "resid_stage2"]]
 533    for row in peak_rows:
 534        rows.append(
 535            [
 536                str(row["cluster_id"]),
 537                str(row["n"]),
 538                f"{row['q']:.16g}",
 539                f"{row['twotheta']:.16g}",
 540                str(int(row["used_stage1"])),
 541                str(int(row["used_stage2"])),
 542                f"{row['resid_stage1']:.16g}",
 543                f"{row['resid_stage2']:.16g}",
 544            ]
 545        )
 546
 547    with open(outfile, "w", newline="", encoding="utf-8") as f:
 548        writer = csv.writer(f)
 549        writer.writerows(rows)
 550
 551
 552def save_guess_cluster_csv(outfile, cluster_rows):
 553    """
 554    膜厚推定モードで解析したクラスター(膜厚候補)情報をCSVファイルに保存します。
 555
 556    Parameters:
 557        :param outfile: str: 保存先ファイルパス。
 558        :param cluster_rows: list[dict]: クラスター情報を含む辞書のリスト。
 559            各辞書は `cluster_id`, `npeaks`, `dq_est`, `thick_fft`, `thick_reg`, `confidence`
 560            のキーを持つと期待されます。
 561    """
 562    rows = [["cluster_id", "npeaks", "dq_est", "thick_fft", "thick_reg", "confidence"]]
 563    for row in cluster_rows:
 564        rows.append(
 565            [
 566                str(row["cluster_id"]),
 567                str(row["npeaks"]),
 568                f"{row['dq_est']:.16g}",
 569                f"{row['thick_fft']:.16g}",
 570                f"{row['thick_reg']:.16g}",
 571                f"{row['confidence']:.16g}",
 572            ]
 573        )
 574    with open(outfile, "w", newline="", encoding="utf-8") as f:
 575        writer = csv.writer(f)
 576        writer.writerows(rows)
 577
 578
 579def parameter_set_to_values(param_set):
 580    """
 581    パラメータセット辞書から組成(x)、緩和度(relax)、膜厚(thick)のタプルを抽出します。
 582
 583    Parameters:
 584        :param param_set: dict: パラメータセット辞書。
 585
 586    Returns:
 587        :returns: tuple[float, float, float]: (x, relax, thick) の値のタプル。
 588    """
 589    return (
 590        float(param_set["x"]["value"]),
 591        float(param_set["relax"]["value"]),
 592        float(param_set["thick"]["value"]),
 593    )
 594
 595
 596def get_optimize_names(param_set):
 597    """
 598    指定されたパラメータセット内で最適化対象(optid=1)となっているパラメータ名のリストを取得します。
 599
 600    Parameters:
 601        :param param_set: dict: パラメータセット辞書。
 602
 603    Returns:
 604        :returns: list[str]: 最適化対象のパラメータ名("x", "relax", "thick")のリスト。
 605    """
 606    names = []
 607    for name in PARAMETER_NAMES:
 608        if int(param_set[name]["optid"]) != 0:
 609            names.append(name)
 610    return names
 611
 612
 613def pack_optimize_values(param_set, optimize_names):
 614    """
 615    最適化対象のパラメータの値をNumPy配列にパックします。
 616
 617    Parameters:
 618        :param param_set: dict: 基準となるパラメータセット。
 619        :param optimize_names: list[str]: 最適化対象のパラメータ名のリスト。
 620
 621    Returns:
 622        :returns: numpy.ndarray: 最適化対象のパラメータ値を含むNumPy配列。
 623    """
 624    return np.array([param_set[name]["value"] for name in optimize_names], dtype=float)
 625
 626
 627def unpack_optimize_values(base_param_set, optimize_names, xvec):
 628    """
 629    NumPy配列の値を、指定されたパラメータ名の順序に従って元のパラメータ辞書形式に戻します。
 630
 631    詳細説明:
 632        `base_param_set` をクローンし、`optimize_names` の順序で `xvec` の値で更新します。
 633        これにより、最適化ルーチンから返された値をパラメータセットに適用できます。
 634
 635    Parameters:
 636        :param base_param_set: dict: 基準となるパラメータセット(最適化対象外の値を保持するために使用)。
 637        :param optimize_names: list[str]: 最適化対象のパラメータ名のリスト。
 638        :param xvec: numpy.ndarray: 最適化によって得られたパラメータ値の配列。
 639
 640    Returns:
 641        :returns: dict: 更新されたパラメータセット。
 642    """
 643    work = clone_parameter_set(base_param_set)
 644    for i, name in enumerate(optimize_names):
 645        work[name]["value"] = float(xvec[i])
 646    return work
 647
 648
 649def set_best_parameter_set(param_sets, param_set):
 650    """
 651    現在の最良のパラメータセットを `param_sets` 辞書内の `DEFAULT_PRIMARY_SET` キーに保存します。
 652
 653    詳細説明:
 654        これにより、最適化の進捗に合わせて最良の解が常に追跡され、
 655        プログラムの実行終了時や中断時に最新の最良パラメータが保存されます。
 656
 657    Parameters:
 658        :param param_sets: dict: 複数のパラメータセットを管理する辞書。
 659        :param param_set: dict: 最良とみなされる単一のパラメータセット。
 660    """
 661    param_sets[DEFAULT_PRIMARY_SET] = clone_parameter_set(param_set)
 662
 663
 664def replace_pso_parameter_sets(param_sets, candidate_sets):
 665    """
 666    PSOモード用の粒子パラメータセットを更新します。
 667
 668    詳細説明:
 669        `param_sets` からPSO関連の既存のパラメータセットを削除し、
 670        `candidate_sets` から新しいPSO粒子パラメータセットを追加します。
 671        PSOのプレフィックスを持たないセットは保持されます。
 672
 673    Parameters:
 674        :param param_sets: dict: 現在の全てのパラメータセット。
 675        :param candidate_sets: list[dict]: PSOの新しい粒子パラメータセットのリスト。
 676
 677    Returns:
 678        :returns: dict: PSO粒子パラメータが更新された新しいパラメータセット辞書。
 679    """
 680    keep_names = [name for name in param_sets.keys() if not name.startswith(DEFAULT_PSO_PREFIX)]
 681    new_sets = {}
 682    for name in keep_names:
 683        new_sets[name] = clone_parameter_set(param_sets[name])
 684
 685    for i, param_set in enumerate(candidate_sets):
 686        set_name = f"{DEFAULT_PSO_PREFIX}{i}"
 687        new_sets[set_name] = clone_parameter_set(param_set)
 688
 689    if DEFAULT_PRIMARY_SET not in new_sets:
 690        new_sets[DEFAULT_PRIMARY_SET] = make_default_parameter_set()
 691
 692    return new_sets
 693
 694
 695def get_pso_parameter_sets(param_sets):
 696    """
 697    `DEFAULT_PSO_PREFIX` (通常 "pso") で始まるキーを持つパラメータセットを全て取得します。
 698
 699    詳細説明:
 700        これはPSOアルゴリズムで使用される各粒子の位置を初期化または取得するために使用されます。
 701        取得されたセットは、元の辞書から独立したコピーとして提供されます。
 702
 703    Parameters:
 704        :param param_sets: dict: 複数のパラメータセットを管理する辞書。
 705
 706    Returns:
 707        :returns: list[dict]: PSO粒子として使用されるパラメータセットのリスト。
 708    """
 709    names = sorted([name for name in param_sets.keys() if name.startswith(DEFAULT_PSO_PREFIX)])
 710    return [clone_parameter_set(param_sets[name]) for name in names]
 711
 712
 713# ============================================================
 714# 材料 / モデル準備
 715# ============================================================
 716def prepare_materials(substrate_file, film_file):
 717    """
 718    CIFファイルから基板と膜の材料オブジェクトを準備し、弾性定数を設定します。
 719
 720    詳細説明:
 721        指定されたCIFファイルパスからX-rayUtilitiesの `Crystal` オブジェクトをロードします。
 722        材料がHexagonal対称性を持つことを確認し、もし弾性テンソルが定義されていない場合は
 723        デフォルトの六方晶系弾性定数を割り当てて、歪み計算が続行できるようにします。
 724        ファイルが見つからない場合や読み込みエラーが発生した場合は、プログラムを終了します。
 725
 726    Parameters:
 727        :param substrate_file: str: 基板のCIFファイルパス。
 728        :param film_file: str: 膜のCIFファイルパス。
 729
 730    Returns:
 731        :returns: tuple[xrayutilities.materials.Crystal, xrayutilities.materials.Crystal]:
 732            (substrate, film) の材料オブジェクト。
 733    """
 734    if not os.path.isfile(substrate_file):
 735        print(f"Error: substrate_file not found : {substrate_file}")
 736        sys.exit(1)
 737
 738    if not os.path.isfile(film_file):
 739        print(f"Error: film_file not found : {film_file}")
 740        sys.exit(1)
 741
 742    try:
 743        substrate = xu.materials.Crystal.fromCIF(substrate_file)
 744    except Exception as e:
 745        print(f"Error reading substrate CIF : {substrate_file}")
 746        print(e)
 747        sys.exit(1)
 748
 749    try:
 750        film = xu.materials.Crystal.fromCIF(film_file)
 751    except Exception as e:
 752        print(f"Error reading film CIF : {film_file}")
 753        print(e)
 754        sys.exit(1)
 755
 756    substrate.symm_class_name = "Hexagonal"
 757    film.symm_class_name = "Hexagonal"
 758
 759    def ensure_elastic(material, name, default_cij):
 760        """
 761        材料オブジェクトに弾性定数が定義されていることを確認し、
 762        存在しない場合はデフォルト値を割り当てます。
 763
 764        Parameters:
 765            :param material: xrayutilities.materials.Crystal: 弾性定数をチェックする材料オブジェクト。
 766            :param name: str: 材料の名前(警告メッセージ用)。
 767            :param default_cij: tuple[float, float, float, float, float]: デフォルトの六方晶系弾性定数 (C11, C12, C13, C33, C44) のタプル。
 768        """
 769        has_elastic = True
 770        try:
 771            material.elastic
 772        except Exception:
 773            has_elastic = False
 774
 775        try:
 776            material.cijkl
 777        except Exception:
 778            has_elastic = False
 779
 780        if has_elastic:
 781            return
 782
 783        print()
 784        print(f"Warning: elastic tensor not defined for {name}")
 785        print("Strain calculation may be inaccurate.")
 786        print("Default hexagonal elastic constants will be used and calculation continues.")
 787        print()
 788
 789        c11, c12, c13, c33, c44 = default_cij
 790        cij = xu.materials.material.HexagonalElasticTensor(c11, c12, c13, c33, c44)
 791        material.elastic = cij
 792        material.cijkl = xu.materials.material.Cij2Cijkl(cij)
 793
 794    ensure_elastic(substrate, "substrate", (390, 145, 106, 398, 105))
 795    ensure_elastic(film, "film", (396, 137, 108, 373, 116))
 796
 797    return substrate, film
 798
 799
 800def make_scan_axis():
 801    """
 802    XRDシミュレーション用のデフォルトの角度軸 (2theta, omega) を生成します。
 803
 804    詳細説明:
 805        `TWOTHETA_MIN` から `TWOTHETA_MAX` の範囲で `NPOINTS` の数の2theta角度を生成し、
 806        それに対応するomega角度 (2thetaの半分) を計算します。
 807
 808    Returns:
 809        :returns: tuple[numpy.ndarray, numpy.ndarray]: (2theta角度の配列 [deg], omega角度の配列 [deg])。
 810    """
 811    twotheta = np.linspace(TWOTHETA_MIN, TWOTHETA_MAX, NPOINTS)
 812    omega = twotheta / 2.0
 813    return twotheta, omega
 814
 815
 816def model(substrate, film, x, relax, thick, energy):
 817    """
 818    指定されたパラメータで動的XRDシミュレーションモデルを構築します。
 819
 820    詳細説明:
 821        X-rayUtilitiesの `Layer` オブジェクトと `PseudomorphicStack001` を使用して、
 822        基板と膜からなるスタックの動的理論モデルを定義します。
 823        膜は基板と膜の合金であり、組成 `x`、緩和度 `relax`、膜厚 `thick` を持ちます。
 824
 825    Parameters:
 826        :param substrate: xrayutilities.materials.Crystal: 基板材料オブジェクト。
 827        :param film: xrayutilities.materials.Crystal: 膜材料オブジェクト(合金のもう一方の端成分)。
 828        :param x: float: 合金の組成比 (0.0 から 1.0)。
 829        :param relax: float: 膜の緩和度 (0.0 は完全擬似格子、1.0 は完全緩和)。
 830        :param thick: float: 膜の厚さ [Å]。
 831        :param energy: float: X線エネルギー [eV]。
 832
 833    Returns:
 834        :returns: xrayutilities.simpack.DynamicalModel: 構築された動的理論モデル。
 835    """
 836    substrate_layer = xu.simpack.Layer(substrate, float("inf"))
 837    alloy = xu.materials.material.Alloy(substrate, film, x)
 838    film_layer = xu.simpack.Layer(alloy, thick, relaxation=relax)
 839    stack = xu.simpack.PseudomorphicStack001("film/substrate", substrate_layer + film_layer)
 840    dyn_model = xu.simpack.DynamicalModel(
 841        stack,
 842        energy=energy,
 843        resolution_width=RESOLUTION_WIDTH,
 844    )
 845    return dyn_model
 846
 847
 848def simulate(dyn_model, omega):
 849    """
 850    動的理論モデルを使用してXRD強度分布を計算します。
 851
 852    Parameters:
 853        :param dyn_model: xrayutilities.simpack.DynamicalModel: 計算に使用する構築済みモデル。
 854        :param omega: numpy.ndarray: 計算する角度範囲のomega値の配列 [deg]。
 855
 856    Returns:
 857        :returns: numpy.ndarray: 計算されたXRD強度分布の配列。
 858    """
 859    intensity = dyn_model.simulate(omega, hkl=HKL)
 860    return np.asarray(intensity, dtype=float)
 861
 862
 863# ============================================================
 864# データ入出力
 865# ============================================================
 866def read_text_data(infile):
 867    """
 868    テキストファイルから2列データ (2theta, intensity) を読み込みます。
 869
 870    詳細説明:
 871        指定されたテキストファイルから数値データを読み込み、
 872        1列目を2theta角度、2列目を強度として解釈します。
 873        ファイルが存在しない場合や、フォーマットが不正な場合はエラーで終了します。
 874        omega角度は2thetaの半分として計算されます。
 875
 876    Parameters:
 877        :param infile: str: 2theta-intensityデータを含むテキストファイルへのパス。
 878
 879    Returns:
 880        :returns: tuple[numpy.ndarray, numpy.ndarray, numpy.ndarray]:
 881            (2theta角度の配列 [deg], omega角度の配列 [deg], 強度値の配列)。
 882    """
 883    if not os.path.isfile(infile):
 884        print(f'Error: infile "{infile}" not found.')
 885        sys.exit(1)
 886
 887    data = np.loadtxt(infile)
 888
 889    if data.ndim != 2 or data.shape[1] < 2:
 890        print("Error: input file must contain at least two columns: 2theta intensity")
 891        sys.exit(1)
 892
 893    twotheta = data[:, 0]
 894    intensity = data[:, 1]
 895    omega = twotheta / 2.0
 896    return twotheta, omega, np.asarray(intensity, dtype=float)
 897
 898
 899def read_data(infile, substrate, film, energy):
 900    """
 901    XRDデータファイルを読み込むか、ファイルが指定されていない場合は模擬データを生成します。
 902
 903    詳細説明:
 904        `infile` が空文字列の場合、`TARGET_X`, `TARGET_RELAX`, `TARGET_THICK`
 905        で定義されたターゲットパラメータを使用して模擬XRDパターンを生成します。
 906        `infile` が指定されている場合は、`read_text_data` を呼び出してファイルを読み込みます。
 907
 908    Parameters:
 909        :param infile: str: 2theta-intensityデータを含むテキストファイルへのパス。
 910                          空の場合は模擬データを生成します。
 911        :param substrate: xrayutilities.materials.Crystal: 基板材料オブジェクト。
 912        :param film: xrayutilities.materials.Crystal: 膜材料オブジェクト。
 913        :param energy: float: X線エネルギー [eV]。
 914
 915    Returns:
 916        :returns: tuple[numpy.ndarray, numpy.ndarray, numpy.ndarray]:
 917            (2theta角度の配列 [deg], omega角度の配列 [deg], 強度値の配列)。
 918    """
 919    if infile == "":
 920        twotheta, omega = make_scan_axis()
 921        target_model = model(substrate, film, TARGET_X, TARGET_RELAX, TARGET_THICK, energy)
 922        intensity = simulate(target_model, omega)
 923        return twotheta, omega, intensity
 924    return read_text_data(infile)
 925
 926
 927# ============================================================
 928# ユーティリティ
 929# ============================================================
 930def clamp_values(x, relax, thick):
 931    """
 932    組成(x)、緩和度(relax)、膜厚(thick)のパラメータを物理的に妥当な範囲にクランプします。
 933
 934    詳細説明:
 935        - `x` は0.0から1.0の範囲に制限されます。
 936        - `relax` は0.0から1.0の範囲に制限されます。
 937        - `thick` は1.0以上の値に制限されます。
 938
 939    Parameters:
 940        :param x: float: 組成比。
 941        :param relax: float: 緩和度。
 942        :param thick: float: 膜厚 [Å]。
 943
 944    Returns:
 945        :returns: tuple[float, float, float]: クランプされた (x, relax, thick) の値。
 946    """
 947    x = min(max(x, 0.0), 1.0)
 948    relax = min(max(relax, 0.0), 1.0)
 949    thick = max(thick, 1.0)
 950    return x, relax, thick
 951
 952
 953def calc_residual(int_target, int_try, residual_scale="log"):
 954    """
 955    ターゲット強度とシミュレーション強度の残差 (L2ノルム) を計算します。
 956
 957    詳細説明:
 958        `residual_scale` が "log" の場合、強度は対数スケールに変換されてから残差が計算されます。
 959        これにより、強度の低い部分のフィッティング精度を向上させることができます。
 960        "linear" の場合は線形スケールで計算されます。
 961        強度がゼロになることを避けるため、非常に小さいイプシロン値 (`eps`) が使用されます。
 962
 963    Parameters:
 964        :param int_target: numpy.ndarray: 実験データまたはターゲット強度。
 965        :param int_try: numpy.ndarray: 計算データまたは試行強度。
 966        :param residual_scale: str: 残差計算に使用するスケール ("log" または "linear")。
 967
 968    Returns:
 969        :returns: float: 計算された残差の総和 (L2ノルムの和)。
 970    """
 971    eps = 1e-30
 972
 973    if residual_scale == "log":
 974        y_target = np.log(np.maximum(int_target, eps))
 975        y_try = np.log(np.maximum(int_try, eps))
 976    else:
 977        y_target = int_target
 978        y_try = int_try
 979
 980    return np.sum((y_target - y_try) ** 2)
 981
 982
 983def update_rrange(res):
 984    """
 985    残差値に基づいて、ランダム探索の探索範囲 (rrange) を動的に調整します。
 986
 987    詳細説明:
 988        残差が大きい場合は探索範囲を広げ、小さい場合は狭めることで、
 989        最適化の効率を向上させます。残差が5.0より大きい場合は0.5を返し、
 990        それ以外の場合は残差の1/10を返します。
 991
 992    Parameters:
 993        :param res: float: 現在の残差値。
 994
 995    Returns:
 996        :returns: float: 更新された探索範囲の係数。
 997    """
 998    if res > 5.0:
 999        return 0.5
1000    return res / 10.0
1001
1002
1003def calc_pattern_from_param_set(substrate, film, energy, omega, param_set):
1004    """
1005    パラメータセットを直接渡してシミュレーション強度を計算します。
1006
1007    詳細説明:
1008        `param_set` から組成、緩和度、膜厚を抽出し、
1009        `clamp_values` で物理的範囲にクランプした後、モデルを構築し強度をシミュレートします。
1010
1011    Parameters:
1012        :param substrate: xrayutilities.materials.Crystal: 基板材料オブジェクト。
1013        :param film: xrayutilities.materials.Crystal: 膜材料オブジェクト。
1014        :param energy: float: X線エネルギー [eV]。
1015        :param omega: numpy.ndarray: 計算する角度範囲のomega値の配列 [deg]。
1016        :param param_set: dict: 評価するパラメータセット。
1017
1018    Returns:
1019        :returns: numpy.ndarray: 計算されたXRD強度分布の配列。
1020    """
1021    x, relax, thick = parameter_set_to_values(param_set)
1022    x, relax, thick = clamp_values(x, relax, thick)
1023    dyn_model = model(substrate, film, x, relax, thick, energy)
1024    intensity = simulate(dyn_model, omega)
1025    return intensity
1026
1027
1028def objective_from_param_set(substrate, film, energy, omega, int_target, param_set, residual_scale="log"):
1029    """
1030    最適化に使用する目的関数を計算します。残差に加えて物理的境界外へのペナルティを含みます。
1031
1032    詳細説明:
1033        `param_set` からパラメータを抽出し、シミュレーションを実行して強度を計算します。
1034        計算された強度とターゲット強度との残差を `calc_residual` で求めます。
1035        また、組成(x)、緩和度(relax)、膜厚(thick)が物理的に妥当な範囲
1036        (x:[0,1], relax:[0,1], thick>0) を逸脱した場合、
1037        大きなペナルティが追加され、最適化がこれらの制約内で解を見つけるように誘導されます。
1038
1039    Parameters:
1040        :param substrate: xrayutilities.materials.Crystal: 基板材料オブジェクト。
1041        :param film: xrayutilities.materials.Crystal: 膜材料オブジェクト。
1042        :param energy: float: X線エネルギー [eV]。
1043        :param omega: numpy.ndarray: 計算する角度範囲のomega値の配列 [deg]。
1044        :param int_target: numpy.ndarray: 実験データまたはターゲット強度。
1045        :param param_set: dict: 評価するパラメータセット。
1046        :param residual_scale: str: 残差計算に使用するスケール ("log" または "linear")。
1047
1048    Returns:
1049        :returns: float: ペナルティ込みの残差の総和。
1050    """
1051    x, relax, thick = parameter_set_to_values(param_set)
1052
1053    penalty = 0.0
1054    if x < 0.0:
1055        penalty += 1e6 * (0.0 - x) ** 2
1056    if x > 1.0:
1057        penalty += 1e6 * (x - 1.0) ** 2
1058    if relax < 0.0:
1059        penalty += 1e6 * (0.0 - relax) ** 2
1060    if relax > 1.0:
1061        penalty += 1e6 * (relax - 1.0) ** 2
1062    if thick <= 0.0:
1063        penalty += 1e6 * (1.0 - thick) ** 2 # thickは1.0未満にならないようにペナルティ
1064
1065    intensity = calc_pattern_from_param_set(substrate, film, energy, omega, param_set)
1066    return calc_residual(int_target, intensity, residual_scale=residual_scale) + penalty
1067
1068
1069def plot_xrd(omega, intensity_list, labels, title, yscale="log", text_lines=None, wait=False):
1070    """
1071    複数のXRDパターンをプロットし表示します。
1072
1073    詳細説明:
1074        与えられたomega軸と複数の強度配列をプロットします。
1075        各強度配列には対応するラベルが付けられます。
1076        Y軸のスケール("linear"または"log")とプロットのタイトルを設定できます。
1077        追加のテキスト行をプロット領域に表示することも可能です。
1078
1079    Parameters:
1080        :param omega: numpy.ndarray: 角度軸 (omega) の配列 [deg]。
1081        :param intensity_list: list[numpy.ndarray]: プロットする強度配列のリスト。
1082        :param labels: list[str]: 各強度配列に対応する凡例ラベルのリスト。
1083        :param title: str: プロットのタイトル。
1084        :param yscale: str: Y軸のスケール ("linear" または "log")。デフォルトは "log"。
1085        :param text_lines: list[str] or None: プロット内に表示する追加テキスト行のリスト。
1086        :param wait: bool: プロットウィンドウが閉じるまで待機するかどうか。Trueの場合、
1087                           ウィンドウが閉じられるまでプログラムの実行がブロックされます。
1088    """
1089    plt.figure()
1090    for intensity, label in zip(intensity_list, labels):
1091        plt.plot(omega, intensity, label=label)
1092
1093    if text_lines:
1094        y0 = 0.90
1095        dy = 0.05
1096        for i, line in enumerate(text_lines):
1097            plt.text(0.55, y0 - i * dy, line, transform=plt.gca().transAxes)
1098
1099    plt.title(title)
1100    plt.yscale(yscale)
1101    plt.xlabel(r"$\omega\ (deg)$")
1102    plt.ylabel("Intensity (arb. u.)")
1103    plt.legend()
1104
1105    if wait:
1106        plt.show()
1107    else:
1108        plt.show()
1109
1110
1111def make_live_plot(omega, int_target, int_init, title, yscale="log"):
1112    """
1113    フィッティングの進捗を表示するためのインタラクティブなライブプロットを作成します。
1114
1115    詳細説明:
1116        初期の実験データ (`int_target`) と初期シミュレーション結果 (`int_init`) を表示し、
1117        フィッティング中に更新される `CurrentFit` のためのラインオブジェクトを準備します。
1118        また、現在のパラメータや残差を表示するためのテキストボックスも作成します。
1119
1120    Parameters:
1121        :param omega: numpy.ndarray: 角度軸 (omega) の配列 [deg]。
1122        :param int_target: numpy.ndarray: 実験データ強度。
1123        :param int_init: numpy.ndarray: 初期シミュレーション強度。
1124        :param title: str: プロットのタイトル。
1125        :param yscale: str: Y軸のスケール ("linear" または "log")。デフォルトは "log"。
1126
1127    Returns:
1128        :returns: tuple[matplotlib.figure.Figure, matplotlib.axes.Axes, matplotlib.lines.Line2D, matplotlib.text.Text]:
1129            (Figureオブジェクト, Axesオブジェクト, CurrentFitのLine2Dオブジェクト, テキストボックスのTextオブジェクト)。
1130    """
1131    plt.ion()
1132    fig, ax = plt.subplots(figsize=(8, 5))
1133    ax.plot(omega, int_target, label="InputData")
1134    ax.plot(omega, int_init, label="InitialSimulation")
1135    line_fit, = ax.plot(omega, int_init, label="CurrentFit")
1136    text_box = ax.text(0.55, 0.92, "", transform=ax.transAxes, va="top", ha="left")
1137
1138    ax.set_title(title)
1139    ax.set_yscale(yscale)
1140    ax.set_xlabel(r"$\omega\ (deg)$")
1141    ax.set_ylabel("Intensity (arb. u.)")
1142    ax.legend()
1143
1144    plt.show(block=False)
1145    fig.canvas.draw()
1146    fig.canvas.flush_events()
1147    plt.pause(0.1)
1148
1149    return fig, ax, line_fit, text_box
1150
1151
1152def update_live_plot(fig, line_fit, text_box, omega, int_fit, text_lines):
1153    """
1154    作成済みのライブプロットの内容を更新します。
1155
1156    詳細説明:
1157        `CurrentFit` のラインデータを新しいフィッティング強度 (`int_fit`) で更新し、
1158        テキストボックスの内容を `text_lines` で更新してプロットを再描画します。
1159        これにより、最適化の進捗がリアルタイムで視覚的に確認できます。
1160
1161    Parameters:
1162        :param fig: matplotlib.figure.Figure: 更新するFigureオブジェクト。
1163        :param line_fit: matplotlib.lines.Line2D: `CurrentFit` のLine2Dオブジェクト。
1164        :param text_box: matplotlib.text.Text: 更新するテキストボックスのTextオブジェクト。
1165        :param omega: numpy.ndarray: 角度軸 (omega) の配列 [deg]。
1166        :param int_fit: numpy.ndarray: 現在のフィッティング強度。
1167        :param text_lines: list[str]: テキストボックスに表示するテキスト行のリスト。
1168    """
1169    line_fit.set_data(omega, int_fit)
1170    text_box.set_text("\n".join(text_lines))
1171    fig.canvas.draw()
1172    fig.canvas.flush_events()
1173    plt.pause(0.001)
1174
1175
1176def show_final_live_plot_wait(fig):
1177    """
1178    ライブプロットを非インタラクティブモードに切り替え、ウィンドウが閉じるまで待機します。
1179
1180    詳細説明:
1181        最適化が完了した後、最終結果を表示したプロットウィンドウを
1182        ユーザーが手動で閉じるまで保持します。
1183
1184    Parameters:
1185        :param fig: matplotlib.figure.Figure: 待機するFigureオブジェクト。
1186    """
1187    plt.ioff()
1188    fig.canvas.draw()
1189    fig.canvas.flush_events()
1190    plt.show()
1191
1192
1193def make_initial_simplex(param_set, optimize_names):
1194    """
1195    Nelder-Mead最適化法のための初期シンプレックスを生成します。
1196
1197    詳細説明:
1198        初期シンプレックスは、`optimize_names` で指定された最適化対象パラメータの
1199        現在の値 (`param_set`) を中心に、N+1個の頂点を持つ形状として生成されます。
1200        各パラメータに対して、現在の値から大きく離れた値を設定した頂点を生成することで、
1201        初期探索空間をカバーします。
1202
1203    Parameters:
1204        :param param_set: dict: 基準となる初期パラメータセット。
1205        :param optimize_names: list[str]: 最適化対象のパラメータ名のリスト。
1206
1207    Returns:
1208        :returns: numpy.ndarray: Nelder-Mead法のための初期シンプレックス。各行が1つの頂点を表します。
1209    """
1210    x, relax, thick = parameter_set_to_values(param_set)
1211    x0 = pack_optimize_values(param_set, optimize_names)
1212
1213    simplex = [x0.copy()]
1214
1215    for idx, name in enumerate(optimize_names):
1216        xnew = x0.copy()
1217
1218        if name == "x":
1219            xnew[idx] = 1.0 if x <= 0.5 else 0.0
1220        elif name == "relax":
1221            xnew[idx] = 1.0 if relax <= 0.5 else 0.0
1222        elif name == "thick":
1223            xnew[idx] = max(thick * 1.25, 1.0) # Thick must be > 0.
1224
1225        simplex.append(xnew)
1226
1227    # thickのみを最適化する場合のシンプレックス調整
1228    # thick * 0.75 と thick * 1.25 で構成
1229    if optimize_names == ["thick"]:
1230        simplex = [
1231            np.array([max(thick * 0.75, 1.0)], dtype=float),
1232            np.array([max(thick * 1.25, 1.0)], dtype=float),
1233        ]
1234
1235    return np.array(simplex, dtype=float)
1236
1237
1238def get_param_bounds(name, current_value):
1239    """
1240    特定のパラメータの探索境界(下限と上限)を取得します。
1241
1242    詳細説明:
1243        組成(`x`)と緩和度(`relax`)はそれぞれ[0.0, 1.0]に制限されます。
1244        膜厚(`thick`)は現在の値を基準に相対的な範囲([0.5*current_value, 1.5*current_value])で、
1245        かつ最低1.0Åという制約が適用されます。
1246        これらはPSOアルゴリズムなどでの粒子の位置更新や初期化に利用されます。
1247
1248    Parameters:
1249        :param name: str: パラメータ名 ("x", "relax", "thick")。
1250        :param current_value: float: そのパラメータの現在の値。
1251
1252    Returns:
1253        :returns: tuple[float, float]: (下限値, 上限値) のタプル。
1254    """
1255    if name == "x":
1256        return 0.0, 1.0
1257    if name == "relax":
1258        return 0.0, 1.0
1259    if name == "thick":
1260        low = max(current_value * 0.5, 1.0)
1261        high = max(current_value * 1.5, low + 1.0) # Ensure high is strictly greater than low if low is 1.0
1262        return low, high
1263    return -np.inf, np.inf # デフォルトでは無限の範囲
1264
1265
1266def randomize_param_set_for_pso(base_param_set, optimize_names):
1267    """
1268    PSOの初期化のために、最適化対象のパラメータをランダムな値に設定した新しいパラメータセットを生成します。
1269
1270    詳細説明:
1271        `base_param_set` をクローンし、`optimize_names` に含まれる各パラメータについて、
1272        `get_param_bounds` で定義された範囲内で一様乱数を用いて値を設定します。
1273
1274    Parameters:
1275        :param base_param_set: dict: 基準となるパラメータセット(最適化対象外の値を保持するために使用)。
1276        :param optimize_names: list[str]: ランダム化する最適化対象のパラメータ名のリスト。
1277
1278    Returns:
1279        :returns: dict: ランダム化された最適化対象パラメータを持つ新しいパラメータセット。
1280    """
1281    ps = clone_parameter_set(base_param_set)
1282    for name in optimize_names:
1283        value = ps[name]["value"]
1284        low, high = get_param_bounds(name, value)
1285        ps[name]["value"] = random.uniform(low, high)
1286    return ps
1287
1288
1289# ============================================================
1290# fit: ランダム探索
1291# ============================================================
1292def fit_random(
1293    substrate,
1294    film,
1295    energy,
1296    omega,
1297    int_target,
1298    param_sets,
1299    parameter_file,
1300    yscale="log",
1301    residual_scale="log",
1302):
1303    """
1304    ランダムウォーク的な手法でXRDフィッティングを行います。
1305
1306    詳細説明:
1307        現在の最適パラメータセットからランダムに少しずつパラメータを変化させ、
1308        残差が改善されればその新しいパラメータセットを採用するという手順を繰り返します。
1309        一定回数改善が見られない場合や、残差が `LIMIT` 以下になれば停止します。
1310        最適化の進捗はライブプロットで視覚的に表示されます。
1311
1312    Parameters:
1313        :param substrate: xrayutilities.materials.Crystal: 基板材料オブジェクト。
1314        :param film: xrayutilities.materials.Crystal: 膜材料オブジェクト。
1315        :param energy: float: X線エネルギー [eV]。
1316        :param omega: numpy.ndarray: 計算する角度範囲のomega値の配列 [deg]。
1317        :param int_target: numpy.ndarray: 実験データ強度。
1318        :param param_sets: dict: 初期パラメータ、最適化の進捗がここに保存されます。
1319        :param parameter_file: str: パラメータの保存先ファイルパス。
1320        :param yscale: str: プロットのY軸スケール ("linear" または "log")。
1321        :param residual_scale: str: 残差計算に使用するスケール ("log" または "linear")。
1322
1323    Returns:
1324        :returns: dict: 最適化されたパラメータセットを含む `param_sets` 辞書。
1325    """
1326    param_set = clone_parameter_set(param_sets[DEFAULT_PRIMARY_SET])
1327    optimize_names = get_optimize_names(param_set)
1328
1329    if len(optimize_names) == 0:
1330        print("All parameters are fixed. No optimization is performed.")
1331        return param_sets
1332
1333    int_init = calc_pattern_from_param_set(substrate, film, energy, omega, param_set)
1334    int_try_now = int_init.copy()
1335    res = calc_residual(int_target, int_try_now, residual_scale=residual_scale)
1336    rrange = update_rrange(res)
1337
1338    fig, ax, line_fit, text_box = make_live_plot(
1339        omega,
1340        int_target,
1341        int_init,
1342        "膜のXRDフィッティング (Random)",
1343        yscale=yscale,
1344    )
1345
1346    total_count = 0
1347    count = 0
1348
1349    while (res > LIMIT) and (count < MAX_STALL):
1350        new_param_set = clone_parameter_set(param_set)
1351
1352        # 各最適化対象パラメータにランダムな変動を加える
1353        if "x" in optimize_names:
1354            new_param_set["x"]["value"] += random.uniform(-rrange * XRANGE, rrange * XRANGE)
1355        if "relax" in optimize_names:
1356            new_param_set["relax"]["value"] += random.uniform(-rrange * RRANGE, rrange * RRANGE)
1357        if "thick" in optimize_names:
1358            new_param_set["thick"]["value"] += random.uniform(-rrange * TRANGE, rrange * TRANGE)
1359
1360        res_new = objective_from_param_set(
1361            substrate, film, energy, omega, int_target, new_param_set, residual_scale=residual_scale
1362        )
1363
1364        total_count += 1
1365        count += 1
1366
1367        if total_count % NINTERVAL_PRINT == 0:
1368            x, relax, thick = parameter_set_to_values(param_set)
1369            print(
1370                f"{total_count} {count} : "
1371                f"residual= {res:.6f} x= {x:.4f} relax= {relax:.4f} T= {thick:.2f}"
1372            )
1373
1374        if res_new < res:
1375            param_set = new_param_set
1376            int_try_now = calc_pattern_from_param_set(substrate, film, energy, omega, param_set)
1377            res = res_new
1378            rrange = update_rrange(res)
1379            count = 0
1380
1381        if total_count % NINTERVAL_SAVE == 0:
1382            set_best_parameter_set(param_sets, param_set)
1383            save_parameters(parameter_file, param_sets)
1384
1385        if total_count % NINTERVAL_PLOT == 0:
1386            x, relax, thick = parameter_set_to_values(param_set)
1387            update_live_plot(
1388                fig,
1389                line_fit,
1390                text_box,
1391                omega,
1392                int_try_now,
1393                [
1394                    f"iter= {total_count}",
1395                    f"residual= {res:.6f}",
1396                    f"x= {x:.4f}",
1397                    f"r= {relax:.4f}",
1398                    f"t= {thick:.2f}",
1399                ],
1400            )
1401
1402    set_best_parameter_set(param_sets, param_set)
1403    save_parameters(parameter_file, param_sets)
1404    show_final_live_plot_wait(fig)
1405    return param_sets
1406
1407
1408# ============================================================
1409# fit: scipy minimize
1410# ============================================================
1411def fit_scipy(
1412    substrate,
1413    film,
1414    energy,
1415    omega,
1416    int_target,
1417    param_sets,
1418    parameter_file,
1419    method_name,
1420    nmaxiter,
1421    tol,
1422    yscale="log",
1423    residual_scale="log",
1424):
1425    """
1426    SciPyの最適化アルゴリズム(Nelder-Mead, BFGS, CG)を使用してXRDフィッティングを行います。
1427
1428    詳細説明:
1429        `scipy.optimize.minimize` 関数を利用し、指定された最適化手法でパラメータを調整します。
1430        目的関数は `objective_from_param_set` で定義されており、物理的な制約へのペナルティを含みます。
1431        最適化の進捗はコールバック関数を通じてライブプロットにリアルタイムで表示され、
1432        一定間隔でパラメータがファイルに保存されます。
1433
1434    Parameters:
1435        :param substrate: xrayutilities.materials.Crystal: 基板材料オブジェクト。
1436        :param film: xrayutilities.materials.Crystal: 膜材料オブジェクト。
1437        :param energy: float: X線エネルギー [eV]。
1438        :param omega: numpy.ndarray: 計算する角度範囲のomega値の配列 [deg]。
1439        :param int_target: numpy.ndarray: 実験データ強度。
1440        :param param_sets: dict: 初期パラメータ、最適化の進捗がここに保存されます。
1441        :param parameter_file: str: パラメータの保存先ファイルパス。
1442        :param method_name: str: 使用するSciPy最適化手法の名前 ("nelder-mead", "bfgs", "cg")。
1443        :param nmaxiter: int: 最大反復回数。
1444        :param tol: float: 収束判定の許容誤差。
1445        :param yscale: str: プロットのY軸スケール ("linear" または "log")。
1446        :param residual_scale: str: 残差計算に使用するスケール ("log" または "linear")。
1447
1448    Returns:
1449        :returns: dict: 最適化されたパラメータセットを含む `param_sets` 辞書。
1450    """
1451    param_set = clone_parameter_set(param_sets[DEFAULT_PRIMARY_SET])
1452    optimize_names = get_optimize_names(param_set)
1453
1454    if len(optimize_names) == 0:
1455        print("All parameters are fixed. No optimization is performed.")
1456        return param_sets
1457
1458    x0 = pack_optimize_values(param_set, optimize_names)
1459    int_init = calc_pattern_from_param_set(substrate, film, energy, omega, param_set)
1460
1461    fig, ax, line_fit, text_box = make_live_plot(
1462        omega,
1463        int_target,
1464        int_init,
1465        f"膜のXRDフィッティング ({method_name})",
1466        yscale=yscale,
1467    )
1468
1469    iter_state = {"count": 0, "best_res": None, "best_param_set": clone_parameter_set(param_set)}
1470
1471    def objective_vec(xvec):
1472        """
1473        最適化アルゴリズムに渡す目的関数 (ベクトル入力用)。
1474
1475        Parameters:
1476            :param xvec: numpy.ndarray: 最適化対象のパラメータ値の配列。
1477        Returns:
1478            :returns: float: 目的関数の評価値。
1479        """
1480        work = unpack_optimize_values(param_set, optimize_names, xvec)
1481        return objective_from_param_set(
1482            substrate, film, energy, omega, int_target, work, residual_scale=residual_scale
1483        )
1484
1485    def callback(xk):
1486        """
1487        最適化の各ステップで呼び出されるコールバック関数。
1488
1489        Parameters:
1490            :param xk: numpy.ndarray: 現在の最適化ステップでのパラメータ値の配列。
1491        """
1492        iter_state["count"] += 1
1493        work = unpack_optimize_values(param_set, optimize_names, xk)
1494        res = objective_from_param_set(
1495            substrate, film, energy, omega, int_target, work, residual_scale=residual_scale
1496        )
1497
1498        if iter_state["best_res"] is None or res < iter_state["best_res"]:
1499            iter_state["best_res"] = res
1500            iter_state["best_param_set"] = clone_parameter_set(work)
1501
1502        x, relax, thick = parameter_set_to_values(work)
1503
1504        if iter_state["count"] % NINTERVAL_PRINT == 0:
1505            print(
1506                f"{iter_state['count']}/{nmaxiter} : "
1507                f"residual= {res:.6f} x= {x:.4f} relax= {relax:.4f} T= {thick:.2f}"
1508            )
1509
1510        if iter_state["count"] % NINTERVAL_SAVE == 0:
1511            set_best_parameter_set(param_sets, iter_state["best_param_set"])
1512            save_parameters(parameter_file, param_sets)
1513
1514        if iter_state["count"] % NINTERVAL_PLOT == 0:
1515            int_fit = calc_pattern_from_param_set(substrate, film, energy, omega, work)
1516            update_live_plot(
1517                fig,
1518                line_fit,
1519                text_box,
1520                omega,
1521                int_fit,
1522                [
1523                    f"iter= {iter_state['count']}",
1524                    f"residual= {res:.6f}",
1525                    f"x= {x:.4f}",
1526                    f"r= {relax:.4f}",
1527                    f"t= {thick:.2f}",
1528                ],
1529            )
1530
1531    scipy_method = {
1532        "nelder-mead": "Nelder-Mead",
1533        "bfgs": "BFGS",
1534        "cg": "CG",
1535    }[method_name]
1536
1537    options = {"maxiter": nmaxiter, "disp": True}
1538
1539    if method_name == "nelder-mead":
1540        options["initial_simplex"] = make_initial_simplex(param_set, optimize_names)
1541        options["xatol"] = tol # absolute tolerance for changes in x
1542        options["fatol"] = tol # absolute tolerance for changes in fn
1543
1544    result = minimize(
1545        objective_vec,
1546        x0,
1547        method=scipy_method,
1548        callback=callback,
1549        options=options,
1550        tol=tol,
1551    )
1552
1553    final_param_set = unpack_optimize_values(param_set, optimize_names, result.x)
1554    final_res = objective_from_param_set(
1555        substrate, film, energy, omega, int_target, final_param_set, residual_scale=residual_scale
1556    )
1557
1558    # コールバック関数で記録された最良の結果と最終結果を比較
1559    if iter_state["best_res"] is not None and iter_state["best_res"] < final_res:
1560        final_param_set = clone_parameter_set(iter_state["best_param_set"])
1561
1562    set_best_parameter_set(param_sets, final_param_set)
1563    save_parameters(parameter_file, param_sets)
1564    show_final_live_plot_wait(fig)
1565    return param_sets
1566
1567
1568# ============================================================
1569# fit: PSO (粒子群最適化)
1570# ============================================================
1571def fit_pso(
1572    substrate,
1573    film,
1574    energy,
1575    omega,
1576    int_target,
1577    param_sets,
1578    parameter_file,
1579    nmaxiter,
1580    tol,
1581    nparticles,
1582    w,
1583    c1,
1584    c2,
1585    yscale="log",
1586    residual_scale="log",
1587    pso_stall_max=20,
1588    pso_spread_rtol=0.01,
1589):
1590    """
1591    粒子群最適化 (PSO) を使用してXRDフィッティングを行います。
1592
1593    詳細説明:
1594        この関数は、粒子群最適化アルゴリズムを実装し、複数の「粒子」が解空間を探索します。
1595        各粒子は、自身の最良の位置 (`pbest`) と群全体の最良の位置 (`gbest`) に基づいて
1596        速度と位置を更新します。一定期間の改善なし (`pso_stall_max`) または
1597        粒子の位置の分散が閾値以下になった場合 (`pso_spread_rtol`) に早期停止します。
1598        最適化の進捗はライブプロットで視覚的に表示され、一定間隔でパラメータがファイルに保存されます。
1599
1600    Parameters:
1601        :param substrate: xrayutilities.materials.Crystal: 基板材料オブジェクト。
1602        :param film: xrayutilities.materials.Crystal: 膜材料オブジェクト。
1603        :param energy: float: X線エネルギー [eV]。
1604        :param omega: numpy.ndarray: 計算する角度範囲のomega値の配列 [deg]。
1605        :param int_target: numpy.ndarray: 実験データ強度。
1606        :param param_sets: dict: 初期パラメータ、最適化の進捗がここに保存されます。
1607        :param parameter_file: str: パラメータの保存先ファイルパス。
1608        :param nmaxiter: int: 最大反復回数。
1609        :param tol: float: 収束判定の許容誤差(残差の改善量に対する閾値)。
1610        :param nparticles: int: 粒子の総数。
1611        :param w: float: PSOの慣性重み。
1612        :param c1: float: PSOの自己学習係数。
1613        :param c2: float: PSOの社会学習係数。
1614        :param yscale: str: プロットのY軸スケール ("linear" または "log")。
1615        :param residual_scale: str: 残差計算に使用するスケール ("log" または "linear")。
1616        :param pso_stall_max: int: 改善が見られない場合にPSOを停止する最大反復数。
1617        :param pso_spread_rtol: float: 粒子の分散に基づく停止条件の相対許容誤差。
1618
1619    Returns:
1620        :returns: dict: 最適化されたパラメータセットを含む `param_sets` 辞書。
1621    """
1622    base_param_set = clone_parameter_set(param_sets[DEFAULT_PRIMARY_SET])
1623    optimize_names = get_optimize_names(base_param_set)
1624
1625    if len(optimize_names) == 0:
1626        print("All parameters are fixed. No optimization is performed.")
1627        return param_sets
1628
1629    int_init = calc_pattern_from_param_set(substrate, film, energy, omega, base_param_set)
1630
1631    fig, ax, line_fit, text_box = make_live_plot(
1632        omega,
1633        int_target,
1634        int_init,
1635        "膜のXRDフィッティング (PSO)",
1636        yscale=yscale,
1637    )
1638
1639    seed_particles = get_pso_parameter_sets(param_sets)
1640    particles = [clone_parameter_set(ps) for ps in seed_particles]
1641
1642    # 必要に応じて粒子数を増やす
1643    while len(particles) < nparticles:
1644        particles.append(randomize_param_set_for_pso(base_param_set, optimize_names))
1645
1646    # 必要に応じて粒子数を減らす
1647    if len(particles) > nparticles:
1648        particles = particles[:nparticles]
1649
1650    positions = []
1651    velocities = []
1652    pbest_positions = []
1653    pbest_scores = []
1654
1655    for ps in particles:
1656        pos = pack_optimize_values(ps, optimize_names)
1657        vel = np.zeros_like(pos)
1658
1659        # 速度の初期化
1660        for i, name in enumerate(optimize_names):
1661            val = ps[name]["value"]
1662            low, high = get_param_bounds(name, val)
1663            span = high - low
1664            # 初期速度を探索範囲の10%程度でランダムに設定
1665            vel[i] = random.uniform(-0.1 * span, 0.1 * span)
1666
1667        score = objective_from_param_set(
1668            substrate, film, energy, omega, int_target, ps, residual_scale=residual_scale
1669        )
1670
1671        positions.append(pos.copy())
1672        velocities.append(vel.copy())
1673        pbest_positions.append(pos.copy())
1674        pbest_scores.append(score)
1675
1676    positions = np.array(positions, dtype=float)
1677    velocities = np.array(velocities, dtype=float)
1678    pbest_positions = np.array(pbest_positions, dtype=float)
1679    pbest_scores = np.array(pbest_scores, dtype=float)
1680
1681    gbest_idx = int(np.argmin(pbest_scores))
1682    gbest_position = pbest_positions[gbest_idx].copy()
1683    gbest_score = float(pbest_scores[gbest_idx])
1684
1685    iter_count = 0
1686    prev_best = gbest_score
1687    no_improve_count = 0
1688
1689    base_x, base_relax, base_thick = parameter_set_to_values(base_param_set)
1690    # 粒子の分散許容誤差をパラメータごとに調整
1691    spread_tol_map = {
1692        "x": pso_spread_rtol * 1.0, # xは[0,1]なので相対値そのまま
1693        "relax": pso_spread_rtol * 1.0, # relaxも[0,1]なので相対値そのまま
1694        "thick": pso_spread_rtol * base_thick, # thickは絶対値なので、基準値に対する相対値
1695    }
1696
1697    while iter_count < nmaxiter:
1698        iter_count += 1
1699
1700        for p in range(nparticles):
1701            for i, name in enumerate(optimize_names):
1702                current_val = positions[p, i]
1703                low, high = get_param_bounds(name, current_val)
1704
1705                r1 = random.random()
1706                r2 = random.random()
1707
1708                # 速度の更新
1709                velocities[p, i] = (
1710                    w * velocities[p, i]
1711                    + c1 * r1 * (pbest_positions[p, i] - positions[p, i])
1712                    + c2 * r2 * (gbest_position[i] - positions[p, i])
1713                )
1714
1715                # 位置の更新
1716                positions[p, i] += velocities[p, i]
1717
1718                # 境界条件処理
1719                if positions[p, i] < low:
1720                    positions[p, i] = low
1721                    velocities[p, i] *= -0.5 # 境界で跳ね返す
1722                elif positions[p, i] > high:
1723                    positions[p, i] = high
1724                    velocities[p, i] *= -0.5 # 境界で跳ね返す
1725
1726            work = unpack_optimize_values(base_param_set, optimize_names, positions[p])
1727            score = objective_from_param_set(
1728                substrate, film, energy, omega, int_target, work, residual_scale=residual_scale
1729            )
1730
1731            # pbestの更新
1732            if score < pbest_scores[p]:
1733                pbest_scores[p] = score
1734                pbest_positions[p] = positions[p].copy()
1735
1736                # gbestの更新
1737                if score < gbest_score:
1738                    gbest_score = float(score)
1739                    gbest_position = positions[p].copy()
1740
1741        best_param_set = unpack_optimize_values(base_param_set, optimize_names, gbest_position)
1742        best_int = calc_pattern_from_param_set(substrate, film, energy, omega, best_param_set)
1743        best_x, best_relax, best_thick = parameter_set_to_values(best_param_set)
1744
1745        if iter_count % NINTERVAL_PRINT == 0:
1746            print(
1747                f"{iter_count}/{nmaxiter} : residual= {gbest_score:.6f} "
1748                f"x= {best_x:.4f} relax= {best_relax:.4f} T= {best_thick:.2f}"
1749            )
1750
1751        if iter_count % NINTERVAL_SAVE == 0:
1752            set_best_parameter_set(param_sets, best_param_set)
1753            # 現在の粒子群の最良なものを保存
1754            particle_sets = []
1755            order = np.argsort(pbest_scores)
1756            for idx in order[:nparticles]: # 上位nparticles個の粒子を保存
1757                ps = unpack_optimize_values(base_param_set, optimize_names, pbest_positions[idx])
1758                particle_sets.append(ps)
1759            param_sets = replace_pso_parameter_sets(param_sets, particle_sets)
1760            save_parameters(parameter_file, param_sets)
1761
1762        if iter_count % NINTERVAL_PLOT == 0:
1763            update_live_plot(
1764                fig,
1765                line_fit,
1766                text_box,
1767                omega,
1768                best_int,
1769                [
1770                    f"iter= {iter_count}",
1771                    f"residual= {gbest_score:.6f}",
1772                    f"x= {best_x:.4f}",
1773                    f"r= {best_relax:.4f}",
1774                    f"t= {best_thick:.2f}",
1775                ],
1776            )
1777
1778        # 停止条件のチェック
1779        improve = prev_best - gbest_score
1780        if improve < tol: # 残差の改善が許容誤差未満
1781            no_improve_count += 1
1782        else:
1783            no_improve_count = 0
1784
1785        # 粒子の分散が十分に小さいかチェック
1786        spread_ok = True
1787        spread_info = {} # デバッグ用
1788        for i, name in enumerate(optimize_names):
1789            spread = float(np.max(positions[:, i]) - np.min(positions[:, i]))
1790            spread_info[name] = spread
1791            if spread > spread_tol_map[name]:
1792                spread_ok = False
1793
1794        if no_improve_count >= pso_stall_max:
1795            print("PSO stop: stall criterion satisfied.")
1796            break
1797        if spread_ok:
1798            print("PSO stop: spread criterion satisfied.")
1799            break
1800        prev_best = gbest_score
1801
1802    set_best_parameter_set(param_sets, best_param_set)
1803    save_parameters(parameter_file, param_sets)
1804    show_final_live_plot_wait(fig)
1805    return param_sets
1806
1807
1808# ============================================================
1809# guessモード ヘルパー
1810# ============================================================
1811def ensure_valid_savgol(points, order, ndata):
1812    """
1813    Savitzky-Golayフィルタのパラメータがデータ長に対して妥当であることを保証します。
1814
1815    詳細説明:
1816        ウィンドウサイズ `points` は奇数であり、データ長 `ndata` を超えないように調整されます。
1817        多項式次数 `order` はウィンドウサイズ `points` より小さく調整されます。
1818
1819    Parameters:
1820        :param points: int: Savitzky-Golayフィルタのウィンドウ点数。
1821        :param order: int: Savitzky-Golayフィルタの多項式次数。
1822        :param ndata: int: 入力データの総点数。
1823
1824    Returns:
1825        :returns: tuple[int, int]: 調整された (ウィンドウ点数, 多項式次数)。
1826    """
1827    points = int(points)
1828    order = int(order)
1829    if points < 3: points = 3
1830    if points % 2 == 0: points += 1 # Ensure odd window length
1831    if points > ndata: points = ndata if ndata % 2 == 1 else ndata - 1
1832    if order >= points: order = points - 1
1833    return points, order
1834
1835
1836def estimate_thickness_from_fft_signal(omega_deg, signal, wavelength, keep_n=5, nq_uniform=DEFAULT_NQ_UNIFORM):
1837    """
1838    信号に対してFFTを行い、主要な周波数成分から膜厚の候補を算出します。
1839
1840    詳細説明:
1841        入力の角度データと信号をq軸に変換し、一様間隔で補間します。
1842        ハニング窓を適用した後、FFTを実行して周波数スペクトルを取得します。
1843        スペクトル中の顕著なピークを検出し、その周波数から膜厚候補を計算します。
1844        複数の候補をスコアの高い順にソートして返します。
1845
1846    Parameters:
1847        :param omega_deg: numpy.ndarray: 角度データ [deg]。
1848        :param signal: numpy.ndarray: 解析対象の信号 (通常はエンベロープ除去後の残差)。
1849        :param wavelength: float: X線波長 [Å]。
1850        :param keep_n: int: 保持する膜厚候補の最大数。
1851        :param nq_uniform: int: FFTのために一様補間するq軸の点数。
1852
1853    Returns:
1854        :returns: tuple[list[dict], numpy.ndarray, numpy.ndarray, numpy.ndarray, numpy.ndarray]:
1855            - `candidates_list`: 膜厚候補のリスト。各候補は辞書 (`method`, `thick`, `score`, `dq`, `freq`)。
1856            - `qz_axis`: 一様化されたqz軸。
1857            - `signal_uniform`: 一様化された信号。
1858            - `freq_axis`: FFTの周波数軸。
1859            - `fft_amplitude`: FFTの振幅。
1860    """
1861    theta_rad = np.deg2rad(omega_deg)
1862    qz = 4.0 * np.pi / wavelength * np.sin(theta_rad)
1863
1864    idx = np.argsort(qz)
1865    qz = qz[idx]
1866    signal = signal[idx]
1867
1868    # q軸を一様間隔に補間
1869    qz_uniform = np.linspace(qz.min(), qz.max(), nq_uniform)
1870    y_uniform = np.interp(qz_uniform, qz, signal)
1871    y_uniform = y_uniform - np.mean(y_uniform) # DC成分除去
1872
1873    dq = qz_uniform[1] - qz_uniform[0]
1874    fft_vals = np.fft.rfft(y_uniform * np.hanning(len(y_uniform))) # ハニング窓適用
1875    fft_amp = np.abs(fft_vals)
1876    freq = np.fft.rfftfreq(len(y_uniform), d=dq)
1877
1878    if len(fft_amp) > 0: fft_amp[0] = 0.0 # DC成分をゼロにする
1879
1880    # FFTスペクトルからピークを検出
1881    peak_idx, props = find_peaks(fft_amp, prominence=np.max(fft_amp) * 0.05 if np.max(fft_amp)>0 else 0)
1882
1883    candidates = []
1884    if len(peak_idx) > 0:
1885        order = np.argsort(props.get("prominences", np.ones(len(peak_idx))))[::-1] # Prominenceでソート
1886        for idxp in peak_idx[order][:keep_n]:
1887            f = freq[idxp]
1888            if f <= 0: continue # 周波数が0以下のものは無視
1889            dq_val = 1.0 / f
1890            thick = 2.0 * np.pi / dq_val # 膜厚 = 2*pi / dq
1891            candidates.append({"method": "fft", "thick": float(thick), "score": float(fft_amp[idxp]), "dq": float(dq_val), "freq": float(f)})
1892
1893    return sorted(candidates, key=lambda d: d["score"], reverse=True), qz_uniform, y_uniform, freq, fft_amp
1894
1895
1896def assign_fringe_index_auto(q_vals, dq_est):
1897    """
1898    FFTによる推定周期 `dq_est` に基づき、各ピークにフリンジ次数 (n) を自動的に割り当てます。
1899
1900    詳細説明:
1901        フリンジのQ値 (`q_vals`) の差分と推定された周期 (`dq_est`) を比較し、
1902        最も近い整数倍をフリンジ次数のステップとして採用します。
1903
1904    Parameters:
1905        :param q_vals: numpy.ndarray: 検出されたフリンジピークのQ値の配列。
1906        :param dq_est: float: FFTによって推定されたフリンジ周期 (Δq)。
1907
1908    Returns:
1909        :returns: numpy.ndarray: 各ピークに割り当てられたフリンジ次数 (n) の配列。
1910    """
1911    if len(q_vals) == 0: return np.array([], dtype=int)
1912    n_vals = [0] # 最初のピークを0次としてスタート
1913    for i in range(1, len(q_vals)):
1914        step = int(round((q_vals[i] - q_vals[i - 1]) / dq_est))
1915        n_vals.append(n_vals[-1] + max(step, 1)) # 少なくとも1次ずつ増えることを保証
1916    return np.array(n_vals, dtype=int)
1917
1918
1919def linear_fit_two_stage(n_vals, q_vals):
1920    """
1921    フリンジ次数 (n) と q 値の関係を2段階の線形回帰で解析し、傾きから膜厚を求めます。
1922
1923    詳細説明:
1924        1. 全てのピークデータを用いて線形回帰を行い、初期の残差と標準偏差を計算します。
1925        2. 初期残差が標準偏差の2倍を超える外れ値を除外し、残りのデータで再度線形回帰を行います。
1926        3. 最終的な回帰の傾き (dq/dn) から、膜厚 (Thickness = 2π / (dq/dn)) を計算します。
1927
1928    Parameters:
1929        :param n_vals: numpy.ndarray: フリンジ次数の配列。
1930        :param q_vals: numpy.ndarray: 対応するQ値の配列。
1931
1932    Returns:
1933        :returns: dict or None: 解析結果を含む辞書。ピークが2つ未満の場合はNone。
1934            - `coef1`: 1段階目の回帰係数。
1935            - `coef2`: 2段階目の回帰係数。
1936            - `used_stage1`: 1段階目で使用されたピークのブール配列。
1937            - `used_stage2`: 2段階目で使用されたピークのブール配列。
1938            - `resid_stage1`: 1段階目の残差。
1939            - `resid_stage2`: 2段階目の残差。
1940            - `thickness`: 計算された膜厚 [Å]。
1941    """
1942    if len(n_vals) < 2: return None # 2点以上必要
1943
1944    # Stage 1: 全てのデータで線形回帰
1945    coef1 = np.polyfit(n_vals, q_vals, 1)
1946    resid1 = q_vals - np.polyval(coef1, n_vals)
1947    sigma1 = np.std(resid1, ddof=1) if len(resid1) >= 2 else 0 # 標本標準偏差 (ddof=1)
1948
1949    # Stage 2: 外れ値除去後に再回帰
1950    if sigma1 > 0:
1951        used2 = np.abs(resid1) < 2.0 * sigma1 # 2σ以内に絞る
1952    else: # sigma1が0の場合、全て使用(データ点が少ないなど)
1953        used2 = np.ones(len(n_vals), dtype=bool)
1954
1955    if np.sum(used2) < 2: # 除去後も2点以上必要
1956        used2 = np.ones(len(n_vals), dtype=bool) # 2点未満なら全て使用に戻す
1957
1958    coef2 = np.polyfit(n_vals[used2], q_vals[used2], 1)
1959    
1960    # 傾きから膜厚を計算 (dq/dn) -> thickness = 2*pi / |slope|
1961    thickness = 2.0 * np.pi / abs(coef2[0]) if coef2[0] != 0 else np.nan
1962
1963    return {
1964        "coef1": coef1,
1965        "coef2": coef2,
1966        "used_stage1": np.ones(len(n_vals), dtype=bool), # 常に全てTrue
1967        "used_stage2": used2,
1968        "resid_stage1": resid1,
1969        "resid_stage2": q_vals - np.polyval(coef2, n_vals),
1970        "thickness": thickness
1971    }
1972
1973
1974def detect_fringe_clusters(q_vals, dq_ref, gap_factor=DEFAULT_CLUSTER_GAP_FACTOR):
1975    """
1976    フリンジの欠損が大きい箇所でデータを分割し、クラスター化します。
1977
1978    詳細説明:
1979        検出されたQ値の差分が参照周期 `dq_ref` に `gap_factor` を掛けた値よりも大きい場合、
1980        その箇所でフリンジピークのシーケンスが途切れていると判断し、データを複数のクラスターに分割します。
1981        これにより、不連続なフリンジパターンでも個別に解析できるようになります。
1982        各クラスターは少なくとも2つのピークを含む必要があります。
1983
1984    Parameters:
1985        :param q_vals: numpy.ndarray: 検出されたフリンジピークのQ値の配列。
1986        :param dq_ref: float: フリンジ周期の参照値 (FFTから得られた推定値など)。
1987        :param gap_factor: float: クラスター分割の閾値を決定するための係数。
1988
1989    Returns:
1990        :returns: list[numpy.ndarray]: 各クラスターに属するピークのインデックスの配列のリスト。
1991    """
1992    if len(q_vals) < 2: return [np.arange(len(q_vals))]
1993    diffs = np.diff(q_vals)
1994    # 差が基準値より大きい箇所で分割
1995    split_idx = np.where(diffs > gap_factor * dq_ref)[0]
1996    clusters = []
1997    start = 0
1998    for idx in split_idx:
1999        clusters.append(np.arange(start, idx + 1))
2000        start = idx + 1
2001    clusters.append(np.arange(start, len(q_vals)))
2002    return [cl for cl in clusters if len(cl) >= 2] # 2点以上のクラスターのみを保持
2003
2004
2005def build_cluster_peak_tables(omega_cut, residual_for_peaks, wavelength, dq_ref, gap_factor):
2006    """
2007    フリンジピークを検出し、検出されたピークをクラスター化し、
2008    各クラスターで線形回帰分析を実行します。
2009
2010    詳細説明:
2011        1. 残差信号からフリンジピークを検出します。
2012        2. 検出されたピークのQ値を計算します。
2013        3. `detect_fringe_clusters` を使用して、ピークを論理的なクラスターに分割します。
2014        4. 各クラスター内で `assign_fringe_index_auto` でフリンジ次数を割り当て、
2015           `linear_fit_two_stage` で線形回帰を行い、膜厚を推定します。
2016        5. 検出されたピークの詳細 (`all_peak_rows`) とクラスターごとの解析結果 (`cluster_rows`)
2017           および回帰結果 (`cluster_fit_results`) を生成します。
2018
2019    Parameters:
2020        :param omega_cut: numpy.ndarray: 角度範囲を限定したomegaの配列 [deg]。
2021        :param residual_for_peaks: numpy.ndarray: ピーク検出に使用する残差信号。
2022        :param wavelength: float: X線波長 [Å]。
2023        :param dq_ref: float: フリンジ周期の参照値 (Δq)。
2024        :param gap_factor: float: クラスター分割の閾値を決定するための係数。
2025
2026    Returns:
2027        :returns: tuple[list[dict], list[dict], list[dict]]:
2028            - `all_peak_rows`: 検出された全ピークの詳細情報。
2029            - `cluster_rows`: 各クラスターごとの解析サマリー。
2030            - `cluster_fit_results`: 各クラスターの線形回帰の詳細結果。
2031            ピークが検出されない場合は、空のリストとNoneが返されます。
2032    """
2033    # 残差信号からピークを検出
2034    peak_idx, props = find_peaks(residual_for_peaks, prominence=np.max(residual_for_peaks) * 0.02 if np.max(residual_for_peaks)>0 else 0)
2035    if len(peak_idx) == 0: return [], [], None
2036
2037    # プロミネンスが高いピークから最大30個を選択し、角度順にソート
2038    order = np.argsort(props.get("prominences", np.ones(len(peak_idx))))[::-1]
2039    peak_idx = np.sort(peak_idx[order][:30])
2040
2041    theta_deg = omega_cut[peak_idx]
2042    q_vals = 4.0 * np.pi / wavelength * np.sin(np.deg2rad(theta_deg))
2043    
2044    # ピークをクラスターに分割
2045    clusters = detect_fringe_clusters(q_vals, dq_ref, gap_factor)
2046
2047    all_peak_rows, cluster_rows, cluster_fit_results = [], [], []
2048
2049    for cluster_id, cl in enumerate(clusters):
2050        q_cl, twotheta_cl = q_vals[cl], 2.0 * theta_deg[cl]
2051        n_cl = assign_fringe_index_auto(q_cl, dq_ref)
2052        fit_result = linear_fit_two_stage(n_cl, q_cl)
2053        if fit_result is None: continue # 回帰ができなかった場合、このクラスターはスキップ
2054
2055        cluster_fit_results.append({"cluster_id": cluster_id, "n_vals": n_cl, "q_vals": q_cl, "fit_result": fit_result})
2056        for i in range(len(q_cl)):
2057            all_peak_rows.append({
2058                "cluster_id": cluster_id, "n": int(n_cl[i]), "q": float(q_cl[i]), "twotheta": float(twotheta_cl[i]),
2059                "used_stage1": True, "used_stage2": bool(fit_result["used_stage2"][i]),
2060                "resid_stage1": float(fit_result["resid_stage1"][i]), "resid_stage2": float(fit_result["resid_stage2"][i])
2061            })
2062        cluster_rows.append({
2063            "cluster_id": cluster_id, "npeaks": len(q_cl), "dq_est": float(dq_ref),
2064            "thick_fft": float(2.0 * np.pi / dq_ref), "thick_reg": float(fit_result["thickness"]),
2065            "confidence": float(len(q_cl)) # クラスター内のピーク数で信頼度を表現
2066        })
2067    return all_peak_rows, cluster_rows, cluster_fit_results
2068
2069
2070# ============================================================
2071# guess モード 本体
2072# ============================================================
2073def guess_thickness(
2074    substrate, film, energy, omega, intensity, parameter_file, param_sets,
2075    guess_low, guess_high, nsmooth_points, nsmooth_order, nguess_keep,
2076    cluster_gap_factor=DEFAULT_CLUSTER_GAP_FACTOR, yscale="log", infile=""
2077):
2078    """
2079    XRDフリンジ解析に基づいて膜厚を推定します。解析結果をプロットし、上位候補をパラメータCSVに保存します。
2080
2081    詳細説明:
2082        1. 指定された角度範囲でXRDデータの一部を切り出します。
2083        2. Savitzky-Golayフィルタを使用して強度データからエンベロープを除去し、残差信号を抽出します。
2084        3. 残差信号にFFTを適用し、主要な周波数成分から膜厚の初期候補 (Δq_est) を算出します。
2085        4. 残差信号からフリンジピークを検出し、Δq_estとギャップ係数に基づいてピークをクラスターに分割します。
2086        5. 各クラスター内でフリンジ次数を割り当て、線形回帰分析を行い、最終的な膜厚候補を算出します。
2087        6. 検出されたピーク情報とクラスター情報をCSVファイルに保存します。
2088        7. FFTと線形回帰の両方から得られた膜厚候補をスコア順にソートし、上位の候補をコンソールに表示します。
2089        8. 最もスコアの高い膜厚候補をプライマリパラメータセットの膜厚値として設定し、
2090           上位の候補はPSO粒子としてパラメータファイルに保存されます。
2091        9. 解析の主要なステップ(信号・残差、FFTスペクトル、n-qプロット)を視覚化します。
2092
2093    Parameters:
2094        :param substrate: xrayutilities.materials.Crystal: 基板材料オブジェクト。
2095        :param film: xrayutilities.materials.Crystal: 膜材料オブジェクト。
2096        :param energy: float: X線エネルギー [eV]。
2097        :param omega: numpy.ndarray: 全角度範囲のomega値の配列 [deg]。
2098        :param intensity: numpy.ndarray: 全強度データ。
2099        :param parameter_file: str: パラメータの保存先ファイルパス。
2100        :param param_sets: dict: パラメータセット(推定結果がここに保存されます)。
2101        :param guess_low: float: 解析を開始する角度 [deg]。
2102        :param guess_high: float: 解析を終了する角度 [deg]。
2103        :param nsmooth_points: int: Savitzky-Golayフィルタのウィンドウ点数。
2104        :param nsmooth_order: int: Savitzky-Golayフィルタの多項式次数。
2105        :param nguess_keep: int: 保持する膜厚候補の数。
2106        :param cluster_gap_factor: float: フリンジクラスター判定用のギャップ係数。
2107        :param yscale: str: プロットのY軸スケール ("linear" または "log")。
2108        :param infile: str: 入力データファイル名(出力ファイル名生成に使用)。
2109
2110    Returns:
2111        :returns: dict: 更新されたパラメータセットを含む `param_sets` 辞書。
2112    """
2113    mask = (omega >= guess_low) & (omega <= guess_high)
2114    if np.sum(mask) < 10:
2115        print("Error: guess range contains too few points.")
2116        return param_sets
2117
2118    omega_cut, int_cut = omega[mask], intensity[mask]
2119    y_signal = np.log(np.maximum(int_cut, 1e-30)) if yscale == "log" else int_cut.copy()
2120
2121    # Savitzky-Golayフィルタのパラメータを調整
2122    npts, norder = ensure_valid_savgol(nsmooth_points, nsmooth_order, len(y_signal))
2123    envelope = savgol_filter(y_signal, window_length=npts, polyorder=norder)
2124    residual = y_signal - envelope
2125
2126    wavelength = xu.wavelength("CuKa1") # Cu Kα1波長を取得
2127
2128    # FFTによる膜厚推定
2129    fft_cands, _, _, freq, fft_amp = estimate_thickness_from_fft_signal(omega_cut, residual, wavelength, keep_n=max(nguess_keep, 10))
2130    if not fft_cands:
2131        print("Warning: No FFT candidates found for thickness estimation.")
2132        return param_sets
2133
2134    dq_ref = fft_cands[0]["dq"] # FFTで最も支配的なΔqを基準とする
2135
2136    # ピーク検出とクラスター化、線形回帰
2137    # 残差が負の値にならないように調整してピーク検出
2138    peak_rows, cluster_rows, cluster_fits = build_cluster_peak_tables(omega_cut, np.maximum(residual - np.min(residual), 0), wavelength, dq_ref, cluster_gap_factor)
2139
2140    if peak_rows: save_guess_peak_csv(get_guess_peaks_filename(infile), peak_rows)
2141    if cluster_rows: save_guess_cluster_csv(get_guess_clusters_filename(infile), cluster_rows)
2142
2143    # 全ての膜厚候補を収集し、スコアでソート
2144    all_cands = []
2145    for c in fft_cands[:nguess_keep]:
2146        all_cands.append({"method": "fft", "thick": c["thick"], "score": c["score"]})
2147    for r in cluster_rows:
2148        if np.isfinite(r["thick_reg"]): # 回帰による膜厚が有効な場合のみ追加
2149            all_cands.append({"method": f"cl{r['cluster_id']}-reg", "thick": r["thick_reg"], "score": r["confidence"]})
2150
2151    all_cands = sorted(all_cands, key=lambda x: x["score"], reverse=True)
2152
2153    if not all_cands:
2154        print("Warning: No valid thickness candidates found.")
2155        return param_sets
2156
2157    # 最良の膜厚をプライマリセットに設定
2158    best_ps = clone_parameter_set(param_sets[DEFAULT_PRIMARY_SET])
2159    best_ps["thick"]["value"] = all_cands[0]["thick"]
2160    set_best_parameter_set(param_sets, best_ps)
2161
2162    # 上位の候補をPSOの粒子としてパラメータファイルに保存
2163    pso_cands = []
2164    for c in all_cands[:nguess_keep]:
2165        ps = clone_parameter_set(param_sets[DEFAULT_PRIMARY_SET])
2166        ps["thick"]["value"] = c["thick"]
2167        pso_cands.append(ps)
2168    param_sets = replace_pso_parameter_sets(param_sets, pso_cands)
2169    save_parameters(parameter_file, param_sets)
2170
2171    print("推定膜厚候補:")
2172    for i, c in enumerate(all_cands[:10]): # 上位10件を表示
2173        print(f"  {i:2d} : {c['method']:10s} thick= {c['thick']:.2f} A")
2174
2175    # プロット生成
2176    plt.ion() # インタラクティブモードON
2177
2178    # Figure 1: Signal/Envelope/Residual
2179    fig1, ax1 = plt.subplots(figsize=(8, 5))
2180    ax1.plot(omega_cut, y_signal, label="signal")
2181    ax1.plot(omega_cut, envelope, label="envelope")
2182    ax1.plot(omega_cut, residual, label="residual")
2183    ax1.legend(); ax1.set_title("Fringe Analysis: Signal & Residual")
2184    ax1.set_xlabel(r"$\omega\ (deg)$")
2185    ax1.set_ylabel("Intensity (arb. u.)")
2186    ax1.set_yscale(yscale)
2187
2188
2189    # Figure 2: FFT Spectrum
2190    fig2, ax2 = plt.subplots(figsize=(8, 5))
2191    if len(freq) > 1 and len(fft_amp) > 1: # DC成分以外を表示
2192        ax2.plot(freq[1:], fft_amp[1:])
2193    ax2.set_title("FFT Spectrum")
2194    ax2.set_xlabel(r"Frequency ($1/\AA^{-1}$)")
2195    ax2.set_ylabel("Amplitude")
2196
2197
2198    # Figure 3: N-Q plot (フリンジ次数 vs Q値)
2199    if cluster_fits:
2200        fig3, ax3 = plt.subplots(figsize=(7, 5))
2201        for f in cluster_fits:
2202            # Stage 2で採用されたピークのみを強調表示
2203            used_peaks_q = f["q_vals"][f["fit_result"]["used_stage2"]]
2204            used_peaks_n = f["n_vals"][f["fit_result"]["used_stage2"]]
2205            rejected_peaks_q = f["q_vals"][~f["fit_result"]["used_stage2"]]
2206            rejected_peaks_n = f["n_vals"][~f["fit_result"]["used_stage2"]]
2207
2208            ax3.plot(used_peaks_n, used_peaks_q, 'o', label=f"cl {f['cluster_id']} (used)")
2209            if len(rejected_peaks_n) > 0:
2210                ax3.plot(rejected_peaks_n, rejected_peaks_q, 'x', color=ax3.lines[-1].get_color(), alpha=0.5, label=f"cl {f['cluster_id']} (rejected)")
2211            
2212            # 回帰直線を描画
2213            ax3.plot(f["n_vals"], np.polyval(f["fit_result"]["coef2"], f["n_vals"]), '--', color=ax3.lines[-1].get_color(), label=f"Fit cl {f['cluster_id']}")
2214        
2215        ax3.legend(); ax3.set_title("Linear Regression: n vs q")
2216        ax3.set_xlabel("Fringe Order (n)")
2217        ax3.set_ylabel(r"Q ($1/\AA^{-1}$)")
2218
2219    plt.ioff(); plt.show() # 非インタラクティブモードに戻し、全てのプロットを表示
2220    return param_sets
2221
2222
2223# ============================================================
2224# メイン実行ルーチン
2225# ============================================================
2226def main(args):
2227    """
2228    コマンドライン引数に基づき、XRDシミュレーション、データ読み込み、フィッティング、または膜厚推定を実行します。
2229
2230    詳細説明:
2231        この関数は、プログラムのエントリポイントであり、以下の処理フローに従います。
2232        1. 基板と膜の材料モデルを準備します。
2233        2. パラメータファイルから初期パラメータを読み込み、固定パラメータを適用して保存します。
2234        3. `args.mode` に応じて、以下のいずれかの操作を実行します。
2235            - "read": 2theta-intensityデータを読み込み、プロットします。
2236            - "sim": 指定されたパラメータでXRDパターンをシミュレートし、プロットします。
2237                     入力ファイルがある場合は実験データと比較してプロットします。
2238            - "guess": フリンジ解析に基づいて膜厚を推定し、結果をプロットしてパラメータファイルを更新します。
2239            - "fit": 指定された最適化手法 (random, nelder-mead, bfgs, cg, pso) を使用して、
2240                     実験データにフィッティングを行い、パラメータを最適化します。
2241                     フィッティングの進捗はライブプロットで表示されます。
2242
2243    Parameters:
2244        :param args: argparse.Namespace: コマンドライン引数をパースした結果のオブジェクト。
2245    """
2246    substrate, film = prepare_materials(args.substrate_file, args.film_file)
2247    # Cu Kα1波長を使用してX線エネルギーを計算
2248    energy = 12390.0 / xu.wavelength("CuKa1")
2249
2250    parameter_file = get_parameter_filename(args.infile)
2251    param_sets = read_parameters(parameter_file)
2252    apply_fix_list(param_sets, parse_fix_list(args.fix))
2253    save_parameters(parameter_file, param_sets) # 初期/読み込み済みパラメータを保存
2254
2255    if args.mode == "read":
2256        twotheta, omega, intensity = read_data(args.infile, substrate, film, energy)
2257        plot_xrd(omega, [intensity], ["ReadData"], "XRDデータ読込", yscale=args.yscale, wait=True)
2258
2259    elif args.mode == "sim":
2260        sim_ps = param_sets[DEFAULT_PRIMARY_SET]
2261        twotheta_sim, omega_sim = make_scan_axis()
2262        int_sim = calc_pattern_from_param_set(substrate, film, energy, omega_sim, sim_ps)
2263        if args.infile:
2264            # 入力ファイルがある場合、それを読み込みシミュレーション結果と比較プロット
2265            _, omega_ref, int_ref = read_data(args.infile, substrate, film, energy)
2266            plot_xrd(omega_sim, [int_ref, int_sim], ["Data", "Sim"], "シミュレーション比較", yscale=args.yscale, wait=True)
2267        else:
2268            # 入力ファイルがない場合、シミュレーション結果のみをプロット
2269            plot_xrd(omega_sim, [int_sim], ["Sim"], "XRDシミュレーション", yscale=args.yscale, wait=True)
2270
2271    elif args.mode == "guess":
2272        _, omega, intensity = read_data(args.infile, substrate, film, energy)
2273        guess_thickness(substrate, film, energy, omega, intensity, parameter_file, param_sets, args.guess_low, args.guess_high, args.nsmooth_points, args.nsmooth_order, args.nguess_keep, args.cluster_gap_factor, args.yscale, args.infile)
2274
2275    elif args.mode == "fit":
2276        _, omega, int_target = read_data(args.infile, substrate, film, energy)
2277        if args.method == "random":
2278            fit_random(substrate, film, energy, omega, int_target, param_sets, parameter_file, args.yscale, args.residual_scale)
2279        elif args.method == "pso":
2280            fit_pso(substrate, film, energy, omega, int_target, param_sets, parameter_file, args.nmaxiter, args.tol, args.pso_nparticles, args.pso_w, args.pso_c1, args.pso_c2, args.yscale, args.residual_scale, args.pso_stall_max, args.pso_spread_rtol)
2281        else:
2282            fit_scipy(substrate, film, energy, omega, int_target, param_sets, parameter_file, args.method, args.nmaxiter, args.tol, args.yscale, args.residual_scale)
2283
2284if __name__ == "__main__":
2285    parser = build_parser()
2286    try:
2287        main(initialize(parser))
2288    except Exception:
2289        traceback.print_exc()
2290        sys.exit(1)