tiny_simulations.md_packets_FuncAnimation のソースコード

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
1D coupled oscillator wave-packet collision / scattering
with argparse options and optional GIF save.

Site mass:
  m_site[i]

Bond spring constants:
  k2_bond[i] : spring between site i and i+1
  k4_bond[i] : nonlinear spring between site i and i+1

Potential:
  V = sum_i [ 1/2 k2_bond[i] (u[i+1]-u[i])^2
            + 1/4 k4_bond[i] (u[i+1]-u[i])^4 ]

Force:
  Let du[i] = u[i+1] - u[i]
  Let g[i]  = k2_bond[i] du[i] + k4_bond[i] du[i]^3

  Then for an interior site j:
      F[j] = g[j] - g[j-1]
"""
"""
.. _md_packets_FuncAnimation:

1次元結合振動子波束散乱シミュレーション

概要:
    1次元結合振動子系における波束の衝突・散乱をシミュレートし、アニメーションで可視化します。
    コマンドライン引数により、シミュレーションパラメータや散乱体の設定、GIF保存の有無などを細かく制御できます。

詳細説明:
    このスクリプトは、線形および4次の非線形ばねで結合された1次元の質点系をシミュレートします。
    初期条件としてガウス波束を設定し、その伝播と、システムに配置された散乱体(質量欠陥、結合欠陥、バリアなど)との相互作用を観測します。
    時間積分にはオイラー法またはヴェルレ法が選択可能です。
    結果はMatplotlibのFuncAnimationを使用してリアルタイムで表示され、オプションでGIFとして保存することもできます。
    さらに、シミュレーション終了後には、FFTによる波数空間のパワー分布と、全エネルギーの時間履歴がプロットされます。

サイト質量:
    m_site[i]: サイト i の質量

結合ばね定数:
    k2_bond[i]: サイト i とサイト i+1 間の線形ばね定数
    k4_bond[i]: サイト i とサイト i+1 間の非線形ばね定数

ポテンシャルエネルギー:
    V = sum_i [ 1/2 k2_bond[i] (u[i+1]-u[i])^2
              + 1/4 k4_bond[i] (u[i+1]-u[i])^4 ]

力:
    du[i] = u[i+1] - u[i]
    g[i]  = k2_bond[i] du[i] + k4_bond[i] du[i]^3

    内部サイト j における力:
        F[j] = g[j] - g[j-1]

関連リンク:
    :doc:`md_packets_FuncAnimation_usage`
