物理の駅 Physics station by 現役研究者

テクノロジーは共有されてこそ栄える

Python mzfit (zfit) で マルチガウシアンフィッティング (Windows非対応)

サイエンスの界隈でよく使われているzfitと、zfitを簡単に使うためのラッパーであるmzfitを使ってみる。

pypi.org

pypi.org

zfitが依存しているfittingのコアの部分である ipopt はWindowsに非対応なので、WindowsユーザーはWSLなどを使ってLinux環境で実行すること。

zfitとmzfitをインストールする。一部のライブラリはバージョン指定があるので、指定されたバージョンをインストールする。以下のコマンドはインストール例。

pip3 install zfit
pip3 install wrapt==1.12.1
pip3 install typing-extensions==3.7.4
pip3 install numpy==1.19.2
pip3 install --upgrade protobuf
pip3 install mplhep
pip3 install mzfit

できたら、Pythonを起動する。WSL+Jupter Labユーザーは jupyter lab --no-browser で表示される http://127.0.0.1:8890/*** をブラウザに貼り付ける。

zfitのインストールの確認のためimportする。

import zfit

ALPHA versionだよと警告が出るが、エラーが出なければ問題ない。zfitのインストールに成功していれば、mzfitもimportできるはずである。

import mzfit

mzfitの使い方は作者のGithubを見ればよいが、メモ代わりに書いておく。

github.com

まずは単純な正規分布で乱数を作って、ビン数と上限下限とともに、mzfit.zfに与える。正規分布の関数を作り、モデル関数に入れる。関数自体に初期値を与えておく必要があるので、関数を使い回すのはやや面倒かもしれない。その後drawするとデータのエラー付き分布(誤差は√N)と、初期値を使った関数の曲線の絵が得られる。

import numpy as np
from zfit import z
import mzfit

np.random.seed(seed=0)
signal1 = np.random.normal(0, 1, 10000)

zf0 = mzfit.zf(signal1,bins=100,lower=-5,upper=5)

def user_func0(x, mu=1, sigma=1):
    return z.exp(-z.square((x - mu) / sigma)/2)

zf0.set_model_func(user_func0)
zf0.draw()

今回、あえて初期値を真値からずらしたので、曲線は分布からずれている。CERN ROOTのgausの場合はConstantが得られるが、zfitではそこは勝手に調整しているようである。次にフィッティングしてから描画する。

zf0.fit()
zf0.draw()

うまくいった。エラーの計算をしてパラメータを表示させる。

zf0.result.error(method='minuit_minos')
print(zf0.result.params)

#name       value         minuit_minos    at limit
#------  --------  -------------------  ----------
#mu      -0.01846  - 0.0098   + 0.0099       False
#sigma     0.9876  -  0.007   +  0.007       False

パラメータの値を取得できるようにする。(もっといい方法がある気がするが...)

def get_parameter(result, name):
    if "minuit_minos" not in result.params[name]:
        result.error(method='minuit_minos')
    return result.params[name]["value"], (result.params[name]["minuit_minos"]["upper"]-result.params[name]["minuit_minos"]["lower"])/2

print("mu", get_parameter(zf0.result, "mu"))
print("sigma", get_parameter(zf0.result, "sigma"))

#mu (-0.01846328491970265, 0.009876115394689126)
#sigma (0.9875869544501649, 0.006983766070591127)

せっかくなので、もう少し高等なfittingをしてみよう。3つのガウシアンピークをもつ分布作り、マルチガウシアンの関数を定義する。

import numpy as np
from zfit import z
import mzfit

np.random.seed(seed=0)
signal1 = np.random.normal(5, 1, 2000)
signal2 = np.random.normal(10, 2, 10000)
signal3 = np.random.normal(15, 0.5, 5000)
data = np.concatenate([signal1, signal2, signal3])
zf3 = mzfit.zf(data)

