commit 738fdc38a6d3f9e0d492849a0a3825f229c01f29 Author: Takahiro OHKUBO Date: Fri Sep 20 17:16:22 2024 +0900 first commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..825acb2 --- /dev/null +++ b/.gitignore @@ -0,0 +1,15 @@ +# white listで管理するため、一旦、全てを無視 +* +!*/ + +# white listの定義 +!.git/* +!.gitignore +!Readme.html +!Readme.org +!xls2ternary.py +!test.xlsx +!a.png +!b.png + + diff --git a/Readme.html b/Readme.html new file mode 100644 index 0000000..5fde809 --- /dev/null +++ b/Readme.html @@ -0,0 +1,475 @@ + + + + + + + +xls2ternary.py + + + + + + + + + + + +
+

xls2ternary.py

+ + + +
+

更新履歴

+
+
    +
  • pandasを使うようにした。<2024-09-20 金>
  • +
+
+
+ +
+

はじめに

+
+

+Excelファイルから三成分系の相図をプロットするプログラムです。3成分系でガラス化範囲をプロットするために作成しました。いくつか3元プロットのライブラリが公開(python-ternary Veusz )されているようですが、どれもイマイチ融通が効かないため、結局自作しました。 +

+ +

+3成分の組成とガラス化の有無(0から1で判定)のデータから、相図を作成しコンタープロットを描きます。excelでファイルを作成し、引数でプログラムに渡します。3組成のそれぞれの成分名は、ファイル中のヘッダで指定します。入力するexcelファイルは、(test.xlsx)を参考にしてください。3行目がヘッダデータ、3組成の成分比とガラス化の有無G/C(0から1の範囲)を列としたデータを作成します。 +

+ +

+成分名については、latex形式で上付き下付きも指定できます。以下はB2O3の例です。 +

+
+${\rm B_{2}O_{3}}$
+
+ +

+※G/Cの列が空欄の場合、データはスキップされプロットされません。 +

+
+
+ +
+

プログラムのインストール

+
+

+プログラムはpython3.xで作成しているのでpython3.xを実行できる環境が必要です。matplotlibは2.0.2です。プログラム本体はxls2ternary.pyです。 +python3.xをminicondaを使ってインストールしてください。numpy, matplotlib, pandasに加えてExcelファイルを読み込むためのopenpyxlが必要です。minicondaのインストールが完了したら以下のコマンドで必要なライブラリをインストールしてください。 +

+
+conda install numpy matplotlib pandas openpyxl
+
+

+後は、パスの通ったところに xls2ternary.py を配置すればインストールは完了です。 +

+
+
+ +
+

プログラムの実行

+
+

+引数でexcelファイルを指定して実行します。テストデータ(test.xlsx)を試しにプロットしてみます。 +

+
+xls2ternary.py test.xlsx
+
+

+次のような内容がターミナルに表示され、mol%で表示された図が得られれば正しくプログラムは動いています。 +

+
+Number of data :  59
+Plot header [row]:  ['A' 'B' 'C'] [1, 2, 3]
+G/C data [row]: G/C [4]
+
+ + +
+

figure +

+

図1: 出力される図

+
+ + + +

+1列目のデータ範囲の最小値を指定できます。1列目のデータを70-100%にする場合です。また、コンタープロットの補完をlinearにしてラベルをwt%にしてみます。 +

+
+xls2ternary.py -xmin 70 --interp linear -xl wt% test.xlsx
+
+ + +
+

figure +

+

図2: 1列目のデータを70から100%に拡大した図

+
+ +

+図をファイルとして保存したい場合は、画面のメニューから選んでも良いですが、-oで保存もできます。またexcelファイル中のヘッダーの行番号やデータの列番号をオプションで指定できます。 +-hで指定できるオプション一覧が表示されます。以下、オプションについてもう少し説明します。 +

