イントロダクション
紫外可視分光法(UV-Vis Spectroscopy)は、紫外線(UV: 200-400 nm)から可視光線(Vis: 400-800 nm)の領域で物質による光の吸収を測定する分析手法です。この領域の光は分子の電子遷移を引き起こすため、UV-Visスペクトルは物質の電子状態に関する豊富な情報を提供します。
材料科学においてUV-Visは、有機分子の共役系解析、半導体のバンドギャップ測定、金属錯体の配位子場分裂エネルギー決定、溶液中の濃度定量など、幅広い応用を持つ基本的な分析ツールです。
- 電子遷移の種類と各遷移のエネルギー特性
- ランベルト・ベール則による定量分析の原理
- 発色団と助色団の概念
- UV-Vis分光光度計の構成と測定原理
- Taucプロットによる半導体バンドギャップの決定
- Pythonによるスペクトルデータ解析の実践
1. 電子遷移の基礎
1.1 分子軌道と電子遷移
分子の電子状態は、結合性軌道、非結合性軌道(孤立電子対)、反結合性軌道で構成されます。UV-Vis領域での光吸収は、電子がより低いエネルギー準位からより高いエネルギー準位へ励起されることで生じます。主な電子遷移には以下の種類があります:
| 遷移の種類 | エネルギー | 波長範囲 | 例 |
|---|---|---|---|
| $\sigma \rightarrow \sigma^*$ | 非常に高い | < 150 nm(真空紫外) | メタン、エタン(C-C, C-H結合) |
| $n \rightarrow \sigma^*$ | 高い | 150-250 nm | 水、アルコール、アミン(孤立電子対) |
| $\pi \rightarrow \pi^*$ | 中程度 | 200-500 nm | エチレン、ベンゼン、共役ジエン |
| $n \rightarrow \pi^*$ | 低い | 250-400 nm | カルボニル化合物、アゾ化合物 |
1.2 遷移エネルギーと波長の関係
電子遷移に必要なエネルギー $\Delta E$ は、吸収される光の波長 $\lambda$ と以下の関係式で結ばれます:
$$\Delta E = h\nu = \frac{hc}{\lambda}$$
ここで、$h$ はプランク定数($6.626 \times 10^{-34}$ J s)、$c$ は光速($2.998 \times 10^8$ m/s)です。波長が短いほど光子のエネルギーは高く、より大きなエネルギーギャップを持つ遷移を引き起こします。
コード例1: 波長とエネルギーの相互変換
import numpy as np
import matplotlib.pyplot as plt
# 物理定数
h = 6.62607015e-34 # J s (プランク定数)
c = 2.99792458e8 # m/s (光速)
eV = 1.602176634e-19 # J (1 eV)
def wavelength_to_energy(wavelength_nm):
"""
波長(nm)から光子エネルギー(eV)を計算
Parameters:
-----------
wavelength_nm : float or array
波長(ナノメートル)
Returns:
--------
energy_eV : float or array
光子エネルギー(電子ボルト)
"""
wavelength_m = wavelength_nm * 1e-9
energy_J = h * c / wavelength_m
energy_eV = energy_J / eV
return energy_eV
def energy_to_wavelength(energy_eV_val):
"""
エネルギー(eV)から波長(nm)を計算
Parameters:
-----------
energy_eV_val : float or array
光子エネルギー(電子ボルト)
Returns:
--------
wavelength_nm : float or array
波長(ナノメートル)
"""
energy_J = energy_eV_val * eV
wavelength_m = h * c / energy_J
wavelength_nm = wavelength_m * 1e9
return wavelength_nm
# 代表的な電子遷移の波長とエネルギー
transitions = {
'sigma-sigma* (C-C)': 135,
'n-sigma* (H2O)': 167,
'n-sigma* (CH3OH)': 183,
'pi-pi* (ethylene)': 165,
'pi-pi* (1,3-butadiene)': 217,
'pi-pi* (benzene)': 254,
'n-pi* (acetone)': 280,
'n-pi* (acetaldehyde)': 290
}
print("=" * 70)
print("電子遷移の波長とエネルギー")
print("=" * 70)
print(f"{'遷移':<25} {'波長 (nm)':<15} {'エネルギー (eV)':<15}")
print("-" * 70)
for transition, wavelength in transitions.items():
energy = wavelength_to_energy(wavelength)
print(f"{transition:<25} {wavelength:<15} {energy:.2f}")
# UV-Vis領域のエネルギー分布をプロット
wavelengths = np.linspace(200, 800, 500)
energies = wavelength_to_energy(wavelengths)
fig, ax1 = plt.subplots(figsize=(12, 6))
# 波長-エネルギー曲線
ax1.plot(wavelengths, energies, 'b-', linewidth=2)
ax1.set_xlabel('Wavelength (nm)', fontsize=12)
ax1.set_ylabel('Photon Energy (eV)', fontsize=12, color='b')
ax1.tick_params(axis='y', labelcolor='b')
ax1.set_xlim(200, 800)
# 可視光の色を背景に追加
colors = [
(380, 450, '#7B68EE'), # 紫
(450, 495, '#0000FF'), # 青
(495, 570, '#00FF00'), # 緑
(570, 590, '#FFFF00'), # 黄
(590, 620, '#FFA500'), # 橙
(620, 750, '#FF0000'), # 赤
]
for start, end, color in colors:
ax1.axvspan(max(200, start), min(800, end), alpha=0.2, color=color)
# 紫外領域のマーク
ax1.axvspan(200, 380, alpha=0.1, color='purple', label='UV region')
ax1.set_title('UV-Vis Energy-Wavelength Relationship', fontsize=14, fontweight='bold')
ax1.grid(True, alpha=0.3)
ax1.legend(loc='upper right')
plt.tight_layout()
plt.savefig('uv_vis_energy_wavelength.png', dpi=300, bbox_inches='tight')
plt.show()
print("\n" + "=" * 70)
print("可視光の色と波長範囲")
print("=" * 70)
visible_colors = [
('紫', 380, 450, wavelength_to_energy(415)),
('青', 450, 495, wavelength_to_energy(472.5)),
('緑', 495, 570, wavelength_to_energy(532.5)),
('黄', 570, 590, wavelength_to_energy(580)),
('橙', 590, 620, wavelength_to_energy(605)),
('赤', 620, 750, wavelength_to_energy(685)),
]
print(f"{'色':<8} {'波長範囲 (nm)':<20} {'平均エネルギー (eV)':<15}")
print("-" * 50)
for color, start, end, energy in visible_colors:
print(f"{color:<8} {start}-{end:<13} {energy:.2f}")
2. ランベルト・ベール則
2.1 光吸収の定量的記述
溶液による光吸収の強度は、ランベルト・ベール則(Lambert-Beer Law)によって定量的に記述されます。透過率 $T$ と吸光度 $A$ は以下のように定義されます:
$$T = \frac{I}{I_0}$$
$$A = -\log_{10}(T) = \log_{10}\left(\frac{I_0}{I}\right) = \varepsilon c l$$
ここで:
- $I_0$:入射光強度
- $I$:透過光強度
- $\varepsilon$:モル吸光係数(L mol-1 cm-1)
- $c$:濃度(mol/L)
- $l$:光路長(cm)
モル吸光係数 $\varepsilon$ は、分子固有の光吸収能力を表します。$\varepsilon$ が大きいほど、その波長での光吸収が強いことを意味します。典型的な値の範囲:
- $n \rightarrow \pi^*$ 遷移:$\varepsilon \approx 10-100$ L mol-1 cm-1(禁制遷移)
- $\pi \rightarrow \pi^*$ 遷移:$\varepsilon \approx 10^3-10^5$ L mol-1 cm-1(許容遷移)
コード例2: ランベルト・ベール則と検量線作成
import numpy as np
import matplotlib.pyplot as plt
from scipy import stats
def beer_lambert(epsilon, c, l):
"""
ランベルト・ベール則による吸光度計算
Parameters:
-----------
epsilon : float
モル吸光係数 (L mol^-1 cm^-1)
c : float or array
濃度 (mol/L)
l : float
光路長 (cm)
Returns:
--------
A : float or array
吸光度
"""
return epsilon * c * l
def calculate_concentration(A, epsilon, l):
"""
吸光度から濃度を計算
Parameters:
-----------
A : float
吸光度
epsilon : float
モル吸光係数 (L mol^-1 cm^-1)
l : float
光路長 (cm)
Returns:
--------
c : float
濃度 (mol/L)
"""
return A / (epsilon * l)
# 過マンガン酸カリウム(KMnO4)の検量線作成
# 525 nm での モル吸光係数: 約 2500 L mol^-1 cm^-1
epsilon_KMnO4 = 2500 # L mol^-1 cm^-1
path_length = 1.0 # cm
# 標準溶液の調製(既知濃度)
concentrations_std = np.array([0.0, 0.02, 0.04, 0.06, 0.08, 0.10]) * 1e-3 # mol/L
# 理論吸光度
absorbances_theoretical = beer_lambert(epsilon_KMnO4, concentrations_std, path_length)
# 実験データをシミュレート(ノイズ付き)
np.random.seed(42)
noise = np.random.normal(0, 0.005, len(concentrations_std))
absorbances_exp = absorbances_theoretical + noise
absorbances_exp[0] = 0 # ブランクは0
# 線形回帰
slope, intercept, r_value, p_value, std_err = stats.linregress(
concentrations_std * 1e3, absorbances_exp
)
print("=" * 70)
print("KMnO4 検量線解析結果")
print("=" * 70)
print(f"波長: 525 nm")
print(f"光路長: {path_length} cm")
print(f"理論モル吸光係数: {epsilon_KMnO4} L mol^-1 cm^-1")
print("-" * 70)
print(f"回帰式: A = {slope:.2f} x c (mM) + {intercept:.4f}")
print(f"実験モル吸光係数: {slope * 1000:.0f} L mol^-1 cm^-1")
print(f"決定係数 R^2: {r_value**2:.6f}")
print(f"標準誤差: {std_err:.4f}")
# 未知試料の濃度決定
unknown_absorbance = 0.175
unknown_concentration = (unknown_absorbance - intercept) / slope # mM
print("-" * 70)
print(f"未知試料の吸光度: {unknown_absorbance}")
print(f"推定濃度: {unknown_concentration:.4f} mM ({unknown_concentration * 1e-3:.6f} mol/L)")
# プロット
fig, axes = plt.subplots(1, 2, figsize=(14, 6))
# 検量線
ax1 = axes[0]
ax1.scatter(concentrations_std * 1e3, absorbances_exp, s=100, c='#f093fb',
edgecolors='black', zorder=5, label='Experimental data')
x_fit = np.linspace(0, 0.12, 100)
y_fit = slope * x_fit + intercept
ax1.plot(x_fit, y_fit, 'r-', linewidth=2, label=f'Linear fit (R$^2$ = {r_value**2:.4f})')
# 未知試料のプロット
ax1.axhline(y=unknown_absorbance, color='green', linestyle='--', alpha=0.7)
ax1.axvline(x=unknown_concentration, color='green', linestyle='--', alpha=0.7)
ax1.scatter([unknown_concentration], [unknown_absorbance], s=150, c='green',
marker='*', zorder=6, label=f'Unknown sample: {unknown_concentration:.3f} mM')
ax1.set_xlabel('Concentration (mM)', fontsize=12)
ax1.set_ylabel('Absorbance', fontsize=12)
ax1.set_title('KMnO4 Calibration Curve (525 nm)', fontsize=14, fontweight='bold')
ax1.legend(loc='upper left')
ax1.grid(True, alpha=0.3)
ax1.set_xlim(-0.005, 0.12)
ax1.set_ylim(-0.01, 0.30)
# ビール則からの偏差(高濃度領域)
ax2 = axes[1]
concentrations_wide = np.linspace(0, 0.5e-3, 100)
absorbances_ideal = beer_lambert(epsilon_KMnO4, concentrations_wide, path_length)
# 高濃度での負の偏差をシミュレート
absorbances_real = absorbances_ideal * (1 - 0.3 * (concentrations_wide / concentrations_wide.max())**2)
ax2.plot(concentrations_wide * 1e3, absorbances_ideal, 'b-', linewidth=2,
label='Ideal (Beer-Lambert law)')
ax2.plot(concentrations_wide * 1e3, absorbances_real, 'r--', linewidth=2,
label='Real (with deviation)')
ax2.axvline(x=0.15, color='orange', linestyle=':', linewidth=2,
label='Recommended upper limit')
ax2.set_xlabel('Concentration (mM)', fontsize=12)
ax2.set_ylabel('Absorbance', fontsize=12)
ax2.set_title('Deviation from Beer-Lambert Law', fontsize=14, fontweight='bold')
ax2.legend(loc='upper left')
ax2.grid(True, alpha=0.3)
plt.tight_layout()
plt.savefig('beer_lambert_calibration.png', dpi=300, bbox_inches='tight')
plt.show()
2.2 ビール則からの偏差
ランベルト・ベール則は理想的な条件下でのみ成立します。以下の要因により偏差が生じることがあります:
- 化学的要因:高濃度での会合・解離平衡の変化
- 機器的要因:単色光の不完全性、迷光
- 屈折率変化:高濃度での溶液屈折率の変化
実用上、吸光度 $A < 1.0$(透過率 $T > 10\%$)の範囲で測定することが推奨されます。
3. 発色団と助色団
3.1 発色団(Chromophore)
発色団は、UV-Vis領域で光を吸収する官能基です。発色団の電子構造が吸収波長を決定します。
| 発色団 | 遷移 | 吸収極大 (nm) | モル吸光係数 |
|---|---|---|---|
| C=C(エチレン) | $\pi \rightarrow \pi^*$ | 165 | 10,000 |
| C=O(アルデヒド) | $n \rightarrow \pi^*$ | 290 | 15 |
| C=O(ケトン) | $n \rightarrow \pi^*$ | 280 | 20 |
| N=N(アゾ) | $n \rightarrow \pi^*$ | 350 | 15 |
| ベンゼン環 | $\pi \rightarrow \pi^*$ | 254 | 200 |
3.2 助色団(Auxochrome)
助色団は、それ自体では発色しませんが、発色団に結合すると吸収波長や吸光度を変化させる官能基です。典型的な助色団には -OH、-NH2、-OR、-NR2、-Cl などがあります。
- 深色シフト(Bathochromic shift / Red shift):吸収極大が長波長側へ移動
- 浅色シフト(Hypsochromic shift / Blue shift):吸収極大が短波長側へ移動
- 濃色効果(Hyperchromic effect):吸光度の増加
- 淡色効果(Hypochromic effect):吸光度の減少
コード例3: 共役系の拡張と吸収スペクトル変化
import numpy as np
import matplotlib.pyplot as plt
from scipy.stats import norm
def generate_absorption_spectrum(lambda_max, epsilon_max, fwhm, wavelengths):
"""
ガウス型吸収スペクトルを生成
Parameters:
-----------
lambda_max : float
吸収極大波長 (nm)
epsilon_max : float
最大モル吸光係数
fwhm : float
半値全幅 (nm)
wavelengths : array
波長配列 (nm)
Returns:
--------
spectrum : array
モル吸光係数のスペクトル
"""
sigma = fwhm / (2 * np.sqrt(2 * np.log(2)))
spectrum = epsilon_max * np.exp(-(wavelengths - lambda_max)**2 / (2 * sigma**2))
return spectrum
# 共役ポリエンの吸収スペクトル(共役長の影響)
# エチレン → ブタジエン → ヘキサトリエン → オクタテトラエン
polyenes = {
'Ethylene (1 C=C)': {'lambda_max': 165, 'epsilon': 10000, 'fwhm': 30},
'1,3-Butadiene (2 C=C)': {'lambda_max': 217, 'epsilon': 21000, 'fwhm': 35},
'1,3,5-Hexatriene (3 C=C)': {'lambda_max': 258, 'epsilon': 35000, 'fwhm': 40},
'1,3,5,7-Octatetraene (4 C=C)': {'lambda_max': 290, 'epsilon': 52000, 'fwhm': 45},
}
wavelengths = np.linspace(150, 450, 500)
fig, axes = plt.subplots(1, 2, figsize=(14, 6))
# 吸収スペクトル
ax1 = axes[0]
colors = ['#f093fb', '#f5576c', '#4ecdc4', '#ffe66d']
for (name, params), color in zip(polyenes.items(), colors):
spectrum = generate_absorption_spectrum(
params['lambda_max'], params['epsilon'], params['fwhm'], wavelengths
)
ax1.plot(wavelengths, spectrum / 1000, linewidth=2, color=color, label=name)
ax1.axvline(x=params['lambda_max'], color=color, linestyle='--', alpha=0.5)
ax1.set_xlabel('Wavelength (nm)', fontsize=12)
ax1.set_ylabel('Molar absorptivity (x10$^3$ L mol$^{-1}$ cm$^{-1}$)', fontsize=12)
ax1.set_title('Conjugated Polyenes: Extended Conjugation Effect', fontsize=14, fontweight='bold')
ax1.legend(loc='upper right', fontsize=9)
ax1.grid(True, alpha=0.3)
ax1.set_xlim(150, 450)
ax1.set_ylim(0, 60)
# 可視光領域のマーク
ax1.axvspan(380, 450, alpha=0.1, color='purple')
# 共役長と吸収極大の関係
ax2 = axes[1]
n_double_bonds = [1, 2, 3, 4]
lambda_max_values = [165, 217, 258, 290]
epsilon_values = [10000, 21000, 35000, 52000]
ax2.plot(n_double_bonds, lambda_max_values, 'o-', markersize=12, linewidth=2,
color='#f093fb', markeredgecolor='black', label='$\\lambda_{max}$')
ax2.set_xlabel('Number of conjugated C=C bonds', fontsize=12)
ax2.set_ylabel('$\\lambda_{max}$ (nm)', fontsize=12, color='#f093fb')
ax2.tick_params(axis='y', labelcolor='#f093fb')
ax2.set_xlim(0.5, 4.5)
ax2.set_ylim(100, 350)
# 第2軸:モル吸光係数
ax2_twin = ax2.twinx()
ax2_twin.plot(n_double_bonds, np.array(epsilon_values) / 1000, 's-', markersize=10,
linewidth=2, color='#4ecdc4', markeredgecolor='black', label='$\\epsilon_{max}$')
ax2_twin.set_ylabel('$\\epsilon_{max}$ (x10$^3$ L mol$^{-1}$ cm$^{-1}$)', fontsize=12, color='#4ecdc4')
ax2_twin.tick_params(axis='y', labelcolor='#4ecdc4')
ax2_twin.set_ylim(0, 60)
ax2.set_title('Effect of Conjugation Length on UV-Vis Absorption', fontsize=14, fontweight='bold')
ax2.grid(True, alpha=0.3)
# 凡例を結合
lines1, labels1 = ax2.get_legend_handles_labels()
lines2, labels2 = ax2_twin.get_legend_handles_labels()
ax2.legend(lines1 + lines2, labels1 + labels2, loc='upper left')
plt.tight_layout()
plt.savefig('conjugation_effect.png', dpi=300, bbox_inches='tight')
plt.show()
# 数値データの表示
print("=" * 70)
print("共役ポリエンのUV-Vis吸収特性")
print("=" * 70)
print(f"{'化合物':<35} {'共役C=C数':<12} {'lambda_max (nm)':<18} {'epsilon_max':<15}")
print("-" * 70)
for (name, params) in polyenes.items():
n_cc = name.split('(')[1].split()[0]
print(f"{name:<35} {n_cc:<12} {params['lambda_max']:<18} {params['epsilon']:<15}")
4. UV-Vis分光光度計の構成
4.1 分光光度計の基本構成
UV-Vis分光光度計は以下の主要構成要素から成ります:
- 光源:重水素ランプ(UV領域: 190-350 nm)、タングステン-ハロゲンランプ(Vis-NIR領域: 350-2500 nm)
- 分光器:回折格子または プリズムで単色光を生成
- 試料室:セルホルダー、温度制御機構(オプション)
- 検出器:光電子増倍管(PMT)または フォトダイオードアレイ(PDA)
4.2 ダブルビーム方式
現代のUV-Vis分光光度計の多くはダブルビーム方式を採用しています。光を二つに分け、一方を試料セル、他方をリファレンス(参照)セルに通すことで、光源の強度変動やセルの汚れによる誤差を補正できます。
コード例4: UV-Visスペクトルのシミュレーションと解析
import numpy as np
import matplotlib.pyplot as plt
from scipy.signal import find_peaks
from scipy.ndimage import gaussian_filter1d
def simulate_uv_vis_spectrum(compounds, wavelengths):
"""
複数成分の混合物のUV-Visスペクトルをシミュレート
Parameters:
-----------
compounds : list of dict
各化合物の情報 {'name', 'lambda_max', 'epsilon', 'fwhm', 'concentration'}
wavelengths : array
波長配列 (nm)
Returns:
--------
total_absorbance : array
合計吸光度スペクトル
individual_spectra : dict
個別成分のスペクトル
"""
total_absorbance = np.zeros_like(wavelengths, dtype=float)
individual_spectra = {}
path_length = 1.0 # cm
for compound in compounds:
sigma = compound['fwhm'] / (2 * np.sqrt(2 * np.log(2)))
epsilon_spectrum = compound['epsilon'] * np.exp(
-(wavelengths - compound['lambda_max'])**2 / (2 * sigma**2)
)
absorbance = epsilon_spectrum * compound['concentration'] * path_length
individual_spectra[compound['name']] = absorbance
total_absorbance += absorbance
return total_absorbance, individual_spectra
def analyze_spectrum(wavelengths, absorbance, prominence=0.01):
"""
スペクトルのピーク解析
Parameters:
-----------
wavelengths : array
波長配列 (nm)
absorbance : array
吸光度配列
prominence : float
ピーク検出の閾値
Returns:
--------
peaks_info : list of dict
検出されたピークの情報
"""
peaks, properties = find_peaks(absorbance, prominence=prominence)
peaks_info = []
for i, peak_idx in enumerate(peaks):
peaks_info.append({
'wavelength': wavelengths[peak_idx],
'absorbance': absorbance[peak_idx],
'prominence': properties['prominences'][i]
})
return peaks_info
# 有機分子混合物のスペクトルシミュレーション
compounds = [
{'name': 'Benzene', 'lambda_max': 254, 'epsilon': 200, 'fwhm': 15,
'concentration': 1e-4},
{'name': 'Naphthalene', 'lambda_max': 275, 'epsilon': 5600, 'fwhm': 18,
'concentration': 2e-5},
{'name': 'Naphthalene (2nd band)', 'lambda_max': 220, 'epsilon': 100000, 'fwhm': 25,
'concentration': 2e-5},
{'name': 'Acetone', 'lambda_max': 280, 'epsilon': 15, 'fwhm': 20,
'concentration': 1e-2},
]
wavelengths = np.linspace(200, 350, 500)
# スペクトル計算
total_abs, individual = simulate_uv_vis_spectrum(compounds, wavelengths)
# ノイズ追加(実験データのシミュレート)
np.random.seed(42)
noise = np.random.normal(0, 0.002, len(wavelengths))
measured_abs = total_abs + noise
# スムージング
smoothed_abs = gaussian_filter1d(measured_abs, sigma=2)
# ピーク検出
peaks_info = analyze_spectrum(wavelengths, smoothed_abs, prominence=0.005)
# プロット
fig, axes = plt.subplots(2, 2, figsize=(14, 12))
# 個別成分スペクトル
ax1 = axes[0, 0]
colors = ['#f093fb', '#f5576c', '#4ecdc4', '#ffe66d']
for (name, spectrum), color in zip(individual.items(), colors):
ax1.plot(wavelengths, spectrum, linewidth=2, color=color, label=name)
ax1.set_xlabel('Wavelength (nm)', fontsize=12)
ax1.set_ylabel('Absorbance', fontsize=12)
ax1.set_title('Individual Component Spectra', fontsize=14, fontweight='bold')
ax1.legend(loc='upper right', fontsize=9)
ax1.grid(True, alpha=0.3)
# 合計スペクトルと測定スペクトル
ax2 = axes[0, 1]
ax2.plot(wavelengths, measured_abs, 'k-', alpha=0.3, linewidth=1, label='Measured (with noise)')
ax2.plot(wavelengths, smoothed_abs, 'b-', linewidth=2, label='Smoothed')
ax2.plot(wavelengths, total_abs, 'r--', linewidth=1.5, label='True spectrum')
# ピークをマーク
for peak in peaks_info:
ax2.axvline(x=peak['wavelength'], color='green', linestyle=':', alpha=0.7)
ax2.annotate(f"{peak['wavelength']:.0f} nm",
xy=(peak['wavelength'], peak['absorbance']),
xytext=(peak['wavelength'] + 10, peak['absorbance'] + 0.02),
fontsize=9, color='green')
ax2.set_xlabel('Wavelength (nm)', fontsize=12)
ax2.set_ylabel('Absorbance', fontsize=12)
ax2.set_title('Mixed Sample UV-Vis Spectrum', fontsize=14, fontweight='bold')
ax2.legend(loc='upper right')
ax2.grid(True, alpha=0.3)
# 透過率スペクトル
ax3 = axes[1, 0]
transmittance = 10**(-smoothed_abs) * 100 # パーセント透過率
ax3.plot(wavelengths, transmittance, 'g-', linewidth=2)
ax3.set_xlabel('Wavelength (nm)', fontsize=12)
ax3.set_ylabel('Transmittance (%)', fontsize=12)
ax3.set_title('Transmittance Spectrum', fontsize=14, fontweight='bold')
ax3.set_ylim(0, 105)
ax3.grid(True, alpha=0.3)
# 一次微分スペクトル
ax4 = axes[1, 1]
derivative = np.gradient(smoothed_abs, wavelengths)
ax4.plot(wavelengths, derivative, 'm-', linewidth=2)
ax4.axhline(y=0, color='black', linestyle='-', linewidth=0.5)
ax4.set_xlabel('Wavelength (nm)', fontsize=12)
ax4.set_ylabel('dA/d$\\lambda$', fontsize=12)
ax4.set_title('First Derivative Spectrum', fontsize=14, fontweight='bold')
ax4.grid(True, alpha=0.3)
plt.tight_layout()
plt.savefig('uv_vis_spectrum_analysis.png', dpi=300, bbox_inches='tight')
plt.show()
# ピーク情報の表示
print("=" * 60)
print("検出されたピーク情報")
print("=" * 60)
print(f"{'波長 (nm)':<15} {'吸光度':<15} {'プロミネンス':<15}")
print("-" * 60)
for peak in sorted(peaks_info, key=lambda x: x['wavelength']):
print(f"{peak['wavelength']:<15.1f} {peak['absorbance']:<15.4f} {peak['prominence']:<15.4f}")
5. 半導体のバンドギャップ決定
5.1 Taucプロット法
半導体材料のバンドギャップ($E_g$)はUV-Vis拡散反射スペクトルから決定できます。Taucの関係式は以下で表されます:
$$(\alpha h\nu)^n = A(h\nu - E_g)$$
ここで:
- $\alpha$:吸収係数
- $h\nu$:光子エネルギー
- $A$:定数
- $n$:遷移の種類による指数(直接遷移: $n=2$、間接遷移: $n=1/2$)
粉末試料の場合、Kubelka-Munk関数 $F(R)$ を用いて吸収係数を近似します:
$$F(R) = \frac{(1-R)^2}{2R} \approx \alpha$$
コード例5: Taucプロットによるバンドギャップ決定
import numpy as np
import matplotlib.pyplot as plt
from scipy import stats
from scipy.ndimage import gaussian_filter1d
def kubelka_munk(reflectance):
"""
Kubelka-Munk変換
Parameters:
-----------
reflectance : array
反射率(0-1)
Returns:
--------
F_R : array
Kubelka-Munk関数値
"""
R = np.clip(reflectance, 0.001, 0.999) # ゼロ除算を防ぐ
F_R = (1 - R)**2 / (2 * R)
return F_R
def tauc_plot(wavelength_nm, reflectance, n=2, fit_range=None):
"""
Taucプロットによるバンドギャップ決定
Parameters:
-----------
wavelength_nm : array
波長(nm)
reflectance : array
反射率(0-1)
n : float
遷移タイプ(2: 直接許容遷移、1/2: 間接許容遷移)
fit_range : tuple
フィッティング範囲(eV)
Returns:
--------
E_g : float
バンドギャップ(eV)
energy : array
光子エネルギー配列
tauc_y : array
Taucプロットのy軸値
fit_params : dict
フィッティングパラメータ
"""
# 波長をエネルギーに変換
h = 6.62607015e-34 # J s
c = 2.99792458e8 # m/s
eV = 1.602176634e-19 # J
wavelength_m = wavelength_nm * 1e-9
energy = (h * c / wavelength_m) / eV # eV
# Kubelka-Munk変換
F_R = kubelka_munk(reflectance)
# Taucプロット値
tauc_y = (F_R * energy)**n
# スムージング
tauc_y_smooth = gaussian_filter1d(tauc_y, sigma=3)
# 線形領域の自動検出またはユーザー指定
if fit_range is None:
# 最大傾斜領域を探す
gradient = np.gradient(tauc_y_smooth, energy)
max_grad_idx = np.argmax(gradient)
# 傾斜が大きい領域の前後を取る
fit_mask = (energy > energy[max_grad_idx] - 0.3) & (energy < energy[max_grad_idx] + 0.3)
else:
fit_mask = (energy >= fit_range[0]) & (energy <= fit_range[1])
# 線形回帰
if np.sum(fit_mask) > 10:
slope, intercept, r_value, p_value, std_err = stats.linregress(
energy[fit_mask], tauc_y_smooth[fit_mask]
)
E_g = -intercept / slope if slope > 0 else np.nan
else:
slope, intercept, E_g, r_value = np.nan, np.nan, np.nan, np.nan
fit_params = {
'slope': slope,
'intercept': intercept,
'r_squared': r_value**2 if not np.isnan(r_value) else np.nan,
'fit_mask': fit_mask
}
return E_g, energy, tauc_y_smooth, fit_params
# 半導体材料のUV-Visデータシミュレーション
def simulate_semiconductor_spectrum(E_g, wavelength_nm, transition='direct'):
"""
半導体の拡散反射スペクトルをシミュレート
"""
h = 6.62607015e-34
c = 2.99792458e8
eV = 1.602176634e-19
wavelength_m = wavelength_nm * 1e-9
energy = (h * c / wavelength_m) / eV
# 吸収係数のモデル
n = 2 if transition == 'direct' else 0.5
alpha = np.where(energy > E_g, 1e5 * (energy - E_g)**n, 0)
# 反射率に変換(簡略化モデル)
reflectance = np.exp(-alpha * 1e-4) # 仮想的な膜厚
reflectance = np.clip(reflectance, 0.01, 0.99)
# ノイズ追加
noise = np.random.normal(0, 0.01, len(reflectance))
reflectance = np.clip(reflectance + noise, 0.01, 0.99)
return reflectance
# 異なるバンドギャップの半導体をシミュレート
semiconductors = {
'TiO2 (anatase)': {'E_g_true': 3.2, 'transition': 'indirect'},
'ZnO': {'E_g_true': 3.37, 'transition': 'direct'},
'CdS': {'E_g_true': 2.42, 'transition': 'direct'},
'Fe2O3 (hematite)': {'E_g_true': 2.1, 'transition': 'indirect'},
}
wavelength = np.linspace(300, 800, 500)
fig, axes = plt.subplots(2, 2, figsize=(14, 12))
results = {}
for idx, (name, params) in enumerate(semiconductors.items()):
ax = axes[idx // 2, idx % 2]
# スペクトル生成
np.random.seed(idx)
reflectance = simulate_semiconductor_spectrum(
params['E_g_true'], wavelength, params['transition']
)
# Taucプロット解析
n = 2 if params['transition'] == 'direct' else 0.5
E_g, energy, tauc_y, fit_params = tauc_plot(
wavelength, reflectance, n=n
)
results[name] = {'E_g_measured': E_g, 'E_g_true': params['E_g_true']}
# プロット
ax.plot(energy, tauc_y, 'b-', linewidth=2, label='Tauc plot')
# フィッティング線
if not np.isnan(fit_params['slope']):
x_fit = np.linspace(E_g - 0.5, np.max(energy[fit_params['fit_mask']]) + 0.5, 100)
y_fit = fit_params['slope'] * x_fit + fit_params['intercept']
y_fit = np.clip(y_fit, 0, None)
ax.plot(x_fit, y_fit, 'r--', linewidth=2,
label=f'Linear fit (R$^2$={fit_params["r_squared"]:.3f})')
# バンドギャップのマーク
ax.axvline(x=E_g, color='green', linestyle=':', linewidth=2,
label=f'$E_g$ = {E_g:.2f} eV')
ax.axvline(x=params['E_g_true'], color='orange', linestyle='--', linewidth=1.5,
alpha=0.7, label=f'True $E_g$ = {params["E_g_true"]:.2f} eV')
transition_type = 'Direct' if params['transition'] == 'direct' else 'Indirect'
ax.set_xlabel('Photon Energy (eV)', fontsize=11)
ax.set_ylabel(f'$(F(R) \\cdot h\\nu)^{{{n}}}$', fontsize=11)
ax.set_title(f'{name} ({transition_type} transition)', fontsize=12, fontweight='bold')
ax.legend(loc='upper left', fontsize=9)
ax.grid(True, alpha=0.3)
ax.set_xlim(1.5, 4.5)
ax.set_ylim(0, np.nanmax(tauc_y) * 1.2)
plt.tight_layout()
plt.savefig('tauc_plot_bandgap.png', dpi=300, bbox_inches='tight')
plt.show()
# 結果の表示
print("=" * 70)
print("Taucプロットによるバンドギャップ決定結果")
print("=" * 70)
print(f"{'材料':<25} {'測定値 (eV)':<15} {'文献値 (eV)':<15} {'誤差 (%)':<10}")
print("-" * 70)
for name, values in results.items():
error = abs(values['E_g_measured'] - values['E_g_true']) / values['E_g_true'] * 100
print(f"{name:<25} {values['E_g_measured']:<15.2f} {values['E_g_true']:<15.2f} {error:<10.1f}")
5.2 直接遷移と間接遷移
半導体のバンド構造により、光学遷移は直接遷移と間接遷移に分類されます:
- 直接遷移型半導体:価電子帯の頂上と伝導帯の底が同じ波数ベクトル $k$ を持つ。GaAs、CdS、ZnOなど。Taucプロットで $n=2$ を使用。
- 間接遷移型半導体:価電子帯の頂上と伝導帯の底の $k$ が異なる。Si、Ge、TiO2など。Taucプロットで $n=1/2$ を使用。フォノンの関与が必要。
コード例6: 複数試料のバンドギャップ比較解析
import numpy as np
import matplotlib.pyplot as plt
from scipy import stats
def batch_bandgap_analysis(samples_data, plot=True):
"""
複数試料のバンドギャップを一括解析
Parameters:
-----------
samples_data : list of dict
各試料のデータ {'name', 'wavelength', 'reflectance', 'transition'}
plot : bool
プロットを表示するか
Returns:
--------
results : dict
各試料のバンドギャップ結果
"""
h = 6.62607015e-34
c = 2.99792458e8
eV = 1.602176634e-19
results = {}
if plot:
n_samples = len(samples_data)
n_cols = min(3, n_samples)
n_rows = (n_samples + n_cols - 1) // n_cols
fig, axes = plt.subplots(n_rows, n_cols, figsize=(5*n_cols, 4*n_rows))
axes = np.atleast_2d(axes).flatten()
for idx, sample in enumerate(samples_data):
wavelength = sample['wavelength']
reflectance = sample['reflectance']
n = 2 if sample['transition'] == 'direct' else 0.5
# エネルギー変換
wavelength_m = wavelength * 1e-9
energy = (h * c / wavelength_m) / eV
# Kubelka-Munk変換
R = np.clip(reflectance, 0.001, 0.999)
F_R = (1 - R)**2 / (2 * R)
# Taucプロット
tauc_y = (F_R * energy)**n
# 微分による線形領域検出
gradient = np.gradient(tauc_y, energy)
gradient_smooth = gaussian_filter1d(gradient, sigma=5)
# 最大勾配点
max_grad_idx = np.argmax(gradient_smooth)
# フィッティング領域
fit_width = int(len(energy) * 0.15)
start_idx = max(0, max_grad_idx - fit_width // 2)
end_idx = min(len(energy), max_grad_idx + fit_width // 2)
# 線形回帰
slope, intercept, r_value, _, _ = stats.linregress(
energy[start_idx:end_idx], tauc_y[start_idx:end_idx]
)
E_g = -intercept / slope if slope > 0 else np.nan
results[sample['name']] = {
'E_g': E_g,
'R_squared': r_value**2,
'transition': sample['transition']
}
if plot:
ax = axes[idx]
ax.plot(energy, tauc_y, 'b-', linewidth=1.5, alpha=0.8)
# フィッティング線
x_fit = np.linspace(E_g - 0.3, energy[end_idx], 50)
y_fit = slope * x_fit + intercept
y_fit = np.clip(y_fit, 0, None)
ax.plot(x_fit, y_fit, 'r--', linewidth=2)
ax.axvline(x=E_g, color='green', linestyle=':', linewidth=2)
ax.set_xlabel('Photon Energy (eV)', fontsize=10)
ax.set_ylabel(f'$(F(R) \\cdot h\\nu)^{{{n}}}$', fontsize=10)
ax.set_title(f"{sample['name']}\n$E_g$ = {E_g:.2f} eV", fontsize=11, fontweight='bold')
ax.grid(True, alpha=0.3)
ax.set_xlim(min(energy) - 0.2, max(energy) + 0.2)
ax.set_ylim(0, max(tauc_y) * 1.1)
# 未使用の軸を非表示
if plot:
for idx in range(len(samples_data), len(axes)):
axes[idx].set_visible(False)
plt.tight_layout()
plt.savefig('batch_bandgap_analysis.png', dpi=300, bbox_inches='tight')
plt.show()
return results
# ドーピングによるバンドギャップ変調のシミュレーション
def simulate_doped_semiconductor(base_E_g, dopant_concentration, wavelength):
"""
ドーピングによるバンドギャップ変調をシミュレート
"""
# バーンシュタイン-モス効果による青方シフト
delta_E = 0.1 * np.log(1 + dopant_concentration * 100)
effective_E_g = base_E_g + delta_E
h = 6.62607015e-34
c = 2.99792458e8
eV = 1.602176634e-19
wavelength_m = wavelength * 1e-9
energy = (h * c / wavelength_m) / eV
# 吸収スペクトル
alpha = np.where(energy > effective_E_g, 1e5 * (energy - effective_E_g)**2, 0)
reflectance = np.exp(-alpha * 1e-4)
reflectance = np.clip(reflectance + np.random.normal(0, 0.005, len(wavelength)), 0.01, 0.99)
return reflectance, effective_E_g
# ZnOドーピング系列のシミュレーション
wavelength = np.linspace(300, 600, 400)
dopant_levels = [0.0, 0.01, 0.02, 0.05, 0.10]
samples = []
true_E_g_values = []
for conc in dopant_levels:
np.random.seed(int(conc * 1000))
reflectance, true_E_g = simulate_doped_semiconductor(3.37, conc, wavelength)
true_E_g_values.append(true_E_g)
samples.append({
'name': f'ZnO (Al {conc*100:.0f}%)',
'wavelength': wavelength,
'reflectance': reflectance,
'transition': 'direct'
})
# バッチ解析
results = batch_bandgap_analysis(samples, plot=True)
# 結果まとめ
print("\n" + "=" * 70)
print("Al-doped ZnOシリーズのバンドギャップ解析")
print("=" * 70)
print(f"{'試料':<20} {'測定E_g (eV)':<15} {'真E_g (eV)':<15} {'R^2':<10}")
print("-" * 70)
for (name, res), true_Eg in zip(results.items(), true_E_g_values):
print(f"{name:<20} {res['E_g']:<15.3f} {true_Eg:<15.3f} {res['R_squared']:<10.4f}")
6. 実践的スペクトル解析
6.1 ベースライン補正とスムージング
実験データには様々なノイズやベースラインのドリフトが含まれます。正確な解析のためには、前処理が重要です。
コード例7: スペクトルデータの前処理
import numpy as np
import matplotlib.pyplot as plt
from scipy.signal import savgol_filter
from scipy.ndimage import gaussian_filter1d
from scipy import sparse
from scipy.sparse.linalg import splu
def baseline_als(y, lam=1e5, p=0.01, niter=10):
"""
Asymmetric Least Squares (ALS) によるベースライン補正
Parameters:
-----------
y : array
スペクトルデータ
lam : float
平滑化パラメータ(大きいほど滑らか)
p : float
非対称パラメータ(0-1、小さいほどベースラインが下に)
niter : int
反復回数
Returns:
--------
baseline : array
推定されたベースライン
"""
L = len(y)
D = sparse.diags([1, -2, 1], [0, -1, -2], shape=(L, L-2))
w = np.ones(L)
for i in range(niter):
W = sparse.spdiags(w, 0, L, L)
Z = W + lam * D.dot(D.T)
z = splu(Z).solve(w * y)
w = p * (y > z) + (1-p) * (y < z)
return z
def preprocess_spectrum(wavelength, absorbance, smooth_method='savgol',
baseline_correct=True, normalize=True):
"""
スペクトルデータの前処理パイプライン
Parameters:
-----------
wavelength : array
波長配列
absorbance : array
吸光度配列
smooth_method : str
スムージング方法 ('savgol', 'gaussian', 'none')
baseline_correct : bool
ベースライン補正を行うか
normalize : bool
正規化を行うか
Returns:
--------
processed : dict
前処理されたデータと中間結果
"""
processed = {
'wavelength': wavelength,
'original': absorbance.copy()
}
# スムージング
if smooth_method == 'savgol':
window_length = min(21, len(absorbance) // 10 * 2 + 1)
smoothed = savgol_filter(absorbance, window_length, polyorder=3)
elif smooth_method == 'gaussian':
smoothed = gaussian_filter1d(absorbance, sigma=2)
else:
smoothed = absorbance.copy()
processed['smoothed'] = smoothed
# ベースライン補正
if baseline_correct:
baseline = baseline_als(smoothed)
corrected = smoothed - baseline
processed['baseline'] = baseline
processed['corrected'] = corrected
else:
corrected = smoothed
processed['corrected'] = corrected
# 正規化
if normalize:
max_abs = np.max(corrected)
if max_abs > 0:
normalized = corrected / max_abs
else:
normalized = corrected
processed['normalized'] = normalized
else:
processed['normalized'] = corrected
return processed
# ノイズとベースラインドリフトを持つスペクトルのシミュレーション
np.random.seed(42)
wavelength = np.linspace(200, 600, 500)
# 真のスペクトル(3つのピーク)
true_spectrum = (
0.8 * np.exp(-(wavelength - 280)**2 / (2 * 15**2)) +
0.5 * np.exp(-(wavelength - 350)**2 / (2 * 20**2)) +
0.3 * np.exp(-(wavelength - 450)**2 / (2 * 25**2))
)
# ベースラインドリフト
baseline_drift = 0.1 + 0.0005 * (wavelength - 200) + 0.00001 * (wavelength - 200)**2
# ノイズ
noise = np.random.normal(0, 0.02, len(wavelength))
# 測定データ
measured = true_spectrum + baseline_drift + noise
# 前処理
result = preprocess_spectrum(wavelength, measured, smooth_method='savgol',
baseline_correct=True, normalize=True)
# プロット
fig, axes = plt.subplots(2, 2, figsize=(14, 10))
# 生データ vs スムージング後
ax1 = axes[0, 0]
ax1.plot(wavelength, measured, 'b-', alpha=0.5, linewidth=1, label='Raw data')
ax1.plot(wavelength, result['smoothed'], 'r-', linewidth=2, label='Smoothed (Savitzky-Golay)')
ax1.set_xlabel('Wavelength (nm)', fontsize=11)
ax1.set_ylabel('Absorbance', fontsize=11)
ax1.set_title('Step 1: Smoothing', fontsize=12, fontweight='bold')
ax1.legend()
ax1.grid(True, alpha=0.3)
# ベースライン推定
ax2 = axes[0, 1]
ax2.plot(wavelength, result['smoothed'], 'b-', linewidth=1.5, label='Smoothed data')
ax2.plot(wavelength, result['baseline'], 'g--', linewidth=2, label='Estimated baseline (ALS)')
ax2.plot(wavelength, baseline_drift, 'orange', linestyle=':', linewidth=2, label='True baseline')
ax2.set_xlabel('Wavelength (nm)', fontsize=11)
ax2.set_ylabel('Absorbance', fontsize=11)
ax2.set_title('Step 2: Baseline Estimation', fontsize=12, fontweight='bold')
ax2.legend()
ax2.grid(True, alpha=0.3)
# ベースライン補正後
ax3 = axes[1, 0]
ax3.plot(wavelength, result['corrected'], 'b-', linewidth=2, label='Baseline corrected')
ax3.plot(wavelength, true_spectrum, 'r--', linewidth=1.5, label='True spectrum')
ax3.set_xlabel('Wavelength (nm)', fontsize=11)
ax3.set_ylabel('Absorbance', fontsize=11)
ax3.set_title('Step 3: Baseline Correction', fontsize=12, fontweight='bold')
ax3.legend()
ax3.grid(True, alpha=0.3)
# 正規化後
ax4 = axes[1, 1]
ax4.plot(wavelength, result['normalized'], 'b-', linewidth=2, label='Processed & normalized')
ax4.plot(wavelength, true_spectrum / np.max(true_spectrum), 'r--', linewidth=1.5,
label='True (normalized)')
ax4.set_xlabel('Wavelength (nm)', fontsize=11)
ax4.set_ylabel('Normalized Absorbance', fontsize=11)
ax4.set_title('Step 4: Normalization', fontsize=12, fontweight='bold')
ax4.legend()
ax4.grid(True, alpha=0.3)
plt.tight_layout()
plt.savefig('spectrum_preprocessing.png', dpi=300, bbox_inches='tight')
plt.show()
# 処理品質の評価
correlation = np.corrcoef(result['normalized'],
true_spectrum / np.max(true_spectrum))[0, 1]
rmse = np.sqrt(np.mean((result['normalized'] - true_spectrum / np.max(true_spectrum))**2))
print("=" * 60)
print("前処理品質評価")
print("=" * 60)
print(f"真のスペクトルとの相関係数: {correlation:.4f}")
print(f"RMSE: {rmse:.4f}")
print(f"処理ステップ: スムージング -> ベースライン補正 -> 正規化")
6.2 多成分混合物のスペクトル分解
コード例8: 最小二乗法による多成分解析
import numpy as np
import matplotlib.pyplot as plt
from scipy.optimize import nnls
def multicomponent_analysis(wavelength, mixture_spectrum, reference_spectra,
method='nnls'):
"""
多成分混合物のスペクトル分解
Parameters:
-----------
wavelength : array
波長配列
mixture_spectrum : array
混合物の吸光度スペクトル
reference_spectra : dict
成分名をキー、参照スペクトルを値とする辞書
method : str
解析方法 ('nnls': 非負最小二乗法, 'lstsq': 通常の最小二乗法)
Returns:
--------
concentrations : dict
各成分の相対濃度
reconstructed : array
再構成スペクトル
residual : array
残差スペクトル
"""
# 参照スペクトル行列の構築
component_names = list(reference_spectra.keys())
A = np.column_stack([reference_spectra[name] for name in component_names])
# 解析
if method == 'nnls':
coefficients, residual_norm = nnls(A, mixture_spectrum)
else:
coefficients, residual_norm, _, _ = np.linalg.lstsq(A, mixture_spectrum, rcond=None)
# 結果の整理
concentrations = {name: coeff for name, coeff in zip(component_names, coefficients)}
# 再構成
reconstructed = A @ coefficients
residual = mixture_spectrum - reconstructed
return concentrations, reconstructed, residual
# 3成分混合物のシミュレーション
wavelength = np.linspace(200, 500, 400)
# 純成分の参照スペクトル
reference_spectra = {
'Component A': 0.8 * np.exp(-(wavelength - 250)**2 / (2 * 20**2)) + \
0.3 * np.exp(-(wavelength - 350)**2 / (2 * 15**2)),
'Component B': 0.6 * np.exp(-(wavelength - 280)**2 / (2 * 25**2)) + \
0.4 * np.exp(-(wavelength - 420)**2 / (2 * 30**2)),
'Component C': 0.9 * np.exp(-(wavelength - 320)**2 / (2 * 22**2))
}
# 既知の混合比
true_concentrations = {'Component A': 0.4, 'Component B': 0.35, 'Component C': 0.25}
# 混合スペクトルの生成
mixture_true = sum(conc * reference_spectra[name]
for name, conc in true_concentrations.items())
# ノイズ追加
np.random.seed(42)
mixture_measured = mixture_true + np.random.normal(0, 0.01, len(wavelength))
# 多成分解析
conc_estimated, reconstructed, residual = multicomponent_analysis(
wavelength, mixture_measured, reference_spectra, method='nnls'
)
# 正規化(合計を1に)
total_conc = sum(conc_estimated.values())
conc_normalized = {name: c / total_conc for name, c in conc_estimated.items()}
# プロット
fig = plt.figure(figsize=(16, 10))
# 参照スペクトル
ax1 = fig.add_subplot(2, 2, 1)
colors = ['#f093fb', '#f5576c', '#4ecdc4']
for (name, spectrum), color in zip(reference_spectra.items(), colors):
ax1.plot(wavelength, spectrum, linewidth=2, color=color, label=name)
ax1.set_xlabel('Wavelength (nm)', fontsize=11)
ax1.set_ylabel('Absorbance', fontsize=11)
ax1.set_title('Reference Spectra (Pure Components)', fontsize=12, fontweight='bold')
ax1.legend()
ax1.grid(True, alpha=0.3)
# 混合スペクトルと再構成
ax2 = fig.add_subplot(2, 2, 2)
ax2.plot(wavelength, mixture_measured, 'k-', linewidth=1.5, alpha=0.7, label='Measured mixture')
ax2.plot(wavelength, reconstructed, 'r--', linewidth=2, label='Reconstructed')
ax2.set_xlabel('Wavelength (nm)', fontsize=11)
ax2.set_ylabel('Absorbance', fontsize=11)
ax2.set_title('Mixture Spectrum Analysis', fontsize=12, fontweight='bold')
ax2.legend()
ax2.grid(True, alpha=0.3)
# 残差
ax3 = fig.add_subplot(2, 2, 3)
ax3.plot(wavelength, residual, 'g-', linewidth=1.5)
ax3.axhline(y=0, color='black', linestyle='-', linewidth=0.5)
ax3.fill_between(wavelength, residual, alpha=0.3, color='green')
ax3.set_xlabel('Wavelength (nm)', fontsize=11)
ax3.set_ylabel('Residual', fontsize=11)
ax3.set_title(f'Residual (RMSE = {np.sqrt(np.mean(residual**2)):.4f})',
fontsize=12, fontweight='bold')
ax3.grid(True, alpha=0.3)
# 濃度比較
ax4 = fig.add_subplot(2, 2, 4)
x = np.arange(len(true_concentrations))
width = 0.35
true_vals = [true_concentrations[name] for name in reference_spectra.keys()]
est_vals = [conc_normalized[name] for name in reference_spectra.keys()]
bars1 = ax4.bar(x - width/2, true_vals, width, label='True', color='#4ecdc4', edgecolor='black')
bars2 = ax4.bar(x + width/2, est_vals, width, label='Estimated', color='#f093fb', edgecolor='black')
ax4.set_ylabel('Relative Concentration', fontsize=11)
ax4.set_title('Component Concentration Comparison', fontsize=12, fontweight='bold')
ax4.set_xticks(x)
ax4.set_xticklabels([f'Comp. {c}' for c in ['A', 'B', 'C']])
ax4.legend()
ax4.set_ylim(0, 0.6)
ax4.grid(axis='y', alpha=0.3)
# 値をバーの上に表示
for bar, val in zip(bars1, true_vals):
ax4.text(bar.get_x() + bar.get_width()/2, bar.get_height() + 0.02,
f'{val:.2f}', ha='center', va='bottom', fontsize=10)
for bar, val in zip(bars2, est_vals):
ax4.text(bar.get_x() + bar.get_width()/2, bar.get_height() + 0.02,
f'{val:.2f}', ha='center', va='bottom', fontsize=10)
plt.tight_layout()
plt.savefig('multicomponent_analysis.png', dpi=300, bbox_inches='tight')
plt.show()
# 結果の表示
print("=" * 70)
print("多成分スペクトル解析結果")
print("=" * 70)
print(f"{'成分':<15} {'真の濃度':<15} {'推定濃度':<15} {'誤差 (%)':<10}")
print("-" * 70)
for name in reference_spectra.keys():
true_c = true_concentrations[name]
est_c = conc_normalized[name]
error = abs(true_c - est_c) / true_c * 100
print(f"{name:<15} {true_c:<15.3f} {est_c:<15.3f} {error:<10.1f}")
print("-" * 70)
print(f"再構成RMSE: {np.sqrt(np.mean(residual**2)):.4f}")
print(f"R^2: {1 - np.sum(residual**2) / np.sum((mixture_measured - np.mean(mixture_measured))**2):.4f}")
7. 演習問題
演習問題(クリックして展開)
Basic レベル(基礎理解)
問題1: 波長 450 nm の光の光子エネルギー(eV)を計算してください。
解答を見る
解答:
# コード例1の関数を使用
energy = wavelength_to_energy(450)
print(f"450 nm の光子エネルギー: {energy:.2f} eV")
# 出力: 約2.76 eV
問題2: モル吸光係数が 15000 L mol-1 cm-1、光路長が 1 cm のとき、吸光度 0.6 を示す溶液の濃度を求めてください。
解答を見る
解答:
ランベルト・ベール則より:
$$c = \frac{A}{\varepsilon l} = \frac{0.6}{15000 \times 1} = 4 \times 10^{-5} \text{ mol/L} = 40 \text{ }\mu\text{M}$$
問題3: n→pi* 遷移と pi→pi* 遷移のモル吸光係数の大きさの違いを説明してください。
解答を見る
解答:
n→pi* 遷移は軌道の対称性の観点から禁制遷移(または弱い許容遷移)であり、モル吸光係数は通常 10-100 L mol-1 cm-1 程度と小さい。一方、pi→pi* 遷移は許容遷移であり、モル吸光係数は 103-105 L mol-1 cm-1 と大きい。
Medium レベル(実践的計算)
問題4: 共役ジエンが長くなると吸収極大が長波長側にシフトする理由を、分子軌道論の観点から説明してください。
解答を見る
解答:
共役系が長くなると、HOMO(最高被占分子軌道)のエネルギーは上昇し、LUMO(最低空分子軌道)のエネルギーは低下します。その結果、HOMO-LUMOギャップが小さくなり、励起に必要なエネルギーが減少します。エネルギーが小さくなると、吸収する光の波長は長くなります(深色シフト/レッドシフト)。
問題5: TiO2(アナターゼ相)のバンドギャップを Tauc プロットから決定する場合、n の値は 2 と 1/2 のどちらを使用すべきですか?理由も説明してください。
解答を見る
解答:
TiO2(アナターゼ相)は間接遷移型半導体であるため、n = 1/2 を使用します。間接遷移では、電子の遷移にフォノンの関与が必要であり、遷移確率が直接遷移より低いため、吸収端の立ち上がりが緩やかになります。
問題6: ある有機色素の水溶液で、濃度を 2 倍にしても吸光度が 2 倍にならなかった。考えられる原因を 2 つ挙げてください。
解答を見る
解答:
- 化学的要因: 高濃度での色素分子の会合(二量体形成など)により、モル吸光係数が変化した。
- 機器的要因: 吸光度が 1.0 を超えると、透過光強度が弱くなり、検出器のS/N比が低下して正確な測定ができなくなる(ビール則からの機器的偏差)。
Hard レベル(高度な解析)
問題7: 以下の Python コードを完成させ、3 成分混合物のスペクトルから各成分の濃度を決定してください。
解答を見る(完全なコード)
# コード例8 を参照
# multicomponent_analysis 関数を使用して解析
# 参照スペクトルを用意(純成分の単位濃度当たりの吸光度)
reference_spectra = {
'Dye A': spectrum_A, # 事前に測定
'Dye B': spectrum_B,
'Dye C': spectrum_C
}
# 未知混合物のスペクトル
mixture_spectrum = measured_data # 測定データ
# 解析実行
concentrations, reconstructed, residual = multicomponent_analysis(
wavelength, mixture_spectrum, reference_spectra, method='nnls'
)
# 結果表示
for name, conc in concentrations.items():
print(f"{name}: {conc:.4f}")
問題8: Kubelka-Munk 変換が必要な理由と、その限界について説明してください。
解答を見る
解答:
必要な理由: 粉末試料や不透明試料では透過率を直接測定できないため、拡散反射率から吸収係数を推定する必要があります。Kubelka-Munk関数 F(R) = (1-R)2/(2R) は、吸収係数と散乱係数の比に比例し、吸収スペクトルの近似となります。
限界:
- 試料の散乱係数が波長に依存する場合、正確な吸収スペクトルが得られない
- 粒子サイズが不均一な場合、散乱特性が変化する
- 高吸収領域では精度が低下する
- 参照標準(BaSO4など)の品質に結果が依存する
問題9: 半導体ナノ粒子のサイズが小さくなるとバンドギャップが大きくなる現象(量子閉じ込め効果)を UV-Vis スペクトルで確認する方法を説明してください。
解答を見る
解答:
量子閉じ込め効果により、ナノ粒子のサイズが減少すると、エネルギー準位の間隔が広がり、有効バンドギャップが増加します。UV-Vis スペクトルでは:
- 異なるサイズのナノ粒子懸濁液の吸収スペクトルを測定
- 吸収端(onset)の波長を比較:小さい粒子ほど短波長側にシフト(青方シフト)
- Tauc プロットからバンドギャップを定量:粒子サイズとバンドギャップの相関をプロット
- Brus の式と比較して、量子閉じ込め効果の大きさを評価
問題10: アセトンのカルボニル基の n→pi* 遷移が溶媒の極性によって青方シフト(浅色シフト)する理由を説明してください。
解答を見る
解答:
n→pi* 遷移では、基底状態の孤立電子対(n軌道)が励起状態ではpi*軌道に移動します。
- 基底状態: 孤立電子対は局在化しており、極性溶媒と強い水素結合や双極子-双極子相互作用を形成し、安定化されます
- 励起状態: 電子がpi*軌道に励起されると、孤立電子対の局在性が失われ、溶媒との相互作用による安定化が減少します
結果として、極性溶媒中では基底状態がより安定化され、遷移に必要なエネルギーが増加します。これにより、吸収波長が短波長側にシフト(青方シフト)します。
学習目標の確認
この章で学んだ内容を振り返り、以下の項目を確認してください。
基本理解
- 電子遷移の種類(sigma→sigma*, n→sigma*, pi→pi*, n→pi*)とそのエネルギー順序を説明できる
- ランベルト・ベール則を使って、吸光度・濃度・モル吸光係数を計算できる
- 発色団と助色団の違い、深色シフトと浅色シフトの意味を理解している
- UV-Vis分光光度計の基本構成を説明できる
実践スキル
- 検量線を作成し、未知試料の濃度を決定できる
- Taucプロットを用いて半導体のバンドギャップを決定できる
- スペクトルデータの前処理(スムージング、ベースライン補正)ができる
- 多成分混合物のスペクトル分解ができる
応用力
- 共役系の長さと吸収波長の関係を分子軌道論で説明できる
- ビール則からの偏差の原因を識別し、適切な測定条件を設定できる
- 直接遷移型と間接遷移型半導体のTaucプロットの違いを理解している
参考文献
- Skoog, D. A., Holler, F. J., Crouch, S. R. (2017). Principles of Instrumental Analysis (7th ed.). Cengage Learning, pp. 336-380 (UV-Vis spectroscopy), pp. 381-420 (molecular luminescence). - 分光分析の標準的教科書
- Perkampus, H. H. (1992). UV-VIS Spectroscopy and Its Applications. Springer-Verlag, pp. 15-45 (electronic transitions), pp. 68-95 (chromophores), pp. 120-150 (applications). - UV-Vis分光法の詳細な解説
- Tauc, J. (1968). Optical properties and electronic structure of amorphous Ge and Si. Materials Research Bulletin, 3(1), 37-46. DOI: 10.1016/0025-5408(68)90023-8 - Taucプロット法の原著論文
- Murphy, A. B. (2007). Band-gap determination from diffuse reflectance measurements of semiconductor films, and application to photoelectrochemical water-splitting. Solar Energy Materials and Solar Cells, 91(14), 1326-1337. DOI: 10.1016/j.solmat.2007.05.005 - バンドギャップ決定法の詳細な解説
- Kubelka, P., Munk, F. (1931). Ein Beitrag zur Optik der Farbanstriche. Zeitschrift fur technische Physik, 12, 593-601. - Kubelka-Munk理論の原著論文
- Woodward, R. B. (1941). Structure and the absorption spectra of alpha, beta-unsaturated ketones. Journal of the American Chemical Society, 63(4), 1123-1126. - Woodward則(共役系の吸収予測)
- SciPy 1.11 Documentation. scipy.optimize.nnls, scipy.signal.savgol_filter. Available at: https://docs.scipy.org/doc/scipy/reference/ - Pythonによるスペクトルデータ処理
次のステップ
第2章では、UV-Vis分光法の原理、ランベルト・ベール則、発色団・助色団、分光光度計の構成、Taucプロットによるバンドギャップ決定を学びました。Pythonを用いた検量線作成、スペクトル前処理、多成分解析の実践的スキルも習得しました。
第3章では、赤外分光法(IR/FTIR)を学びます。分子振動の理論、官能基の特性吸収、フーリエ変換赤外分光法の原理、高分子・有機材料の構造解析など、振動分光法の基礎から応用までをカバーします。
免責事項
- 本コンテンツは教育・研究・情報提供のみを目的としており、専門的な助言(法律・会計・技術的保証など)を提供するものではありません。
- 本コンテンツおよび付随するコード例は「現状有姿(AS IS)」で提供され、明示または黙示を問わず、商品性、特定目的適合性、権利非侵害、正確性・完全性、動作・安全性等いかなる保証もしません。
- 外部リンク、第三者が提供するデータ・ツール・ライブラリ等の内容・可用性・安全性について、作成者および東北大学は一切の責任を負いません。
- 本コンテンツの利用・実行・解釈により直接的・間接的・付随的・特別・結果的・懲罰的損害が生じた場合でも、適用法で許容される最大限の範囲で、作成者および東北大学は責任を負いません。
- 本コンテンツの内容は、予告なく変更・更新・提供停止されることがあります。
- 本コンテンツの著作権・ライセンスは明記された条件(例: CC BY 4.0)に従います。当該ライセンスは通常、無保証条項を含みます。