def user_func3(x, mu1=3, sigma1=1, mu2=10, sigma2=1, c2=1, mu3=16, sigma3=1, c3=1):
    y=0
    y+=z.exp(-z.square((x - mu1) / sigma1)/2)/sigma1
    y+=z.exp(-z.square((x - mu2) / sigma2)/2)/sigma2*c2
    y+=z.exp(-z.square((x - mu3) / sigma3)/2)/sigma3*c3
    return y

zf3.set_model_func(user_func3)
zf3.draw()

フィッティングして描画

zf3.fit()
zf3.draw()

パラメータを取得する。get_parameterは先程と同じ。

zf3.result.error(method='minuit_minos')
print(zf3.result.params)

出力

name      value         minuit_minos    at limit
------  -------  -------------------  ----------
mu1       4.949  -   0.04   +  0.042       False
sigma1   0.9579  -  0.028   +   0.03       False
mu2       9.947  -  0.029   +  0.029       False
sigma2     2.01  -  0.031   +  0.032       False
c2        5.192  -   0.23   +   0.24       False
mu3       15.01  - 0.0078   + 0.0077       False
sigma3   0.4907  - 0.0063   + 0.0064       False
c3        2.575  -  0.099   +    0.1       False

分布を作ったときの真値とフィッティングで得られた値を比較する。

パラメータ 真値 得られた値
mu1 5 4.95±0.04
sigma1 1 0.96±0.03
mu2 10 9.95±0.03
sigma2 2 2.01±0.03
c2 5 5.0±0.2
mu3 15 15.014±0.008
sigma3 0.5 0.491±0.006
c3 2.5 2.47±0.09

誤差を含め正しく評価されているように見える。

標準出力を抑止して平均値とsigmaだけ計算する関数

import mzfit
import zfit

def get_parameter(result, name):
    if "minuit_minos" not in result.params[name]:
        result.error(method='minuit_minos')
    return result.params[name]["value"], (result.params[name]["minuit_minos"]["upper"]-result.params[name]["minuit_minos"]["lower"])/2

def get_mean_stdev(data):
    import os
    from contextlib import redirect_stdout
    with redirect_stdout(open(os.devnull, 'w')):
        zf = mzfit.zf(data)
        zf.set_model('gauss') 
        zf.fit()
    return get_parameter(zf.result,"mu")[0],get_parameter(zf.result,"sigma")[0]

元の関数の強度(amplitude)を得るときは、fittingを行うときのビン数、最小値(lower)、最大値(upper)を決めて、次のように計算させれば良い。フィッティングの範囲で面積を規格化すること忘れないように。

import mzfit
import zfit

def get_parameter(result, name):
    if "minuit_minos" not in result.params[name]:
        result.error(method='minuit_minos')
    return result.params[name]["value"], (result.params[name]["minuit_minos"]["upper"]-result.params[name]["minuit_minos"]["lower"])/2

def user_func0(x, mu=1, sigma=1):
    from zfit import z
    return z.exp(-z.square((x - mu) / sigma)/2)

def get_mean_stdev_amplitude(data,bins,lower,upper):
    import os
    from contextlib import redirect_stdout
    with redirect_stdout(open(os.devnull, 'w')):
        zf = mzfit.zf(data,bins=bins,lower=lower,upper=upper)
        zf.set_model_func(user_func0)
        zf.fit()
        n_sample = len([d for d in data if lower < d < upper])
        amplitude = n_sample/zf.fit_bins*float(zf.obs.area())
    return get_parameter(zf.result,"mu")[0],get_parameter(zf.result,"sigma")[0],amplitude

def user_func1(x, lower, upper, mu, sigma, amplitude):
    from scipy.stats import norm
    area =(norm.cdf(upper, loc=mu, scale=sigma)-norm.cdf(lower, loc=mu, scale=sigma))*np.sqrt(2*np.pi*sigma**2)
    y = np.exp(-np.square((x - mu) / sigma)/2)/area*amplitude
    return y