+ + + + +++ ++ ++ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
オプション内容default
-hヘルプを表します。 
-sixlsファイル中のシート番号(0からはじまる)を指定します。0
-hlxlsファイル中のヘッダが入っている行番号を指定します。3
-m組成データを含む3つの列番号を指定します。1 2 3
-pガラス率(0から1の範囲)のデータを含む列を指定します。4
-xmin1列目のデータをxminから100の範囲に変更します。0
-o図を保存するファイル名前指定します。拡張子(pdfやpng等)で保存形式を自動判別します。None
-xl図の下のラベルを指定します。mol%
-gコンタープロットのために内挿するグリッドの数です。400
-fs保存する図の大きさです。8
-ls頂点のラベルのフォントの大きさです。16
-ts目盛の数字のフォントの大きさです。12
–labeloffset頂点のラベルの位置のオフセット位置です。2
–tickoffset目盛の数字の位置のオフセット位置です。1
–colormapコンタープロットのカラーマップです。ここ から好きなものを選べます。bwr
–interpコンタープロットする時の補完の方法です。[linear, cubic, nearest]から選択linear
–nolegend凡例を表示しません。 
–nocontourコンタープロットを表示しません。 
–nocolorbarカラーバーを表示しません。 
+
+
+ + +
+

座標ピックアップ

+
+

+表示された図中でマウスのボタン(左、中、右どれでも)をクリックすれば、3 +成分の割合が以下のようにターミナルに表示されます。 +

+
+button=1 ['B' 'C' 'A']=(24.52 51.03 24.45)
+button=1 ['B' 'C' 'A']=(24.49 44.60 30.91)
+button=1 ['B' 'C' 'A']=(45.65 26.83 27.53)
+
+
+
+
+
+

著者: 大窪 貴洋 (千葉大院工) ohkubo.takahiro@faculty.chiba-u.jp

+

Created: 2024-09-20 金 17:05

+

Validate

