#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
位相配列のビームフォーミングアニメーションと関連するプロットを生成するスクリプト。

このスクリプトは、コマンドライン引数 `argparse` を使用して、
位相配列アンテナのビームフォーミングシミュレーションの様々なパラメータ（波源数、波長、操向角など）を制御できます。
波の伝播のアニメーションと、各波源の位相オフセットのプロットを表示します。

:doc:`aesa_usage`

Examples:
    python aesa.py
    python aesa.py --steer-angle 45 --n-sources 32
    python aesa.py --wavelength 1.0 --spacing-multiple 0.5 --frames 80
"""

import argparse
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.animation import FuncAnimation


def parse_args():
    """
    コマンドライン引数を解析する。

    位相配列ビームフォーミングアニメーションとソース位相オフセットプロットのための
    設定オプションを定義し、コマンドラインからそれらを読み込む。

    :returns: argparse.Namespace: 解析されたコマンドライン引数を含むオブジェクト。
    """
    parser = argparse.ArgumentParser(
        description="Animate phased-array beamforming and plot source phase offsets."
    )
    parser.add_argument("--n-sources", type=int, default=16,
                        help="Number of wave sources. Default: 16")
    parser.add_argument("--wavelength", type=float, default=1.0,
                        help="Wavelength. Default: 1.0")
    parser.add_argument("--spacing", type=float, default=None,
                        help="Source spacing. If omitted, wavelength * spacing_multiple is used.")
    parser.add_argument("--spacing-multiple", type=float, default=0.5,
                        help="Source spacing in units of wavelength. Used only when --spacing is omitted. Default: 0.5")
    parser.add_argument("--steer-angle", type=float, default=30.0,
                        help="Beam steering angle in degrees. Default: 30")
    parser.add_argument("--speed", type=float, default=1.0,
                        help="Wave speed in simulation units. Default: 1.0")
    parser.add_argument("--xmin", type=float, default=-10.0,
                        help="Minimum x value of calculation region. Default: -10")
    parser.add_argument("--xmax", type=float, default=10.0,
                        help="Maximum x value of calculation region. Default: 10")
    parser.add_argument("--nx", type=int, default=300,
                        help="Number of x mesh points. Default: 300")
    parser.add_argument("--ymin", type=float, default=0.0,
                        help="Minimum y value of calculation region. Default: 0")
    parser.add_argument("--ymax", type=float, default=15.0,
                        help="Maximum y value of calculation region. Default: 15")
    parser.add_argument("--ny", type=int, default=300,
                        help="Number of y mesh points. Default: 300")
    parser.add_argument("--frames", type=int, default=50,
                        help="Number of animation frames. Default: 50")
    parser.add_argument("--interval", type=int, default=50,
                        help="Animation interval in milliseconds. Default: 50")
    parser.add_argument("--dt", type=float, default=0.1,
                        help="Time step per frame. Default: 0.1")
    parser.add_argument("--levels", type=int, default=50,
                        help="Number of contour levels. Default: 50")
    parser.add_argument("--vmin", type=float, default=-1.5,
                        help="Minimum color scale value. Default: -1.5")
    parser.add_argument("--vmax", type=float, default=1.5,
                        help="Maximum color scale value. Default: 1.5")
    parser.add_argument("--fig-width", type=float, default=8.0,
                        help="Figure width. Default: 8")
    parser.add_argument("--fig-height", type=float, default=6.8,
                        help="Figure height. Default: 6.8")
    return parser.parse_args()


def wrap_phase(phi):
    """
    位相を `[-π, π)` の範囲にラップする。

    位相値がこの範囲を超える場合に、周期性に基づいて等価な値に変換する。
    例えば、`3π` は `π` に、`-3π/2` は `π/2` にラップされる。

    :param phi: float or numpy.ndarray: ラップする位相値（ラジアン）。
    :returns: float or numpy.ndarray: `[-π, π)` の範囲にラップされた位相値。
    """
    return (phi + np.pi) % (2.0 * np.pi) - np.pi


def main():
    """
    位相配列ビームフォーミングアニメーションのメイン処理を実行する。

    コマンドライン引数に基づいて波源、計算領域を設定し、
    波の伝播とビームフォーミング効果を視覚化するアニメーションと
    位相オフセットのプロットを生成する。
    """
    args = parse_args()

    n_sources = args.n_sources
    wavelength = args.wavelength
    k = 2.0 * np.pi / wavelength
    spacing = args.spacing if args.spacing is not None else wavelength * args.spacing_multiple
    steer_angle = args.steer_angle
    speed_of_light = args.speed

    x_range = np.linspace(args.xmin, args.xmax, args.nx)
    y_range = np.linspace(args.ymin, args.ymax, args.ny)
    X, Y = np.meshgrid(x_range, y_range)

    source_xs = np.linspace(
        -(n_sources - 1) * spacing / 2.0,
        +(n_sources - 1) * spacing / 2.0,
        n_sources,
    )

    theta_rad = np.radians(steer_angle)
    phases = -k * source_xs * np.sin(theta_rad)
    phases_wrapped = wrap_phase(phases)

    def get_initial_phase(i):
        """
        ビームステアリングのための初期位相オフセットを計算する。

        指定された波源インデックス `i` に対応する、計算済みの位相オフセット `phases[i]` を返す。
        これはビームを特定の角度に操向するために各波源に与えられる。

        :param i: int: 波源のインデックス。
        :returns: float: 指定された波源の初期位相オフセット（ラジアン）。
        """
        return phases[i]

    fig = plt.figure(figsize=(args.fig_width, args.fig_height))
    gs = fig.add_gridspec(2, 1, height_ratios=[9, 1], hspace=0.28)
    ax = fig.add_subplot(gs[0, 0])
    ax_phase = fig.add_subplot(gs[1, 0])

    def calculate_field(frame):
        """
        指定されたアニメーションフレームにおける実数波場を計算する。

        各波源からの球面波を合成し、指定された時間のスナップショットにおける総波場を計算する。
        各波源にはビームステアリングのための位相オフセットが適用される。
        振幅は距離の平方根に反比例して減衰する。

        :param frame: int: 現在のアニメーションフレーム番号。
        :returns: numpy.ndarray: 計算された実数波場。
        """
        t = frame * args.dt
        omega = k * speed_of_light

        total_field = np.zeros_like(X, dtype=complex)
        for i, sx in enumerate(source_xs):
            r = np.sqrt((X - sx) ** 2 + Y ** 2)
            phi_0 = get_initial_phase(i)
            total_field += np.exp(1j * (omega * t - k * r + phi_0)) / np.sqrt(r + 0.5)
        return np.real(total_field)

    initial_field = calculate_field(0)
    ax.contourf(
        X, Y, initial_field,
        levels=args.levels,
        cmap="RdBu_r",
        vmin=args.vmin,
        vmax=args.vmax,
    )
    ax.scatter(source_xs, np.zeros_like(source_xs), color="black", s=5, zorder=10)
    ax.set_title(f"Phased Array Beamforming (Angle: {steer_angle:g}°)")
    ax.set_aspect("equal")
    ax.set_xlabel("x")
    ax.set_ylabel("y")

    ax_phase.plot(np.arange(n_sources), phases, marker="o", linewidth=0.8, label="phase")
    ax_phase.plot(np.arange(n_sources), phases_wrapped, marker="s", linewidth=0.8,
                  label="wrapped phase [-π,π)")
    ax_phase.axhline(0.0, color="black", linewidth=0.5)
    ax_phase.set_xlim(-0.5, n_sources - 0.5)
    ax_phase.set_ylabel("phase [rad]")
    ax_phase.set_xlabel("source index")
    ax_phase.tick_params(labelsize=8)
    ax_phase.legend(fontsize=8, loc="upper right")
    ax_phase.grid(True, linewidth=0.3)

    def update(frame):
        """
        アニメーションの各フレームで波の状態を更新する。

        `calculate_field` 関数を呼び出して現在のフレームの波場を計算し、
        それを `matplotlib` の `contourf` でプロットを更新する。
        波源の位置も再描画される。

        :param frame: int: 現在のアニメーションフレーム番号。
        :returns: tuple: 更新された `matplotlib.contourf` オブジェクトを含むタプル。
        """
        ax.clear()
        field_real = calculate_field(frame)
        im = ax.contourf(
            X, Y, field_real,
            levels=args.levels,
            cmap="RdBu_r",
            vmin=args.vmin,
            vmax=args.vmax,
        )
        ax.scatter(source_xs, np.zeros_like(source_xs), color="black", s=5)
        ax.set_title(f"Phased Array Beamforming (Angle: {steer_angle:g}°)")
        ax.set_aspect("equal")
        ax.set_xlabel("x")
        ax.set_ylabel("y")
        return im,

    ani = FuncAnimation(fig, update, frames=args.frames, interval=args.interval, blit=False)
    plt.pause(0.01)


if __name__ == "__main__":
    main()
    input("\nPress ENTER to terminate>>")