# 以下、テスト用
for sigma in [2.0,1.0,0.2]:
    import numpy as np
    np.random.seed(seed=0)
    signal1 = np.random.normal(5, sigma, 20000)

    lower=4
    upper=6
    binw=0.1
    bins=int(np.round((upper-lower)/binw))
    mu, stdev, amplitude = get_mean_stdev_amplitude(signal1,bins,lower,upper)

    import matplotlib.pyplot as plt
    plt.hist(signal1,range=[0,10],bins=int(10/binw))
    x = np.linspace(0,10,100)
    y = user_func1(x,lower,upper,mu,stdev,amplitude)
    plt.plot(x,y)
    plt.show()

Woods-Saxon potentialをPythonで描画する

import numpy as np
from matplotlib import pyplot as plt
v0 = 14
A = 15
a = 0.5
r0 = 1.25
R = r0*A**(1/3)
x = np.linspace(0.0, 10.0)
y = -v0/(1+np.exp((x-R)/a))
plt.plot(x, y, "b-")
plt.title(r"Woods-Saxon potential")
plt.text(3, -v0*0.8, 
         r"$V(r)=\frac{-V_0}{1+{\rm exp}(r-R)/a}$"+
         "\n"+
         r"Parameters: A$={}$, $V_0={}$ MeV".format(A,v0)+
         "\n"+
         r"$a={}$ fm, $r_0={}$ fm, $R={:.1f} $fm".format(a,r0,R),
        fontsize=12)
plt.xlabel(r"$r$ [fm]")
plt.ylabel(r"$V(r)$ [MeV]")
plt.show()

f:id:onsanai:20210205111754p:plain

オリジナルコード

http://nucleartalent.github.io/Course2ManyBodyMethods/doc/pub/intro/pdf/intro-print.pdf

C++ Eigenを使った正規分布のフィッティング手法

過去に、ROOTとPythonで正規分布のフィッティングをした。

phst.hateblo.jp

だが、ROOTはWindowsでx64と共存しないので、x64と共存するライブラリだけを使ったフィッティングを試みた。まずは、

ガウス分布のフィッティング用のサンプルデータ Sample data

を、gaussian_sample.txt としてダウンロードしておく。

正規分布へのフィッティングは Eigenライブラリの NonLinearOptimization を使った。 パラメータの誤差を求めるときは、ほぼコピペの二分法を使った。通常、パラメータの誤差はカイ二乗が1.0増えるときであるが、 ConstantSigma は 1.5増える時がROOTのパラメータと近かったので、そちらを採用した。なぜ1.0ではなく1.5なのか知ってる人はコメントください。

ROOTのパラメータ

Constant 402.531 +/- 4.95816
Mean     -0.00397889 +/- 0.00989405
Sigma    0.983115 +/- 0.00704207
Chi2     83.6418

下記のコードで求めたパラメータ

Constant:402.531 +/- 4.9502
Mean    :-0.00397891 +/- 0.0102539
Sigma   :0.983115 +/- 0.00708008
Chi2    :83.6418

ソースコード

#include <Eigen/Dense>
#include <unsupported/Eigen/NonLinearOptimization>
#include <unsupported/Eigen/NumericalDiff>

#include <iostream>
#include <sstream>
#include <fstream>

namespace {
    // Generic functor
    template<typename _Scalar, int NX = Eigen::Dynamic, int NY = Eigen::Dynamic>
    struct Functor
    {
        typedef _Scalar Scalar;
        enum {
            InputsAtCompileTime = NX,
            ValuesAtCompileTime = NY
        };
        typedef Eigen::Matrix<Scalar, InputsAtCompileTime, 1> InputType;
        typedef Eigen::Matrix<Scalar, ValuesAtCompileTime, 1> ValueType;
        typedef Eigen::Matrix<Scalar, ValuesAtCompileTime, InputsAtCompileTime> JacobianType;
    };