+
+ + diff --git a/Readme.org b/Readme.org new file mode 100644 index 0000000..4a2fa1d --- /dev/null +++ b/Readme.org @@ -0,0 +1,100 @@ +#+TITLE: xls2ternary.py +#+AUTHOR: 大窪 貴洋 (千葉大院工) ohkubo.takahiro@faculty.chiba-u.jp +#+LANGUAGE: ja +#+EMAIL: ohkubo.takahiro@faculty.chiba-u.jp +#+OPTIONS: toc:2 num:nil author:t creator:nil LaTeX:nil \n:nil ^:{} +#+SETUPFILE: https://amorphous.tf.chiba-u.jp/org-html-themes/org/theme-readtheorg.setup +#+MACRO: color @@html:$2@@ +#+MACRO: kakomi @@html:
$1
@@ + + +* 更新履歴 +- pandasを使うようにした。<2024-09-20 金> + +* はじめに +Excelファイルから三成分系の相図をプロットするプログラムです。3成分系でガラス化範囲をプロットするために作成しました。いくつか3元プロットのライブラリが公開([[https://github.com/marcharper/python-ternary][python-ternary]] や[[http://home.gna.org/veusz/examples/#ternary][ Veusz]] )されているようですが、どれもイマイチ融通が効かないため、結局自作しました。 + +3成分の組成とガラス化の有無(0から1で判定)のデータから、相図を作成しコンタープロットを描きます。excelでファイルを作成し、引数でプログラムに渡します。3組成のそれぞれの成分名は、ファイル中のヘッダで指定します。入力するexcelファイルは、([[file:test.xlsx][test.xlsx]])を参考にしてください。3行目がヘッダデータ、3組成の成分比とガラス化の有無G/C(0から1の範囲)を列としたデータを作成します。 + +成分名については、latex形式で上付き下付きも指定できます。以下はB_{2}O_{3}の例です。 +#+BEGIN_EXAMPLE +${\rm B_{2}O_{3}}$ +#+END_EXAMPLE + +※G/Cの列が空欄の場合、データはスキップされプロットされません。 + +* プログラムのインストール +プログラムはpython3.xで作成しているのでpython3.xを実行できる環境が必要です。matplotlibは2.0.2です。プログラム本体は[[file:xls2ternary.py][xls2ternary.py]]です。 +python3.xを[[https://docs.anaconda.com/miniconda/][miniconda]]を使ってインストールしてください。numpy, matplotlib, pandasに加えてExcelファイルを読み込むためのopenpyxlが必要です。minicondaのインストールが完了したら以下のコマンドで必要なライブラリをインストールしてください。 +#+BEGIN_EXAMPLE +conda install numpy matplotlib pandas openpyxl +#+END_EXAMPLE +後は、パスの通ったところに [[file:xls2ternary.py][xls2ternary.py]] を配置すればインストールは完了です。 + +* プログラムの実行 +引数でexcelファイルを指定して実行します。テストデータ([[file:test.xlsx][test.xlsx]])を試しにプロットしてみます。 +#+BEGIN_EXAMPLE +xls2ternary.py test.xlsx +#+END_EXAMPLE +次のような内容がターミナルに表示され、mol%で表示された図が得られれば正しくプログラムは動いています。 +#+BEGIN_EXAMPLE +Number of data : 59 +Plot header [row]: ['A' 'B' 'C'] [1, 2, 3] +G/C data [row]: G/C [4] +#+END_EXAMPLE + +#+CAPTION: 出力される図 +#+ATTR_HTML: :alt figure :title titiel mame :align center +#+ATTR_HTML: :width 440px +[[file:a.png][file:a.png]] + + + +1列目のデータ範囲の最小値を指定できます。1列目のデータを70-100%にする場合です。また、コンタープロットの補完をlinearにしてラベルをwt%にしてみます。 +#+BEGIN_EXAMPLE +xls2ternary.py -xmin 70 --interp linear -xl wt% test.xlsx +#+END_EXAMPLE + +#+CAPTION: 1列目のデータを70から100%に拡大した図 +#+ATTR_HTML: :alt figure :title titiel mame :align center +#+ATTR_HTML: :width 450px +[[file:b.png][file:b.png]] + +図をファイルとして保存したい場合は、画面のメニューから選んでも良いですが、-oで保存もできます。またexcelファイル中のヘッダーの行番号やデータの列番号をオプションで指定できます。 +-hで指定できるオプション一覧が表示されます。以下、オプションについてもう少し説明します。 + +|---------------+------------------------------------------------------------------------------------+---------| +| オプション | 内容 | default | +|---------------+------------------------------------------------------------------------------------+---------| +| -h | ヘルプを表します。 | | +|---------------+------------------------------------------------------------------------------------+---------| +| -si | xlsファイル中のシート番号(0からはじまる)を指定します。 | 0 | +| -hl | xlsファイル中のヘッダが入っている行番号を指定します。 | 3 | +| -m | 組成データを含む3つの列番号を指定します。 | 1 2 3 | +| -p | ガラス率(0から1の範囲)のデータを含む列を指定します。 | 4 | +| -xmin | 1列目のデータをxminから100の範囲に変更します。 | 0 | +|---------------+------------------------------------------------------------------------------------+---------| +| -o | 図を保存するファイル名前指定します。拡張子(pdfやpng等)で保存形式を自動判別します。 | None | +| -xl | 図の下のラベルを指定します。 | mol% | +| -g | コンタープロットのために内挿するグリッドの数です。 | 400 | +| -fs | 保存する図の大きさです。 | 8 | +| -ls | 頂点のラベルのフォントの大きさです。 | 16 | +| -ts | 目盛の数字のフォントの大きさです。 | 12 | +| --labeloffset | 頂点のラベルの位置のオフセット位置です。 | 2 | +| --tickoffset | 目盛の数字の位置のオフセット位置です。 | 1 | +| --colormap | コンタープロットのカラーマップです。[[https://matplotlib.org/examples/color/colormaps_reference.html][ここ]] から好きなものを選べます。 | bwr | +| --interp | コンタープロットする時の補完の方法です。[linear, cubic, nearest]から[[https://docs.scipy.org/doc/scipy-0.18.1/reference/generated/scipy.interpolate.griddata.html][選択]] 。 | linear | +| --nolegend | 凡例を表示しません。 | | +| --nocontour | コンタープロットを表示しません。 | | +| --nocolorbar | カラーバーを表示しません。 | | +|---------------+------------------------------------------------------------------------------------+---------| + + +* 座標ピックアップ +表示された図中でマウスのボタン(左、中、右どれでも)をクリックすれば、3 +成分の割合が以下のようにターミナルに表示されます。 +#+BEGIN_EXAMPLE +button=1 ['B' 'C' 'A']=(24.52 51.03 24.45) +button=1 ['B' 'C' 'A']=(24.49 44.60 30.91) +button=1 ['B' 'C' 'A']=(45.65 26.83 27.53) +#+END_EXAMPLE diff --git a/a.png b/a.png new file mode 100644 index 0000000..e8c3462 Binary files /dev/null and b/a.png differ diff --git a/b.png b/b.png new file mode 100644 index 0000000..dea5eb6 Binary files /dev/null and b/b.png differ diff --git a/test.xlsx b/test.xlsx new file mode 100644 index 0000000..c55a203 Binary files /dev/null and b/test.xlsx differ diff --git a/xls2ternary.py b/xls2ternary.py new file mode 100755 index 0000000..983f2e3 --- /dev/null +++ b/xls2ternary.py @@ -0,0 +1,239 @@ +#!/usr/bin/env python +import argparse +import os +import pandas as pd +import numpy as np +import matplotlib as mpl +import matplotlib.pyplot as plt +from scipy.interpolate import griddata + +plt.style.use('classic') +plt.rcParams.update({'legend.fancybox': True, 'legend.labelspacing': 0.25, + 'legend.fontsize': 14, 'legend.scatterpoints': 1, + 'font.size': 10, 'font.family': 'sans-serif', + 'savefig.transparent': True, 'savefig.dpi': 300, + 'savefig.bbox': 'tight', + 'xtick.direction': 'in', + 'figure.dpi': 80, + 'axes.grid': True, + 'axes.xmargin': 0, 'axes.labelsize': 16, + 'xtick.major.size': 5, 'xtick.minor.size': 3, + 'ps.useafm': True, + 'pdf.use14corefonts': True}) + + +description = """describe""" +par = argparse.ArgumentParser(description=description) +par.add_argument('xlsfile', help="input excel file") +par.add_argument('-si', '--sheet_index', default=0, type=int, + help="sheet index in excel file [0]") +par.add_argument('-hl', '--header', default=0, type=int, + help="header line number in excel file[0]") +par.add_argument('-m', '--mol_col', nargs=3, default=[0, 1, 2], type=int, + help="row number for mol percent [0 1 2]") +par.add_argument('-p', '--GC_col', default=3, type=int, + help="row number for G/C ratio [3]") +par.add_argument('-xmin', '--xmin', default=0, type=float, + help="x minimum [0]") +par.add_argument('-o', '--outfile', default=None, + help="output file[None]") +par.add_argument('-xl', '--xlabel', default="mol%", + help='xlabel [mol%%]') +par.add_argument('-g', '--grid', default=400, type=int, + help="grid size [400]") +par.add_argument('-fs', '--figsize', default=8, type=int, + help="figure size [8]") +par.add_argument('-ls', '--labelsize', default=16, type=float, + help="label font size [16]") +par.add_argument('-ts', '--ticksize', default=10, type=float, + help="ticks font size [10]") +par.add_argument('--labeloffset', default=6, + help="lebel offset [6]") +par.add_argument('--ticksoffset', default=4, + help="ticks offset [1]") +par.add_argument('--colormap', default='bwr', + help="colour color map [bwr]") +par.add_argument('--interp', default="linear", + choices=['linear', 'nearest', 'cubic'], help="[linear]") +par.add_argument('--nolegend', default=False, + action='store_true', help='not show legend [False]') +par.add_argument('--nocontour', default=False, + action='store_true', help='not contour plot [False]') +par.add_argument('--nocolorbar', default=False, + action='store_true', help='not colorbar [False]') +args = par.parse_args() + +plt.rcParams.update({'font.size': args.labelsize}) + + +def tri2car(data, k=100): + a = data[:, 0] + b = data[:, 1] + c = data[:, 2] + x = k * 0.5 * (2. * b + c) / (a + b + c) + y = k * 0.5 * np.sqrt(3) * c / (a + b + c) + return x, y + + +def tri2car_(data, k=100): + a = data[:, 0] + b = data[:, 1] + c = data[:, 2] + dx = 100 - args.xmin + a = (a - 0)/dx * 100 + b = (b - 0)/dx * 100 + c = (c - args.xmin)/dx * 100 + x = k * 0.5 * (2. * b + c) / (a + b + c) + y = k * 0.5 * np.sqrt(3) * c / (a + b + c) + return x, y + + +def car2tri(x, y): + if x is None or y is None: + return None, None, None + x, y = 0.01 * x, 0.01 * y + c = 2 / np.sqrt(3) * y + b = x - y / np.sqrt(3) + a = 1 - b - c + a = a * (100 - args.xmin) + b = b * (100 - args.xmin) + c = c * (100 - args.xmin) + args.xmin + return a, b, c + + +def MakeFig(label, xlabel, psum): + # 枠線の生成 + frame = np.array([[1, 0, 0], [0, 1, 0], [0, 0, 1], [1, 0, 0]]) + fig, ax = plt.subplots(figsize=(args.figsize, args.figsize)) + x, y = tri2car(frame) + ax.plot(x, y, "k-", lw=1) + # grid線の生成 + m = 100 * np.linspace(0.1, 0.9, 9) + g1, g2, g3 = np.zeros((9, 3)), np.zeros((9, 3)), np.zeros((9, 3)) + # test + m = 100 * np.linspace(0, 1.0, 11) + mx = np.linspace(args.xmin, 100, 11) + my = np.linspace(0, 100 - args.xmin, 11) + mz = np.linspace(0, 100 - args.xmin, 11) + g1, g2, g3 = np.zeros((11, 3)), np.zeros((11, 3)), np.zeros((11, 3)) + + g1[:, 0], g1[:, 1] = m, m[::-1] + g2[:, 0], g2[:, 2] = m, m[::-1] + g3[:, 1], g3[:, 2] = m, m[::-1] + x1, y1 = tri2car(g1) + x2, y2 = tri2car(g2) + x3, y3 = tri2car(g3) + x4, y4 = x1[::-1], y1[::-1] + dashed = [2, 2] + t1, t2 = args.ticksoffset, args.ticksoffset / np.sqrt(2) + t1, t2 = args.ticksoffset * 0.5, args.ticksoffset * 0.5 * np.sqrt(3) + props = {'ha': 'center', 'va': 'center', 'size': args.ticksize} + for i, xi in enumerate(x1): + ax.plot([x1[i], x2[i]], [y1[i], y2[i]], "k:", lw=0.2, dashes=dashed) + ax.plot([x2[i], x3[i]], [y2[i], y3[i]], "k:", lw=0.2, dashes=dashed) + ax.plot([x4[i], x3[i]], [y4[i], y3[i]], "k:", lw=0.2, dashes=dashed) + v1, v2 = "%.1f" % (m[i]), "%.1f" % (m[-i - 1]) + # bottom, C + v2 = "%.1f" % (mz[-i - 1] * 0.01 * psum) + ax.text(x1[i] - t1, y1[i] - t2, v2, props, rotation=60) + # left, B + v1 = "%.1f" % (my[i] * 0.01 * psum) + ax.text(x2[i] - t1, y2[i] + t2, v1, props, rotation=-60) + # right, A + v2 = "%.1f" % (mx[-i - 1] * 0.01 * psum) + ax.text(x3[i] + args.ticksoffset, y3[i], v2, props) + # ラベルの生成 + t1, t2 = args.labeloffset, args.labeloffset / np.sqrt(2) + ax.text(-t2, -t2, label[0], ha="right") + ax.text(100 + t2, -t2, label[1], ha="left") + ax.text(50, 50 * np.sqrt(3) + t1, label[2], ha="center", va="bottom") + ax.text(50, -12, xlabel, ha="center", va="bottom") + ax.axis('off') + offset = 1 + ax.set_xlim(-offset, 120) + ax.set_ylim(-offset, 50 * np.sqrt(3) + offset) + fig.gca().set_aspect('equal', adjustable='box') + return fig, ax + + +def ScatterPlot(header, data, xlabel, val, psum=100, order=[1, 2, 0]): + # cmap = plt.cm.get_cmap(args.colormap) + cmap = mpl.colormaps.get_cmap(args.colormap) + fig, ax = MakeFig(header[order], xlabel, psum) + x, y = tri2car_(data[:, order]) + # contour + if args.nocontour is False: + xi = np.linspace(0, 100, args.grid) + yi = np.linspace(0, 100, args.grid) + zi = griddata((x, y), val, (xi[None, :], yi[:, None]), + method=args.interp) + zi[zi == 0] = 1e-8 + alpha = 0.8 + level = np.linspace(0, 1, 20) + plt.contourf(xi, yi, zi, level, cmap=cmap, alpha=alpha) + # 内挿して得られる最大ポイントの表示 + # zi_ = np.nan_to_num(zi) + # y_, x_ = divmod(np.argmax(zi_), args.grid) + # a_, b_, c_ = car2tri(xi[x_], yi[y_]) + # s = ", ".join(header[order]) + # print("Maximum point: %s = %.2f %.2f %.2f " % (s, a_, b_, c_)) + + # scatter + bounds = np.linspace(0, 1, 11) + norm = mpl.colors.BoundaryNorm(bounds, cmap.N) + # glassのプロット + idx = (val == 1) + if len(idx) != 0: + ax.scatter(x[idx], y[idx], marker="o", label="glass", + color=cmap(0.999), edgecolor="black") + idx = ((0 < val) & (val < 1)) + # crystalとglassの混ざったもののプロット + if len(idx) != 0: + ax.scatter(x[idx], y[idx], marker="^", label="glass/crystal", + color=cmap(0.5), edgecolor="black") + cs = ax.scatter(x, y, c=val, cmap=cmap, norm=norm, s=0.0) + # crystalのプロット + idx = (val == 0) + if len(idx) != 0: + ax.scatter(x[idx], y[idx], marker="x", label="crystal", color=cmap(0)) + # colorbarのプロット + if args.nocolorbar is False: + cbar = fig.colorbar(cs, fraction=0.035, alpha=1.0) + cbar.ax.set_ylabel('glass vol%', fontsize=14) + cbar.ax.set_yticklabels(['%.0f' % (200 * x) + for x in bounds], fontsize=12) + if args.nolegend is False: + ax.legend(loc="best") + return fig + + +# xlsデータの読み込み +df = pd.read_excel(args.xlsfile, skiprows=args.header) +df = df.dropna() +# mol_data = mol_data/18.67*100 +mol_header = df.columns +mol_data = df.iloc[:, args.mol_col].to_numpy() +val = df.iloc[:, args.GC_col].to_numpy() +print("Number of data : ", df.shape) +print("Plot header [col]: ", mol_header[args.mol_col].to_list(), args.mol_col) +print("G/C data [col]: {} [{}]".format(mol_header[args.GC_col], args.GC_col)) + +psum = np.sum(mol_data, axis=1)[0] +# プロット +fig_mol = ScatterPlot(mol_header, mol_data, args.xlabel, val, psum) + +if args.outfile is not None: + fig_mol.savefig(args.outfile) + print(args.outfile, "was created.") + + +def onclick(event): + a, b, c = car2tri(event.xdata, event.ydata) + if a is None: + return + s = mol_header[[1, 2, 0]].to_list() + print('button=%d %s=(%.2f %.2f %.2f)' % (event.button, s, a, b, c)) + + +fig_mol.canvas.mpl_connect('button_press_event', onclick) +plt.show()