"""

import argparse
import re
import sys
from pathlib import Path

import numpy as np
import matplotlib.pyplot as plt
from matplotlib.animation import FuncAnimation


# ============================================================
# argparse
# ============================================================
[ドキュメント] def build_argparser(): """ 概要: コマンドライン引数を解析するためのArgumentParserオブジェクトを構築します。 詳細説明: 1次元結合振動子波束散乱シミュレーションの様々なパラメータを定義します。 これには、基本的なシミュレーション設定、波束パラメータ、散乱体パラメータ、 および出力/表示に関するオプションが含まれます。 戻り値: :returns: argparse.ArgumentParser: 構築されたArgumentParserオブジェクト。 """ p = argparse.ArgumentParser( description=( "1D coupled oscillator wave-packet scattering simulation " "with linear + quartic springs." ), formatter_class=argparse.ArgumentDefaultsHelpFormatter, ) # -------------------------- # basic simulation parameters # -------------------------- p.add_argument("--n", type=int, default=200, help="number of masses") p.add_argument("--a", type=float, default=1.0, help="lattice spacing") p.add_argument("--m0", type=float, default=1.0, help="background mass") p.add_argument("--k2", type=float, default=5.0, help="background linear spring constant") p.add_argument("--k4", type=float, default=500.0, help="background nonlinear spring constant") p.add_argument( "--method", type=str, default="verlet", choices=["euler", "verlet"], help="time-integration method", ) p.add_argument("--dt", type=float, default=0.05, help="time step") p.add_argument("--nstep", type=int, default=1000, help="number of MD/animation steps") p.add_argument("--interval", type=int, default=1, help="animation interval [ms]") p.add_argument( "--save_interval", type=int, default=10, help="interval for FFT and energy snapshots", ) p.add_argument( "--boundary", type=str, default="fixed", choices=["fixed"], help="boundary condition", ) # -------------------------- # wave packet parameters # -------------------------- p.add_argument("--A", type=float, default=0.20, help="packet amplitude") p.add_argument("--sigma", type=float, default=2.0, help="packet width") p.add_argument("--k0", type=float, default=0.1, help="central wave number") p.add_argument( "--initial_mode", type=str, default="left_only", choices=["left_only", "two_packets"], help="initial packet configuration", ) p.add_argument( "--x0_left", type=float, default=None, help="left packet center. If omitted, n*a*0.25 is used.", ) p.add_argument( "--x0_right", type=float, default=None, help="right packet center. If omitted, n*a*0.75 is used.", ) # -------------------------- # scatterer parameters # -------------------------- p.add_argument( "--scatterer_mode", type=str, default="heavy_mass", choices=[ "none", "heavy_mass", "light_mass", "soft_bond", "hard_bond", "nonlinear_bond", "barrier", ], help="scatterer type", ) p.add_argument( "--scatterer_center", type=int, default=None, help="scatterer center index. If omitted, n//2 is used.", ) # The following values reproduce the current hard-coded settings by default. p.add_argument("--mass_width", type=int, default=3, help="mass-defect width") p.add_argument("--heavy_mass_factor", type=float, default=10.0, help="heavy mass factor") p.add_argument("--light_mass_factor", type=float, default=0.2, help="light mass factor") p.add_argument("--bond_width", type=int, default=4, help="bond-defect width") p.add_argument("--soft_bond_k2_factor", type=float, default=0.2, help="soft-bond k2 factor") p.add_argument("--soft_bond_k4_factor", type=float, default=1.0, help="soft-bond k4 factor") p.add_argument("--hard_bond_k2_factor", type=float, default=5.0, help="hard-bond k2 factor") p.add_argument("--hard_bond_k4_factor", type=float, default=1.0, help="hard-bond k4 factor") p.add_argument("--nonlinear_bond_k2_factor", type=float, default=1.0, help="nonlinear-bond k2 factor") p.add_argument("--nonlinear_bond_k4_factor", type=float, default=10.0, help="nonlinear-bond k4 factor") p.add_argument("--barrier_half_width", type=int, default=3, help="barrier half width") p.add_argument("--barrier_mass", type=float, default=8.0, help="barrier mass value") p.add_argument("--barrier_k2", type=float, default=20.0, help="barrier k2 value") p.add_argument( "--barrier_k4", type=float, default=None, help="barrier k4 value. If omitted, background k4 is used.", ) # -------------------------- # output / display # -------------------------- p.add_argument( "--save", type=int, default=0, choices=[0, 1], help="save GIF with ani.save after the animation window is closed", ) p.add_argument( "--gif_fps", type=int, default=30, help="frames per second for GIF save", ) p.add_argument( "--gif_dpi", type=int, default=100, help="DPI for GIF save", ) return p
[ドキュメント] def fmt_param(x): """ 概要: パラメータ値をファイル名に適した短い文字列に変換します。 詳細説明: 浮動小数点数などの値を、`.`を`p`に、`-`を`m`に置換することで、 ファイル名に安全な形式に変換します。 引数: :param x: Any: 変換するパラメータ値。 戻り値: :returns: str: ファイル名に適した短い文字列。 """ s = f"{x:g}" s = s.replace("-", "m").replace("+", "") s = s.replace(".", "p") return s
[ドキュメント] def safe_stem(s): """ 概要: スクリプトのステムをWindowsで安全な出力ファイル名にサニタイズします。 詳細説明: ファイル名に使用できない文字(数字、英字、アンダースコア、ハイフン以外)を アンダースコアに置換し、前後のアンダースコアを削除します。 これにより、異なるOS環境でも安全なファイル名が生成されます。 引数: :param s: str: サニタイズする文字列。 戻り値: :returns: str: サニタイズされた文字列。 """ return re.sub(r"[^0-9A-Za-z_\-]+", "_", s).strip("_") or "md_packets"
[ドキュメント] def make_gif_filename(args): """ 概要: GIF保存用のファイル名を生成します。 詳細説明: スクリプト名、シミュレーションメソッド、初期モード、散乱体モード、 ばね定数k2、k4の値に基づいてファイル名を構築します。 生成されるファイル名は、シミュレーション設定を反映したものになります。 引数: :param args: argparse.Namespace: コマンドライン引数を格納するオブジェクト。 戻り値: :returns: str: 生成されたGIFファイル名。 """ stem = safe_stem(Path(sys.argv[0]).stem) return ( f"{stem}_{args.method}_{args.initial_mode}_{args.scatterer_mode}_" f"{fmt_param(args.k2)}_{fmt_param(args.k4)}.gif" )
[ドキュメント] def main(argv=None): """ 概要: 1次元結合振動子波束散乱シミュレーションのメインエントリポイントです。 詳細説明: この関数は、コマンドライン引数を解析し、シミュレーションを初期化、実行し、 結果を可視化します。具体的には、以下の処理を行います。 1. 引数の解析とパラメータの初期設定。 2. サイト質量と結合ばね定数配列の準備。 3. `scatterer_mode`に基づいた散乱体の設定。 4. 初期波束(変位と速度)の生成。 5. シミュレーション中の状態(FFTスナップショット、エネルギー履歴)の記録。 6. Matplotlib FuncAnimationによるシミュレーションのリアルタイム可視化。 7. オプションでシミュレーション結果のアニメーションGIF保存。 8. FFTおよびエネルギーの時間履歴のプロットによる詳細分析。 9. シミュレーション終了時のエネルギー保存のレポート。 引数: :param argv: list, optional: コマンドライン引数のリスト。 デフォルトはNoneで、`sys.argv`を使用します。 戻り値: :returns: None """ args = build_argparser().parse_args(argv) # ============================================================ # parameters # ============================================================ n = args.n if n < 3: raise ValueError("--n must be >= 3") a = args.a m0 = args.m0 k2_0 = args.k2 k4_0 = args.k4 method = args.method dt = args.dt nstep = args.nstep interval = args.interval save_interval = args.save_interval boundary = args.boundary A = args.A sigma = args.sigma k0 = args.k0 initial_mode = args.initial_mode x0_left = args.x0_left if args.x0_left is not None else n * a * 0.25 x0_right = args.x0_right if args.x0_right is not None else n * a * 0.75 scatterer_mode = args.scatterer_mode scatterer_center = args.scatterer_center if args.scatterer_center is not None else n // 2 barrier_k4 = args.barrier_k4 if args.barrier_k4 is not None else k4_0 # ============================================================ # equilibrium positions # ============================================================ x_eq = np.arange(n) * a # ============================================================ # site and bond arrays # ============================================================ m_site = np.full(n, m0, dtype=float) # k2_bond[i], k4_bond[i] are for the bond between site i and i+1 k2_bond = np.full(n - 1, k2_0, dtype=float) k4_bond = np.full(n - 1, k4_0, dtype=float) scatterer_spans = [] # ============================================================ # scatterer helper functions # ============================================================ def add_mass_defect(center, width=1, m_factor=5.0): """ 概要: 質量欠陥を追加します。 詳細説明: 指定された中心サイトと幅に基づいて、サイトの質量を乗算係数で変更します。 質量欠陥が適用されるサイトの範囲は、`[center - width // 2, center - width // 2 + width)` です。 計算された範囲は、配列の有効なインデックス内に収まるようにクリップされます。 欠陥領域は `scatterer_spans` リストに記録され、プロットに表示されます。 引数: :param center: int: 欠陥領域の中心サイトインデックス。 :param width: int, optional: 欠陥領域のサイト数。デフォルトは1。 :param m_factor: float, optional: 質量に乗算する係数。例: `m_factor=10` は質量を10倍にします。 デフォルトは5.0。 戻り値: :returns: None """ i1 = max(0, int(center - width // 2)) i2 = min(n, int(i1 + width)) if i2 <= i1: return m_site[i1:i2] *= m_factor x1 = x_eq[i1] x2 = x_eq[i2 - 1] scatterer_spans.append((x1, x2, f"mass x{m_factor:g}")) def set_mass_region(i1, i2, m_value): """ 概要: 指定された範囲のサイト質量を設定します。 詳細説明: サイトのインデックス範囲`[i1, i2)` (i2は排他的) の`m_site`の値を`m_value`に設定します。 計算された範囲は、配列の有効なインデックス内に収まるようにクリップされます。 欠陥領域は `scatterer_spans` リストに記録され、プロットに表示されます。 引数: :param i1: int: 質量を設定する開始サイトインデックス (含む)。 :param i2: int: 質量を設定する終了サイトインデックス (排他的)。 :param m_value: float: 設定する質量値。 戻り値: :returns: None """ i1 = max(0, int(i1)) i2 = min(n, int(i2)) if i2 <= i1: return m_site[i1:i2] = m_value x1 = x_eq[i1] x2 = x_eq[i2 - 1] scatterer_spans.append((x1, x2, f"m={m_value:g}")) def add_bond_defect(center, width=1, k2_factor=1.0, k4_factor=1.0): """ 概要: 結合欠陥を追加します。 詳細説明: これはサイトではなく結合を変更します。 `k2_bond[i]`と`k4_bond[i]`はサイト`i`とサイト`i+1`間のばね定数です。 指定された中心結合と幅に基づいて、結合のばね定数`k2_bond`と`k4_bond`を乗算係数で変更します。 結合欠陥が適用される結合の範囲は、`[center - width // 2, center - width // 2 + width)` です。 計算された範囲は、配列の有効なインデックス内に収まるようにクリップされます。 欠陥領域は `scatterer_spans` リストに記録され、プロットに表示されます。 引数: :param center: int: 欠陥領域の中心結合インデックス。 :param width: int, optional: 欠陥領域の結合数。デフォルトは1。 :param k2_factor: float, optional: k2に乗算する係数。デフォルトは1.0。 :param k4_factor: float, optional: k4に乗算する係数。デフォルトは1.0。 戻り値: :returns: None """ i1 = max(0, int(center - width // 2)) i2 = min(n - 1, int(i1 + width)) if i2 <= i1: return k2_bond[i1:i2] *= k2_factor k4_bond[i1:i2] *= k4_factor x1 = x_eq[i1] x2 = x_eq[i2] scatterer_spans.append( (x1, x2, f"k2 x{k2_factor:g}, k4 x{k4_factor:g}") ) def set_bond_region(i1, i2, k2_value=None, k4_value=None): """ 概要: 指定された結合領域のばね定数を設定します。 詳細説明: 結合インデックス範囲`[i1, i2)` (i2は排他的) のばね定数`k2_bond`と`k4_bond`の値を変更します。 `k2_value`または`k4_value`がNoneでない場合、対応するばね定数が設定値に置き換えられます。 計算された範囲は、配列の有効なインデックス内に収まるようにクリップされます。 欠陥領域は `scatterer_spans` リストに記録され、プロットに表示されます。 引数: :param i1: int: ばね定数を設定する開始結合インデックス (含む)。 :param i2: int: ばね定数を設定する終了結合インデックス (排他的)。 :param k2_value: float or None, optional: 設定するk2値。Noneの場合、変更されません。 :param k4_value: float or None, optional: 設定するk4値。Noneの場合、変更されません。 戻り値: :returns: None """ i1 = max(0, int(i1)) i2 = min(n - 1, int(i2)) if i2 <= i1: return label_parts = [] if k2_value is not None: k2_bond[i1:i2] = k2_value label_parts.append(f"k2={k2_value:g}") if k4_value is not None: k4_bond[i1:i2] = k4_value label_parts.append(f"k4={k4_value:g}") if label_parts: x1 = x_eq[i1] x2 = x_eq[i2] scatterer_spans.append((x1, x2, ", ".join(label_parts))) # ============================================================ # scatterer settings # ============================================================ def setup_scatterer(): """ 概要: コマンドライン引数に基づいて散乱体を設定します。 詳細説明: `scatterer_mode`引数の値に応じて、異なる種類の散乱体(質量欠陥、結合欠陥、またはバリア)を シミュレーションシステムに適用します。 各モードは、対応するヘルパー関数(`add_mass_defect`, `add_bond_defect`, `set_mass_region`, `set_bond_region`)を呼び出します。 `--scatterer_center`, `--mass_width`, `--bond_width` などの引数で散乱体の特性を制御できます。 戻り値: :returns: None 例外: ValueError: 未知の`scatterer_mode`が指定された場合。 """ if scatterer_mode == "none": pass elif scatterer_mode == "heavy_mass": # A local heavy-mass defect near the center. # This gives reflection and transmission. add_mass_defect( center=scatterer_center, width=args.mass_width, m_factor=args.heavy_mass_factor, ) elif scatterer_mode == "light_mass": # A local light-mass defect. add_mass_defect( center=scatterer_center, width=args.mass_width, m_factor=args.light_mass_factor, ) elif scatterer_mode == "soft_bond": # A soft spring region. add_bond_defect( center=scatterer_center, width=args.bond_width, k2_factor=args.soft_bond_k2_factor, k4_factor=args.soft_bond_k4_factor, ) elif scatterer_mode == "hard_bond": # A hard spring region. add_bond_defect( center=scatterer_center, width=args.bond_width, k2_factor=args.hard_bond_k2_factor, k4_factor=args.hard_bond_k4_factor, ) elif scatterer_mode == "nonlinear_bond": # Only nonlinear spring constant is enhanced. add_bond_defect( center=scatterer_center, width=args.bond_width, k2_factor=args.nonlinear_bond_k2_factor, k4_factor=args.nonlinear_bond_k4_factor, ) elif scatterer_mode == "barrier": # A wider barrier-like region. hw = args.barrier_half_width set_mass_region(scatterer_center - hw, scatterer_center + hw + 1, m_value=args.barrier_mass) set_bond_region(scatterer_center - hw, scatterer_center + hw, k2_value=args.barrier_k2, k4_value=barrier_k4) else: raise ValueError(f"Unknown scatterer_mode: {scatterer_mode}") setup_scatterer() # ============================================================ # reference dispersion relation for the homogeneous incident region # ============================================================ def omega_k(k): """ 概要: 均質な入射領域における線形鎖の分散関係を計算します。 詳細説明: `omega(k) = 2 sqrt(k2_0/m0) |sin(k a / 2)|` の式を用いて、 波数`k`に対応する角振動数`omega`を計算します。 この関数は主に、初期入射波束の構築に使用されます。 引数: :param k: float: 波数を表す値。 戻り値: :returns: float: 波長kに対応する角振動数。 """ return 2.0 * np.sqrt(k2_0 / m0) * np.abs(np.sin(0.5 * k * a)) def vg_k(k): """ 概要: 均質な入射領域における群速度 `d omega / d k` を計算します。 詳細説明: `vg = sqrt(k2_0/m0) a cos(k a / 2) sign(k)` の式を用いて、 波数`k`に対応する群速度を計算します。 `k=0`の場合は群速度を0とします。 この関数は初期入射波束の構築にのみ使用されます。 引数: :param k: float: 波数を表す値。 戻り値: :returns: float: 波長kに対応する群速度。 """ if k == 0: return 0.0 return ( np.sqrt(k2_0 / m0) * a * np.cos(0.5 * np.abs(k) * a) * np.sign(k) ) # ============================================================ # traveling wave packet # ============================================================ def packet_u(x, x0, k): """ 概要: ガウス波束の初期変位を計算します。 詳細説明: `u(x,0) = A exp[-(x-x0)^2/(2 sigma^2)] cos[k(x-x0)]` の式に基づいて、 時刻`t=0`における各サイトの変位を計算します。 ここで、`A`は振幅、`sigma`は波束の幅、`k`は中心波数、`x0`は波束の中心位置です。 引数: :param x: numpy.ndarray: サイトの平衡位置の配列。 :param x0: float: 波束の中心位置。 :param k: float: 中心波数。 戻り値: :returns: numpy.ndarray: 各サイトの初期変位の配列。 """ dx = x - x0 env = np.exp(-dx**2 / (2.0 * sigma**2)) return A * env * np.cos(k * dx) def packet_v(x, x0, k): """ 概要: 進行するガウス波束の初期速度を計算します。 詳細説明: 近似的に `u(x,t) = A exp[-(x-x0-vg t)^2/(2 sigma^2)] cos[k(x-x0) - omega t]` と仮定し、時刻`t=0`における変位の時間微分 `v(x,0) = du/dt` を計算します。 これにより、波束が初期に`vg0`の群速度で進行するような速度場が設定されます。 `k > 0` の場合、波束は正のx方向へ移動し、`k < 0` の場合は負のx方向へ移動します。 引数: :param x: numpy.ndarray: サイトの平衡位置の配列。 :param x0: float: 波束の中心位置。 :param k: float: 中心波数。 戻り値: :returns: numpy.ndarray: 各サイトの初期速度の配列。 """ dx = x - x0 env = np.exp(-dx**2 / (2.0 * sigma**2)) omega0 = omega_k(k) vg0 = vg_k(k) return A * env * ( vg0 * dx / sigma**2 * np.cos(k * dx) + omega0 * np.sin(k * dx) ) # ============================================================ # force calculation # ============================================================ def force(u): """ 概要: 最近接結合の線形および4次非線形ばねによる力を計算します。 詳細説明: 各サイトに作用する力は、隣接するサイトとの変位差と、 線形および4次非線形ばね定数`k2_bond`、`k4_bond`に基づいて計算されます。 `k2_bond[i]`と`k4_bond[i]`はサイト`i`と`i+1`間のばね定数です。 固定端境界条件が適用される場合、両端のサイトの力は0に設定されます。 引数: :param u: numpy.ndarray: 各サイトの変位の配列。 戻り値: :returns: numpy.ndarray: 各サイトに作用する力の配列。 """ f = np.zeros_like(u) du = u[1:] - u[:-1] # bond force-like quantity # g[i] = dV_i / d(u[i+1]-u[i]) g = k2_bond * du + k4_bond * du**3 # interior sites f[1:-1] = g[1:] - g[:-1] # endpoints are fixed, so forces there are not used f[0] = 0.0 f[-1] = 0.0 return f def apply_boundary(u, v): """ 概要: 固定境界条件を適用します。 詳細説明: `boundary`変数が`"fixed"`の場合、両端のサイト(インデックス0と-1)の変位`u`と 速度`v`をゼロに設定します。これにより、両端が壁に固定された効果をシミュレートします。 他の境界条件が指定された場合はエラーを発生させます。 引数: :param u: numpy.ndarray: 各サイトの変位の配列。 :param v: numpy.ndarray: 各サイトの速度の配列。 戻り値: :returns: tuple[numpy.ndarray, numpy.ndarray]: 境界条件が適用された変位と速度の配列。 例外: ValueError: 未知の境界条件が指定された場合。 """ if boundary == "fixed": u[0] = 0.0 u[-1] = 0.0 v[0] = 0.0 v[-1] = 0.0 else: raise ValueError(f"Unknown boundary condition: {boundary}") return u, v # ============================================================ # energy calculation # ============================================================ def total_energy(u, v): """ 概要: システムの全エネルギー(運動エネルギー + ポテンシャルエネルギー)を計算します。 詳細説明: 各サイトの運動エネルギーと、隣接するサイト間の線形および4次非線形ばねによる ポテンシャルエネルギーの合計を計算します。 引数: :param u: numpy.ndarray: 各サイトの変位の配列。 :param v: numpy.ndarray: 各サイトの速度の配列。 戻り値: :returns: float: システムの総エネルギー。 """ kinetic = 0.5 * np.sum(m_site * v**2) du = u[1:] - u[:-1] potential2 = 0.5 * np.sum(k2_bond * du**2) potential4 = 0.25 * np.sum(k4_bond * du**4) return kinetic + potential2 + potential4 # ============================================================ # initial condition # ============================================================ def make_initial_condition(): """ 概要: 初期条件(変位と速度)を生成します。 詳細説明: `initial_mode`引数の値に応じて、初期波束の構成を選択します。 - `"left_only"`: 左側から右方向へ進行する単一のガウス波束を生成します。 - `"two_packets"`: 左から右へ、右から左へ進行する2つのガウス波束を生成します。 波束生成後、`apply_boundary`関数を呼び出して境界条件を適用します。 戻り値: :returns: tuple[numpy.ndarray, numpy.ndarray]: 初期変位と初期速度の配列。 例外: ValueError: 未知の`initial_mode`が指定された場合。 """ if initial_mode == "left_only": # One packet from left to right. # This is useful for seeing scattering by a defect. u0 = packet_u(x_eq, x0_left, +k0) v0 = packet_v(x_eq, x0_left, +k0) elif initial_mode == "two_packets": # Left packet moves right: k = +k0 # Right packet moves left: k = -k0 u0 = ( packet_u(x_eq, x0_left, +k0) + packet_u(x_eq, x0_right, -k0) ) v0 = ( packet_v(x_eq, x0_left, +k0) + packet_v(x_eq, x0_right, -k0) ) else: raise ValueError(f"Unknown initial_mode: {initial_mode}") u0, v0 = apply_boundary(u0, v0) return u0, v0 u_initial, v_initial = make_initial_condition() u = u_initial.copy() v = v_initial.copy() E0 = total_energy(u, v) # ============================================================ # console output # ============================================================ print("=== Simulation settings ===") print(f"n = {n}") print(f"a = {a}") print(f"m0 = {m0}") print(f"k2_0 = {k2_0}") print(f"k4_0 = {k4_0}") print(f"dt = {dt}") print(f"nstep = {nstep}") print(f"method = {method}") print(f"boundary = {boundary}") print(f"scatterer_mode = {scatterer_mode}") print(f"initial_mode = {initial_mode}") print(f"save = {args.save}") print() print("=== Array summary ===") print(f"m_site min/max = {m_site.min():.6g} / {m_site.max():.6g}") print(f"k2_bond min/max = {k2_bond.min():.6g} / {k2_bond.max():.6g}") print(f"k4_bond min/max = {k4_bond.min():.6g} / {k4_bond.max():.6g}") print() print("=== Wave packet settings ===") print(f"A = {A}") print(f"sigma = {sigma}") print(f"k0 = {k0}") print(f"x0_left = {x0_left}") print(f"x0_right = {x0_right}") print(f"omega0 = {omega_k(k0):.8f}") print(f"vg0 = {vg_k(k0):.8f}") print() print(f"Initial energy = {E0:.12e}") print() # ============================================================ # storage for FFT and energy # ============================================================ snapshots = [] snapshot_times = [] energy_list = [] energy_times = [] animation_phase = "display" def clear_observables(): """ 概要: FFTとエネルギーのスナップショットデータをクリアします。 詳細説明: `snapshots`, `snapshot_times`, `energy_list`, `energy_times`のリストを空にします。 これは、シミュレーションのリセット時や、GIF保存前など、 新しいデータ収集サイクルを開始する際に使用されます。 戻り値: :returns: None """ snapshots.clear() snapshot_times.clear() energy_list.clear() energy_times.clear() def reset_state(): """ 概要: シミュレーションの状態を初期状態にリセットします。 詳細説明: 変位`u`と速度`v`を、`make_initial_condition`で生成された初期値に戻します。 また、アニメーションのプロットデータ(`line.set_ydata`)と タイトルテキストを初期状態に設定し、すべてのオブザーバブルデータをクリアします。 主にGIF保存前に、アニメーションの再生を初期状態から開始するために使用されます。 戻り値: :returns: None """ nonlocal u, v u = u_initial.copy() v = v_initial.copy() line.set_ydata(u) title_text.set_text("1D wave packet scattering") clear_observables() # ============================================================ # one MD step # ============================================================ def step_euler(u, v): """ 概要: オイラー法を用いて1MDステップを進めます。 詳細説明: 現在の変位`u`と速度`v`から、現在の力`f`を計算します。 その後、オイラー法(速度の更新に現在の力、変位の更新に新しい速度を使用)に よって次の時刻の変位`u_new`と速度`v_new`を更新します。 最後に、境界条件を適用します。 引数: :param u: numpy.ndarray: 現在の各サイトの変位の配列。 :param v: numpy.ndarray: 現在の各サイトの速度の配列。 戻り値: :returns: tuple[numpy.ndarray, numpy.ndarray]: 更新された変位と速度の配列。 """ f = force(u) v_new = v + dt * f / m_site u_new = u + dt * v_new u_new, v_new = apply_boundary(u_new, v_new) return u_new, v_new def step_verlet(u, v): """ 概要: ヴェルレ法を用いて1MDステップを進めます。 詳細説明: 現在の変位`u`と速度`v`から、現在の力`f`を計算します。 ヴェルレ法(速度の半分更新、変位の更新、新しい変位に基づく力の再計算、 速度の残り半分更新)によって次の時刻の変位`u_new`と速度`v_new`を更新します。 変位更新後に境界条件を適用し、新しい力を計算します。 引数: :param u: numpy.ndarray: 現在の各サイトの変位の配列。 :param v: numpy.ndarray: 現在の各サイトの速度の配列。 戻り値: :returns: tuple[numpy.ndarray, numpy.ndarray]: 更新された変位と速度の配列。 """ f = force(u) v_half = v + 0.5 * dt * f / m_site u_new = u + dt * v_half # Apply boundary before calculating new force if boundary == "fixed": u_new[0] = 0.0 u_new[-1] = 0.0 f_new = force(u_new) v_new = v_half + 0.5 * dt * f_new / m_site u_new, v_new = apply_boundary(u_new, v_new) return u_new, v_new def md_step(u, v): """ 概要: 指定された時間積分メソッドで1MDステップを進めます。 詳細説明: `method`引数の値(`"euler"`または`"verlet"`)に基づいて、 対応する時間積分関数(`step_euler`または`step_verlet`)を呼び出します。 これにより、シミュレーションの次の状態が計算されます。 引数: :param u: numpy.ndarray: 現在の各サイトの変位の配列。 :param v: numpy.ndarray: 現在の各サイトの速度の配列。 戻り値: :returns: tuple[numpy.ndarray, numpy.ndarray]: 更新された変位と速度の配列。 例外: ValueError: 未知の`method`が指定された場合。 """ if method == "euler": return step_euler(u, v) elif method == "verlet": return step_verlet(u, v) else: raise ValueError(f"Unknown method: {method}") # ============================================================ # animation update # ============================================================ def update(frame): """ 概要: MatplotlibのFuncAnimationの各フレームを更新します。 詳細説明: この関数はFuncAnimationによってフレームごとに呼び出されます。 1. `md_step`関数を呼び出してシミュレーションを1ステップ進めます。 2. `save_interval`で指定された間隔で、現在の変位`u`と時刻`t`を`snapshots`と`snapshot_times`に、 現在の全エネルギー`E`を`energy_list`と`energy_times`に記録します。 3. プロットのラインデータ(`line.set_ydata(u)`)を更新し、変位を可視化します。 4. `step_no`が特定の間隔(20ステップごと、または1ステップ目)で、 現在の全エネルギーと初期エネルギーからの相対ドリフトを計算し、タイトルテキストを更新します。 5. `step_no`が特定の間隔(50ステップごと、または最終ステップ)で、 現在のシミュレーション進捗とエネルギー情報がコンソールに出力されます。 引数: :param frame: int: 現在のアニメーションフレーム番号(0から`nstep-1`まで)。 戻り値: :returns: tuple: 更新されたmatplotlibオブジェクト(line, title_text)のタプル。 """ nonlocal u, v u, v = md_step(u, v) step_no = frame + 1 t = step_no * dt if save_interval > 0 and step_no % save_interval == 0: snapshots.append(u.copy()) snapshot_times.append(t) E = total_energy(u, v) energy_list.append(E) energy_times.append(t) line.set_ydata(u) if step_no % 20 == 0 or step_no == 1: E = total_energy(u, v) drift = (E - E0) / E0 title_text.set_text( f"1D wave packet scattering step={step_no}, t={t:.2f}, " f"energy drift={drift:.2e}" ) if step_no % 50 == 0 or step_no == nstep: E = total_energy(u, v) drift = (E - E0) / E0 print( f"[{animation_phase}] step={step_no:6d}/{nstep}, " f"t={t:.4g}, energy drift={drift:.3e}", flush=True, ) return line, title_text # ============================================================ # plot setup # ============================================================ fig, ax = plt.subplots(figsize=(10, 4)) line, = ax.plot(x_eq, u, lw=2) title_text = ax.set_title("1D wave packet scattering") ax.set_xlim(x_eq[0], x_eq[-1]) ax.set_ylim(-0.5, 0.5) ax.set_xlabel("x") ax.set_ylabel("u") ax.grid(True) # mark scatterer regions for x1, x2, label in scatterer_spans: ax.axvspan(x1, x2, alpha=0.20) ax.text( 0.5 * (x1 + x2), 0.43, label, ha="center", va="center", fontsize=9, rotation=90, ) ani = FuncAnimation( fig, update, frames=nstep, interval=interval, blit=False, ) plt.show() # ============================================================ # optional GIF save # ============================================================ if args.save: # FuncAnimation with a stateful update function replays frames during ani.save. # Reset first so the saved GIF starts from the same initial condition. animation_phase = "save" reset_state() gif_name = make_gif_filename(args) print(f"Saving GIF: {gif_name}") try: ani.save(gif_name, writer="pillow", fps=args.gif_fps, dpi=args.gif_dpi) except Exception: print("ERROR: failed to save GIF.") print("Please check that pillow is installed:") print(" pip install pillow") raise print(f"Saved GIF: {gif_name}") # ============================================================ # FFT visualization # ============================================================ if len(snapshots) > 1: snapshots_arr = np.array(snapshots) snapshot_times_arr = np.array(snapshot_times) # Remove spatial average before FFT snapshots0 = snapshots_arr - snapshots_arr.mean(axis=1, keepdims=True) Uk = np.fft.rfft(snapshots0, axis=1) P = np.abs(Uk)**2 k_axis = 2.0 * np.pi * np.fft.rfftfreq(n, d=a) plt.figure(figsize=(10, 5)) plt.imshow( P.T, aspect="auto", origin="lower", extent=[ snapshot_times_arr[0], snapshot_times_arr[-1], k_axis[0], k_axis[-1], ], ) plt.xlabel("time") plt.ylabel("k") plt.ylim([0, min(np.pi / a, k0 * 5)]) plt.title(r"Mode power $|u_k(t)|^2$") plt.colorbar(label="Power") plt.tight_layout() plt.show() # ============================================================ # energy drift visualization # ============================================================ if len(energy_list) > 1: energy_arr = np.array(energy_list) energy_times_arr = np.array(energy_times) plt.figure(figsize=(10, 4)) plt.plot(energy_times_arr, (energy_arr - E0) / E0) plt.xlabel("time") plt.ylabel("relative energy drift") plt.title("Energy conservation check") plt.grid(True) plt.tight_layout() plt.show() # ============================================================ # final report # ============================================================ E1 = total_energy(u, v) print() print("=== Final energy check ===") print(f"Initial energy = {E0:.12e}") print(f"Final energy = {E1:.12e}") print(f"Relative drift = {(E1 - E0) / E0:.12e}")
if __name__ == "__main__": main()