    double gaussign_function(const Eigen::VectorXd& b, const double& x) {
        return b[0] * exp(-(x - b[1]) * (x - b[1]) / (2 * b[2] * b[2]));
    }

    double gaussign_function(const double& constant, const double& mean, const double& sigma, const double& x) {
        return constant * exp(-(x - mean) * (x - mean) / (2 * sigma * sigma));
    }

    struct gaussian_functor2 : Functor<double>
    {
        gaussian_functor2(int inputs, size_t values, double* x, double* y)
            : inputs_(inputs), values_(values), x(x), y(y) {}

        double* x;
        double* y;
        int operator()(const Eigen::VectorXd& b, Eigen::VectorXd& fvec) const
        {
            for (int i = 0; i < values_; ++i) {
                if (y[i] == 0) { fvec[i] = 0; }
                else {
                    double dy = std::abs(gaussign_function(b, x[i]) - y[i]);
                    fvec[i] = dy / std::sqrt(y[i]);
                }
            }
            return 0;
        }
        const int inputs_;
        const int values_;
        int inputs() const { return inputs_; }
        int values() const { return values_; }
    };

    double chi2_gaussign_function(const std::vector<double>& x, const std::vector<double>& y, const Eigen::VectorXd& q,
        double d0 = 0, double d1 = 0, double d2 = 0) {

        double chi2 = 0;
        for (int i = 0; i < x.size(); i++) {
            if (y[i] == 0)continue;
            double y_exp = gaussign_function(q[0] + d0, q[1] + d1, q[2] + d2, x[i]);
            chi2 += (y[i] - y_exp) * (y[i] - y_exp) / y[i];
        }
        return chi2;
    }

    /// <summary>
    /// 2分法
    /// </summary>
    /// <param name="func"></param>
    /// <param name="xr"></param>
    /// <param name="max_iter"></param>
    /// <param name="x_found"></param>
    /// <param name="error"></param>
    void bisection(std::function<double(const double&)> func, double xr, const int& max_iter, double& x_found, double& error)
    {
        double xl = 0;

        double f = 0;
        double fmid = 0;

        for (int k = 0; k < max_iter; k++) {
            f = func(xl);
            fmid = func(xr);
            if (f * fmid < 0.0) break;
            xr *= 2;
        }

        if (f * fmid >= 0.0) { throw std::exception("bisection q1_error"); }

        double dx = fabs(xr - xl), xmid;
        for (int k = 0; k < max_iter; ++k) {
            xmid = 0.5 * (xl + xr);    // 中点
            dx *= 0.5;

            // 中点での関数値を求める
            fmid = func(xmid);

            // 収束判定
            if (dx < error || fmid == 0.0) {
                x_found = xmid;
                error = dx;
                return;
            }

            // 新しい区間
            if (f * fmid < 0) {
                xr = xmid;
            }
            else {
                xl = xmid;
                f = fmid;
            }
        }
        throw std::exception("bisection q2_error");
    }
    Eigen::VectorXd error_gaussign_function(const std::vector<double>& x, const std::vector<double>& y, const Eigen::VectorXd& q,
        double chi2) {

        auto func0 = [&x, &y, &q, &chi2](const double& diff) {
            return chi2_gaussign_function(x, y, q, diff, 0, 0) + chi2_gaussign_function(x, y, q, -diff, 0, 0) - chi2 * 2 - 1.5*2; };
        auto func1 = [&x, &y, &q, &chi2](const double& diff) {
            return chi2_gaussign_function(x, y, q, 0, diff, 0) + chi2_gaussign_function(x, y, q, 0, -diff, 0) - chi2 * 2 - 1.0*2; };
        auto func2 = [&x, &y, &q, &chi2](const double& diff) {
            return chi2_gaussign_function(x, y, q, 0, 0, diff) + chi2_gaussign_function(x, y, q, 0, 0, -diff) - chi2 * 2 - 1.5*2; };

        double q0_error, q1_error, q2_error;

        double bisection_error = 0.001;

        bisection(func0, 1, 100, q0_error, bisection_error);
        bisection(func1, 1, 100, q1_error, bisection_error);
        bisection(func2, 1, 100, q2_error, bisection_error);

        Eigen::VectorXd q_error(3);
        q_error << q0_error, q1_error, q2_error;
        return q_error;
    }
}

void main() {

    std::ifstream ifs("gaussian_sample.txt");
    const int n = 3;
    Eigen::VectorXd q(n);
    q << 400, 0, 1;

    std::vector<double> xbuf, ybuf;

    double x, y;
    while (ifs >> x >> y) {
        xbuf.emplace_back(x);
        ybuf.emplace_back(y);
    }

    gaussian_functor2 functor(n, xbuf.size(), xbuf.data(), ybuf.data());

    Eigen::NumericalDiff<gaussian_functor2> numDiff(functor);
    Eigen::LevenbergMarquardt<Eigen::NumericalDiff<gaussian_functor2> > lm(numDiff);

    int info = lm.minimize(q);

    double chi2 = chi2_gaussign_function(xbuf, ybuf, q);
    Eigen::VectorXd error = error_gaussign_function(xbuf, ybuf, q, chi2);
    std::cout << "Constant:" << q[0] << " +/- " << error[0] << std::endl;
    std::cout << "Mean    :" << q[1] << " +/- " << error[1] << std::endl;
    std::cout << "Sigma   :" << q[2] << " +/- " << error[2] << std::endl;
    std::cout << "Chi2    :" << chi2 << std::endl;
}

参考文献

非線形方程式の根 - PukiWiki for PBCG Lab

単スリットのフラウンホーファー回折のシミュレーション

http://www.sci.keio.ac.jp/gp/87B7D75A/A6070F75/6E345155.pdf

単スリットについて考えてみた。 スリット幅が波長に比べて十分広いか、スクリーンの位置が十分遠い場合には、回折による干渉模様を観察できる。

数式をPDFファイルからパクって書いておく。

\displaystyle
\begin{eqnarray}
\frac{xd'}{l\lambda}
 =
  \begin{cases}
    0, \pm\frac{3}{2}, \pm\frac{5}{2}, \pm\frac{7}{2},\cdot\cdot\cdot のとき明線 \\
\\
    \pm 1, \pm 2, \pm 3 \cdot\cdot\cdot  のとき暗線
  \end{cases}
\end{eqnarray}


  • x: スクリーン中央を0としたときの各干渉模様の位置
  • d: スリット間隔 今回は0.01 mm、0.1 mm、1 mmを試してみる
  • l: スリットからスクリーンまでの距離 干渉模様の位置を同じにするため、10 mm、100 mm、1000 mmを試してみる
  • λ: レーザー光の波長 そんなのないけど500 nmの波長とする

シミュレーションではスクリーンの空間で2ミクロンごと(画像の1ピクセルが2ミクロン相当、縦横2048ピクセル)に計算させた。最小のスリット間隔 10ミクロンの1/5なので十分だと思う(たぶん)。

f:id:onsanai:20200125031334j:plainf:id:onsanai:20200125031337j:plainf:id:onsanai:20200125031351j:plain
左から順にスリット間隔0.01mm、0.1mm、1mm

フラウンホーファー回折が成り立つ条件は、ざっくり

\displaystyle
\frac{d^2}{l\lambda}\ll1

らしい。各条件での左辺の値は0.02、0.2、2、であり、確かに一番右の画像ではきれいな回折像が確認できなくなっている。同じスリット幅でも、スリットとスクリーンまでの距離を10倍に離すと、以下の像が得られる。

f:id:onsanai:20200125033639j:plain:w220

ただし、横方向の干渉模様の位置を同じにするため、画像の1ピクセルを20ミクロン相当にした。そのため、縦方向にも縮んでしまっているが、横方向の回折像はくっきり確認できる。