🌐 EN | 🇯🇵 JP | Last sync: 2026-07-05

Chapter 3: Phase Equilibria and Phase Diagrams

🎯 Learning Objectives

📖 Fundamentals of Phase Equilibria

Phase Equilibrium Conditions

Conditions for two phases α and β to be in equilibrium:

  • Thermal equilibrium: \(T^\alpha = T^\beta\)
  • Mechanical equilibrium: \(P^\alpha = P^\beta\)
  • Chemical equilibrium: \(\mu_i^\alpha = \mu_i^\beta\) (for each component i)

Here \(\mu_i\) is the chemical potential of component i.

Gibbs Phase Rule

The Gibbs phase rule determines the number of degrees of freedom of a system:

\[ F = C - P + 2 \]

Examples:

💻 Example 3.1: Vapor Pressure Curves from the Clausius-Clapeyron Equation

Clausius-Clapeyron Equation

Relation between pressure and temperature along a phase equilibrium curve:

\[ \frac{dP}{dT} = \frac{L}{T \Delta V} \]

Here \(L\) is the latent heat of the phase transition and \(\Delta V\) is the volume change associated with the transition.

For liquid-gas equilibrium (with \(V_{\text{gas}} \gg V_{\text{liquid}}\) and the ideal-gas approximation):

\[ \frac{d \ln P}{dT} = \frac{L_{\text{vap}}}{RT^2} \]

Integrating gives:

\[ \ln P = -\frac{L_{\text{vap}}}{RT} + C \]

Python Implementation: Computing Vapor Pressure Curves
import numpy as np import matplotlib.pyplot as plt from scipy.integrate import odeint from scipy.optimize import fsolve # Integrated form of the Clausius-Clapeyron equation def vapor_pressure_clausius_clapeyron(T, L_vap, R, P0, T0): """Vapor pressure from the Clausius-Clapeyron equation""" return P0 * np.exp(-L_vap / R * (1/T - 1/T0)) # Vapor pressure data for water R = 8.314 # J/(mol·K) L_vap_water = 40660 # J/mol (heat of vaporization at 100°C) T0_water = 373.15 # K (100°C) P0_water = 101325 # Pa (1 atm) # Temperature range T_range = np.linspace(273.15, 473.15, 200) # 0-200°C # Vapor pressure curve P_vapor_water = vapor_pressure_clausius_clapeyron(T_range, L_vap_water, R, P0_water, T0_water) # Another substance (ethanol) L_vap_ethanol = 38560 # J/mol T0_ethanol = 351.5 # K (78.3°C) P0_ethanol = 101325 # Pa P_vapor_ethanol = vapor_pressure_clausius_clapeyron(T_range, L_vap_ethanol, R, P0_ethanol, T0_ethanol) # Visualization fig, axes = plt.subplots(1, 2, figsize=(14, 6)) # Vapor pressure curves (linear scale) ax1 = axes[0] ax1.plot(T_range - 273.15, P_vapor_water / 1e5, 'b-', linewidth=2, label='H₂O') ax1.plot(T_range - 273.15, P_vapor_ethanol / 1e5, 'r-', linewidth=2, label='C₂H₅OH') ax1.axhline(1.0, color='gray', linestyle='--', alpha=0.5, label='1 atm') ax1.set_xlabel('Temperature (°C)') ax1.set_ylabel('Vapor pressure (bar)') ax1.set_title('Vapor Pressure Curves (Clausius-Clapeyron)') ax1.legend() ax1.grid(True, alpha=0.3) # Vapor pressure curves (log scale) ax2 = axes[1] ax2.semilogy(T_range - 273.15, P_vapor_water / 1e5, 'b-', linewidth=2, label='H₂O') ax2.semilogy(T_range - 273.15, P_vapor_ethanol / 1e5, 'r-', linewidth=2, label='C₂H₅OH') ax2.axhline(1.0, color='gray', linestyle='--', alpha=0.5, label='1 atm') ax2.set_xlabel('Temperature (°C)') ax2.set_ylabel('Vapor pressure (bar, log scale)') ax2.set_title('Vapor Pressure Curves (Log Plot)') ax2.legend() ax2.grid(True, alpha=0.3, which='both') plt.tight_layout() plt.savefig('phase_vapor_pressure_clausius_clapeyron.png', dpi=300, bbox_inches='tight') plt.show() # Boiling point calculation def boiling_point(P_target, L_vap, R, P0, T0): """Compute the boiling point at a specified pressure""" # Solve ln(P) = -L/(RT) + C for T T = 1 / (1/T0 - R/L_vap * np.log(P_target / P0)) return T # Boiling points at various pressures pressures = [0.5e5, 1.0e5, 2.0e5, 5.0e5] # Pa print("=== Boiling Point Prediction via Clausius-Clapeyron ===\n") print(f"{'Pressure (bar)':<15} {'Water b.p. (°C)':<20} {'Ethanol b.p. (°C)':<25}") print("-" * 60) for P in pressures: T_bp_water = boiling_point(P, L_vap_water, R, P0_water, T0_water) T_bp_ethanol = boiling_point(P, L_vap_ethanol, R, P0_ethanol, T0_ethanol) print(f"{P/1e5:<15.1f} {T_bp_water - 273.15:<20.2f} {T_bp_ethanol - 273.15:<25.2f}") # Comparison with the Antoine equation print("\n=== Comparison with Experiment (Water, 1 atm) ===") print(f"Clausius-Clapeyron: {boiling_point(101325, L_vap_water, R, P0_water, T0_water) - 273.15:.2f} °C") print(f"Experimental value: 100.00 °C") print("\nThe Clausius-Clapeyron equation is highly accurate over narrow temperature ranges")

💻 Example 3.2: van der Waals Isotherms and the Maxwell Construction

van der Waals Equation of State

\[ \left(P + \frac{a}{V^2}\right)(V - b) = RT \]

At temperatures below the critical point, the isotherms contain an unphysical region (\(\frac{\partial P}{\partial V} > 0\)).

Maxwell construction: Determines the pressure in the gas-liquid coexistence region by the equal-area rule

\[ \int_{V_L}^{V_G} P_{\text{vdW}}(V) dV = P_{\text{eq}}(V_G - V_L) \]

Python Implementation: van der Waals Isotherms and the Maxwell Construction
import numpy as np import matplotlib.pyplot as plt from scipy.optimize import fsolve, minimize # van der Waals pressure def P_vdw(V, T, a, b, R): """van der Waals equation of state""" return R * T / (V - b) - a / V**2 # van der Waals parameters for CO₂ R = 8.314e-6 # MPa·m³/(mol·K) a = 0.3658 # MPa·m⁶/mol² b = 4.267e-5 # m³/mol # Critical point T_c = 8 * a / (27 * R * b) P_c = a / (27 * b**2) V_c = 3 * b print(f"=== Critical Point of CO₂ ===") print(f"T_c = {T_c:.2f} K = {T_c - 273.15:.2f} °C") print(f"P_c = {P_c:.4f} MPa") print(f"V_c = {V_c * 1e6:.2f} cm³/mol\n") # Implementation of the Maxwell construction def maxwell_construction(T, a, b, R): """Compute the equilibrium pressure via the Maxwell construction""" # Find the extrema of the van der Waals isotherm def dP_dV(V): return -R * T / (V - b)**2 + 2 * a / V**3 # Spinodal points (dP/dV = 0) V_range = np.linspace(b * 1.1, 10 * b, 1000) candidates = [] for i in range(len(V_range) - 1): V1, V2 = V_range[i], V_range[i+1] if dP_dV(V1) * dP_dV(V2) < 0: V_spin = fsolve(dP_dV, (V1 + V2) / 2)[0] candidates.append(V_spin) if len(candidates) < 2: return None, None, None V_spin_min, V_spin_max = sorted(candidates)[:2] # Maxwell equal-area rule def area_difference(P_eq): # Area below P_eq minus area above def integrand_lower(V): return P_vdw(V, T, a, b, R) - P_eq from scipy.integrate import quad area_lower, _ = quad(integrand_lower, V_spin_min, V_spin_max) return area_lower # Search for P_eq P_min = P_vdw(V_spin_max, T, a, b, R) P_max = P_vdw(V_spin_min, T, a, b, R) P_eq = fsolve(area_difference, (P_min + P_max) / 2)[0] # Find the liquid and gas volumes def find_volumes(P_eq): V_liquid = fsolve(lambda V: P_vdw(V, T, a, b, R) - P_eq, V_spin_min)[0] V_gas = fsolve(lambda V: P_vdw(V, T, a, b, R) - P_eq, V_spin_max)[0] return V_liquid, V_gas V_L, V_G = find_volumes(P_eq) return P_eq, V_L, V_G # Isotherms at multiple temperatures temperatures = [280, 300, T_c, 320, 350] # K colors = ['blue', 'green', 'red', 'orange', 'purple'] fig, ax = plt.subplots(figsize=(10, 8)) V_plot = np.linspace(b * 1.05, 10 * b, 1000) for T, color in zip(temperatures, colors): P_plot = [P_vdw(V, T, a, b, R) for V in V_plot] if T < T_c: label = f'T = {T:.0f} K (< T_c)' ax.plot(V_plot * 1e6, P_plot, color=color, linestyle='--', linewidth=1.5, alpha=0.5) # Maxwell construction P_eq, V_L, V_G = maxwell_construction(T, a, b, R) if P_eq is not None: ax.plot([V_L * 1e6, V_G * 1e6], [P_eq, P_eq], color=color, linewidth=2.5, label=label) elif T == T_c: label = f'T = {T:.0f} K (= T_c)' ax.plot(V_plot * 1e6, P_plot, color=color, linewidth=2.5, label=label) else: label = f'T = {T:.0f} K (> T_c)' ax.plot(V_plot * 1e6, P_plot, color=color, linewidth=2, label=label) # Mark the critical point ax.plot(V_c * 1e6, P_c, 'ko', markersize=10, label='Critical point') ax.set_xlabel('Molar volume (cm³/mol)') ax.set_ylabel('Pressure (MPa)') ax.set_title('van der Waals Isotherms and Maxwell Construction (CO₂)') ax.set_xlim([0, 500]) ax.set_ylim([0, 15]) ax.legend() ax.grid(True, alpha=0.3) plt.tight_layout() plt.savefig('phase_vdw_maxwell_construction.png', dpi=300, bbox_inches='tight') plt.show() # Numerical results of the Maxwell construction print("=== Gas-Liquid Equilibrium via Maxwell Construction ===\n") print(f"{'Temp. (K)':<12} {'Equil. pressure (MPa)':<18} {'Liquid V (cm³/mol)':<20} {'Gas V (cm³/mol)':<20}") print("-" * 70) for T in [280, 290, 300]: P_eq, V_L, V_G = maxwell_construction(T, a, b, R) if P_eq is not None: print(f"{T:<12.0f} {P_eq:<18.4f} {V_L*1e6:<20.2f} {V_G*1e6:<20.2f}")

💻 Example 3.3: Drawing a Binary Phase Diagram (Eutectic Type)

Classification of Binary Phase Diagrams

  • Eutectic: A eutectic point with the lowest melting temperature exists
  • Peritectic: Two solid phases form from the liquid phase
  • Complete solid solution: A solid solution forms over the entire composition range
  • Monotectic: Liquid-phase separation occurs
Python Implementation: Eutectic Binary Phase Diagram
import numpy as np import matplotlib.pyplot as plt # Simplified model of the Pb-Sn system (eutectic type) # Simplified phase diagram based on experimental data def pb_sn_phase_diagram(): """Phase diagram data for the Pb-Sn eutectic system""" # Composition (Sn atomic fraction) x_Sn = np.array([0.0, 0.1, 0.183, 0.183, 0.5, 0.619, 0.619, 0.8, 1.0]) # Liquidus T_liquidus = np.array([327, 300, 183, 183, 220, 183, 183, 210, 232]) # °C # Solidus - α phase (Pb-rich) x_alpha = np.array([0.0, 0.05, 0.183]) T_alpha = np.array([327, 250, 183]) # Solidus - β phase (Sn-rich) x_beta = np.array([0.619, 0.8, 1.0]) T_beta = np.array([183, 210, 232]) # Eutectic temperature T_eutectic = 183 # °C x_eutectic = 0.619 # Sn 61.9% return { 'liquidus': (x_Sn, T_liquidus), 'alpha_solidus': (x_alpha, T_alpha), 'beta_solidus': (x_beta, T_beta), 'eutectic_T': T_eutectic, 'eutectic_x': x_eutectic } # Draw the phase diagram phase_data = pb_sn_phase_diagram() fig, ax = plt.subplots(figsize=(10, 8)) # Liquidus x_liq, T_liq = phase_data['liquidus'] ax.plot(x_liq * 100, T_liq, 'r-', linewidth=2.5, label='Liquidus') # α-phase solidus x_alpha, T_alpha = phase_data['alpha_solidus'] ax.plot(x_alpha * 100, T_alpha, 'b-', linewidth=2.5, label='α-phase solidus (Pb-rich)') # β-phase solidus x_beta, T_beta = phase_data['beta_solidus'] ax.plot(x_beta * 100, T_beta, 'g-', linewidth=2.5, label='β-phase solidus (Sn-rich)') # Eutectic line T_e = phase_data['eutectic_T'] ax.axhline(T_e, color='orange', linestyle='--', linewidth=2, label=f'Eutectic temperature ({T_e}°C)') # Eutectic point x_e = phase_data['eutectic_x'] ax.plot(x_e * 100, T_e, 'ko', markersize=12, label=f'Eutectic point (Sn {x_e*100:.1f}%)') # Region labels ax.text(10, 280, 'L (liquid)', fontsize=14, ha='center', bbox=dict(boxstyle='round', facecolor='white', alpha=0.8)) ax.text(10, 210, 'L + α', fontsize=12, ha='center') ax.text(70, 210, 'L + β', fontsize=12, ha='center') ax.text(10, 150, 'α (Pb-rich solid solution)', fontsize=12, ha='center') ax.text(70, 150, 'β (Sn-rich solid solution)', fontsize=12, ha='center') ax.text(30, 170, 'α + β (eutectic microstructure)', fontsize=11, ha='center') ax.set_xlabel('Sn concentration (at%)', fontsize=12) ax.set_ylabel('Temperature (°C)', fontsize=12) ax.set_title('Pb-Sn Binary Phase Diagram (Eutectic Type)', fontsize=14, fontweight='bold') ax.set_xlim([0, 100]) ax.set_ylim([100, 350]) ax.legend(loc='upper right', fontsize=10) ax.grid(True, alpha=0.3) plt.tight_layout() plt.savefig('phase_diagram_pb_sn_eutectic.png', dpi=300, bbox_inches='tight') plt.show() # Print phase diagram information print("=== Pb-Sn Eutectic Phase Diagram ===\n") print(f"Eutectic point: Sn {x_e*100:.1f}%, temperature {T_e}°C") print(f"Melting point of Pb: 327°C") print(f"Melting point of Sn: 232°C") print(f"Lowest melting point: {T_e}°C (eutectic temperature)") print("\nApplication: solder (Sn-37Pb, close to the eutectic composition, is traditional)")

💻 Example 3.4: Lever Rule Calculations

Lever Rule

Rule for determining the amount of each phase in a two-phase coexistence region:

When an alloy of composition \(x_0\) separates at temperature \(T\) into an α phase (composition \(x_\alpha\)) and a β phase (composition \(x_\beta\)):

\[ \frac{f_\alpha}{f_\beta} = \frac{x_\beta - x_0}{x_0 - x_\alpha} \]

Phase fractions:

\[ f_\alpha = \frac{x_\beta - x_0}{x_\beta - x_\alpha}, \quad f_\beta = \frac{x_0 - x_\alpha}{x_\beta - x_\alpha} \]

Python Implementation: Lever Rule Calculation and Visualization
import numpy as np import matplotlib.pyplot as plt def lever_rule(x0, x_alpha, x_beta): """Compute phase fractions with the lever rule Args: x0: overall composition x_alpha: α-phase composition x_beta: β-phase composition Returns: f_alpha, f_beta: fraction of each phase """ f_beta = (x0 - x_alpha) / (x_beta - x_alpha) f_alpha = 1 - f_beta return f_alpha, f_beta # Concrete example in the Pb-Sn system # Overall composition: Sn 40%, α + β region at 200°C x0 = 0.40 # Sn 40% T = 200 # °C # Phase compositions at this temperature (read from the phase diagram) x_alpha = 0.15 # Sn concentration of the α phase (Pb-rich) x_beta = 0.90 # Sn concentration of the β phase (Sn-rich) # Lever rule calculation f_alpha, f_beta = lever_rule(x0, x_alpha, x_beta) print("=== Application of the Lever Rule ===\n") print(f"System: Pb-Sn alloy, overall composition Sn {x0*100:.0f}%, temperature {T}°C") print(f"α-phase composition: Sn {x_alpha*100:.0f}% (Pb-rich)") print(f"β-phase composition: Sn {x_beta*100:.0f}% (Sn-rich)") print(f"\nPhase fractions:") print(f" α phase: {f_alpha*100:.2f}%") print(f" β phase: {f_beta*100:.2f}%") print(f"\nCheck: f_α + f_β = {f_alpha + f_beta:.4f} (should be 1.000)") # Visualization fig, axes = plt.subplots(1, 2, figsize=(14, 6)) # Lever diagram ax1 = axes[0] ax1.plot([x_alpha, x_beta], [1, 1], 'ko-', linewidth=2, markersize=10) ax1.plot(x0, 1, 'r^', markersize=15, label=f'Overall composition (Sn {x0*100:.0f}%)') # Lever arms ax1.plot([x_alpha, x0], [0.95, 0.95], 'b-', linewidth=3, label=f'β-side arm (L_β)') ax1.plot([x0, x_beta], [0.95, 0.95], 'g-', linewidth=3, label=f'α-side arm (L_α)') ax1.text(x_alpha, 1.05, f'α phase\n(Sn {x_alpha*100:.0f}%)', ha='center', fontsize=11) ax1.text(x_beta, 1.05, f'β phase\n(Sn {x_beta*100:.0f}%)', ha='center', fontsize=11) ax1.text(x0, 0.85, f'f_α/f_β = L_β/L_α', ha='center', fontsize=12, color='red') ax1.set_xlim([0, 1]) ax1.set_ylim([0.7, 1.2]) ax1.set_xlabel('Sn concentration (atomic fraction)', fontsize=12) ax1.set_title('Illustration of the Lever Rule', fontsize=13, fontweight='bold') ax1.legend(loc='lower center', fontsize=10) ax1.axis('off') # Phase fractions vs. composition ax2 = axes[1] x0_range = np.linspace(x_alpha, x_beta, 100) f_alpha_range = [] f_beta_range = [] for x in x0_range: f_a, f_b = lever_rule(x, x_alpha, x_beta) f_alpha_range.append(f_a) f_beta_range.append(f_b) ax2.plot(x0_range * 100, np.array(f_alpha_range) * 100, 'b-', linewidth=2.5, label='α-phase fraction') ax2.plot(x0_range * 100, np.array(f_beta_range) * 100, 'g-', linewidth=2.5, label='β-phase fraction') ax2.axvline(x0 * 100, color='r', linestyle='--', linewidth=1.5, label=f'Example: Sn {x0*100:.0f}%') ax2.set_xlabel('Overall composition Sn (at%)', fontsize=12) ax2.set_ylabel('Phase fraction (%)', fontsize=12) ax2.set_title(f'Composition Dependence of Phase Fractions (T = {T}°C)', fontsize=13, fontweight='bold') ax2.legend(fontsize=10) ax2.grid(True, alpha=0.3) plt.tight_layout() plt.savefig('phase_lever_rule.png', dpi=300, bbox_inches='tight') plt.show() # Verify mass conservation print("\n=== Mass Conservation Check ===") x_average = f_alpha * x_alpha + f_beta * x_beta print(f"f_α · x_α + f_β · x_β = {f_alpha:.4f} × {x_alpha:.2f} + {f_beta:.4f} × {x_beta:.2f}") print(f" = {x_average:.4f}") print(f"Overall composition x₀ = {x0:.4f}") print(f"Error = {abs(x_average - x0):.6f}")

💻 Example 3.5: Analysis of Eutectic and Peritectic Points

Python Implementation: Visualizing Eutectic and Peritectic Reactions
import numpy as np import matplotlib.pyplot as plt def plot_eutectic_peritectic(): """Comparison of eutectic and peritectic reactions""" fig, axes = plt.subplots(1, 2, figsize=(14, 6)) # Eutectic type ax1 = axes[0] # Liquidus x_L_left = np.linspace(0, 0.4, 50) T_L_left = 400 - 200 * x_L_left x_L_right = np.linspace(0.4, 1.0, 50) T_L_right = 320 + 80 * (x_L_right - 0.4) ax1.plot(x_L_left * 100, T_L_left, 'r-', linewidth=2.5) ax1.plot(x_L_right * 100, T_L_right, 'r-', linewidth=2.5, label='Liquidus') # Solidus ax1.plot([0, 20], [400, 320], 'b-', linewidth=2.5, label='α-phase solidus') ax1.plot([70, 100], [320, 400], 'g-', linewidth=2.5, label='β-phase solidus') # Eutectic line ax1.plot([0, 100], [320, 320], 'orange', linestyle='--', linewidth=2) # Eutectic point ax1.plot(40, 320, 'ko', markersize=12) ax1.text(40, 310, 'Eutectic point E', ha='center', fontsize=12, fontweight='bold') # Reaction equation ax1.text(50, 380, 'L → α + β', fontsize=14, ha='center', bbox=dict(boxstyle='round', facecolor='yellow', alpha=0.7)) # Region labels ax1.text(20, 370, 'L', fontsize=14, ha='center') ax1.text(10, 340, 'L+α', fontsize=11, ha='center') ax1.text(60, 340, 'L+β', fontsize=11, ha='center') ax1.text(30, 290, 'α+β', fontsize=12, ha='center') ax1.set_xlabel('Composition B (at%)', fontsize=12) ax1.set_ylabel('Temperature (°C)', fontsize=12) ax1.set_title('Eutectic Type', fontsize=14, fontweight='bold') ax1.set_xlim([0, 100]) ax1.set_ylim([280, 420]) ax1.legend(loc='upper right', fontsize=10) ax1.grid(True, alpha=0.3) # Peritectic type ax2 = axes[1] # Liquidus x_L = np.linspace(0, 1.0, 100) T_L = 500 - 150 * x_L ax2.plot(x_L * 100, T_L, 'r-', linewidth=2.5, label='Liquidus') # α-phase solidus ax2.plot([0, 30], [500, 420], 'b-', linewidth=2.5, label='α-phase solidus') # β-phase solidus ax2.plot([50, 100], [420, 350], 'g-', linewidth=2.5, label='β-phase solidus') # Peritectic line ax2.plot([0, 100], [420, 420], 'purple', linestyle='--', linewidth=2) # Peritectic point ax2.plot(40, 420, 'ko', markersize=12) ax2.text(40, 410, 'Peritectic point P', ha='center', fontsize=12, fontweight='bold') # Reaction equation ax2.text(50, 470, 'L + α → β', fontsize=14, ha='center', bbox=dict(boxstyle='round', facecolor='lightblue', alpha=0.7)) # Region labels ax2.text(20, 460, 'L', fontsize=14, ha='center') ax2.text(15, 440, 'L+α', fontsize=11, ha='center') ax2.text(60, 440, 'L+β', fontsize=11, ha='center') ax2.text(15, 390, 'α', fontsize=12, ha='center') ax2.text(70, 390, 'β', fontsize=12, ha='center') ax2.text(35, 390, 'α+β', fontsize=11, ha='center') ax2.set_xlabel('Composition B (at%)', fontsize=12) ax2.set_ylabel('Temperature (°C)', fontsize=12) ax2.set_title('Peritectic Type', fontsize=14, fontweight='bold') ax2.set_xlim([0, 100]) ax2.set_ylim([340, 520]) ax2.legend(loc='upper right', fontsize=10) ax2.grid(True, alpha=0.3) plt.tight_layout() plt.savefig('phase_eutectic_peritectic_comparison.png', dpi=300, bbox_inches='tight') plt.show() plot_eutectic_peritectic() # Explanation of the reactions print("=== Comparison of Eutectic and Peritectic Reactions ===\n") print("[Eutectic reaction]") print(" Reaction: L → α + β") print(" - The liquid phase separates into two solid phases on cooling") print(" - Has the lowest melting point (melting point depression)") print(" - Examples: Pb-Sn (183°C), Al-Si (577°C)") print(" - Applications: solder, casting alloys") print() print("[Peritectic reaction]") print(" Reaction: L + α → β") print(" - The liquid phase reacts with solid phase α to form solid phase β") print(" - In non-equilibrium solidification, residual α tends to remain") print(" - Examples: Fe-C (1493°C), Pt-Ag") print(" - Challenges: homogenization is difficult; segregation occurs easily") print() print("[Checking the Gibbs phase rule]") print(" Eutectic/peritectic point: C = 2, P = 3 → F = 2 - 3 + 2 = 1") print(" At fixed pressure F = 0 → both temperature and composition are fixed (invariant system)")

💻 Example 3.6: Visualizing Ternary Phase Diagrams

Python Implementation: Ternary Phase Diagram (Isothermal Section)
import numpy as np import matplotlib.pyplot as plt from matplotlib.patches import Polygon from matplotlib.collections import PatchCollection def ternary_to_cartesian(a, b, c): """Convert ternary composition coordinates to Cartesian coordinates Args: a, b, c: atomic fraction of each component (a + b + c = 1) Returns: x, y: Cartesian coordinates """ x = 0.5 * (2 * b + c) / (a + b + c) y = (np.sqrt(3) / 2) * c / (a + b + c) return x, y def plot_ternary_diagram(): """Basic template for a ternary phase diagram""" fig, ax = plt.subplots(figsize=(10, 9)) # Vertices of the triangle vertices = np.array([ [0, 0], # A (bottom left) [1, 0], # B (bottom right) [0.5, np.sqrt(3)/2] # C (top) ]) # Triangle frame triangle = Polygon(vertices, fill=False, edgecolor='black', linewidth=2) ax.add_patch(triangle) # Grid lines (10% increments) for i in range(1, 10): frac = i / 10 # Parallel to the A-B axis (constant fraction of C) x1, y1 = ternary_to_cartesian(1-frac, 0, frac) x2, y2 = ternary_to_cartesian(0, 1-frac, frac) ax.plot([x1, x2], [y1, y2], 'gray', linewidth=0.5, alpha=0.5) # Parallel to the B-C axis (constant fraction of A) x1, y1 = ternary_to_cartesian(frac, 1-frac, 0) x2, y2 = ternary_to_cartesian(frac, 0, 1-frac) ax.plot([x1, x2], [y1, y2], 'gray', linewidth=0.5, alpha=0.5) # Parallel to the C-A axis (constant fraction of B) x1, y1 = ternary_to_cartesian(1-frac, frac, 0) x2, y2 = ternary_to_cartesian(0, frac, 1-frac) ax.plot([x1, x2], [y1, y2], 'gray', linewidth=0.5, alpha=0.5) # Vertex labels ax.text(0, -0.05, 'A', fontsize=16, ha='center', fontweight='bold') ax.text(1, -0.05, 'B', fontsize=16, ha='center', fontweight='bold') ax.text(0.5, np.sqrt(3)/2 + 0.05, 'C', fontsize=16, ha='center', fontweight='bold') # Axis labels for i in [0.2, 0.4, 0.6, 0.8]: # A axis x, y = ternary_to_cartesian(1-i, i, 0) ax.text(x, y - 0.03, f'{int(i*100)}', fontsize=9, ha='center', color='blue') # B axis x, y = ternary_to_cartesian(i, 0, 1-i) ax.text(x + 0.03, y, f'{int(i*100)}', fontsize=9, ha='left', color='green') # C axis x, y = ternary_to_cartesian(0, 1-i, i) ax.text(x - 0.03, y, f'{int(i*100)}', fontsize=9, ha='right', color='red') # Example: draw single-phase and two-phase regions (mock data) # Single-phase region α (A-rich) alpha_region = np.array([ ternary_to_cartesian(1.0, 0.0, 0.0), ternary_to_cartesian(0.8, 0.2, 0.0), ternary_to_cartesian(0.8, 0.0, 0.2), ]) alpha_patch = Polygon(alpha_region, facecolor='lightblue', edgecolor='blue', linewidth=1.5, alpha=0.5, label='α phase') ax.add_patch(alpha_patch) ax.text(0.85, 0.05, 'α', fontsize=14, ha='center', fontweight='bold') # Single-phase region β (B-rich) beta_region = np.array([ ternary_to_cartesian(0.0, 1.0, 0.0), ternary_to_cartesian(0.2, 0.8, 0.0), ternary_to_cartesian(0.0, 0.8, 0.2), ]) beta_patch = Polygon(beta_region, facecolor='lightgreen', edgecolor='green', linewidth=1.5, alpha=0.5, label='β phase') ax.add_patch(beta_patch) ax.text(0.15, 0.05, 'β', fontsize=14, ha='center', fontweight='bold') # Single-phase region γ (C-rich) gamma_region = np.array([ ternary_to_cartesian(0.0, 0.0, 1.0), ternary_to_cartesian(0.2, 0.0, 0.8), ternary_to_cartesian(0.0, 0.2, 0.8), ]) gamma_patch = Polygon(gamma_region, facecolor='lightcoral', edgecolor='red', linewidth=1.5, alpha=0.5, label='γ phase') ax.add_patch(gamma_patch) ax.text(0.5, 0.75, 'γ', fontsize=14, ha='center', fontweight='bold') # Two-phase region ax.text(0.5, 0.4, 'α+β+γ\n(three-phase region)', fontsize=12, ha='center', bbox=dict(boxstyle='round', facecolor='yellow', alpha=0.6)) ax.set_xlim([-0.1, 1.1]) ax.set_ylim([-0.15, 1.0]) ax.set_aspect('equal') ax.axis('off') ax.set_title('Ternary Phase Diagram (A-B-C System, Isothermal Section)', fontsize=15, fontweight='bold', pad=20) ax.legend(loc='upper right', fontsize=11) plt.tight_layout() plt.savefig('phase_ternary_diagram.png', dpi=300, bbox_inches='tight') plt.show() plot_ternary_diagram() # Example composition calculations for a ternary system print("=== Examples of Ternary Compositions ===\n") # Example: Al-Cu-Mg alloy system compositions = [ ("Al 90% - Cu 5% - Mg 5%", 0.90, 0.05, 0.05), ("Al 70% - Cu 20% - Mg 10%", 0.70, 0.20, 0.10), ("Al 50% - Cu 30% - Mg 20%", 0.50, 0.30, 0.20), ] print(f"{'Composition':<30} {'Al':<8} {'Cu':<8} {'Mg':<8} {'Total':<8}") print("-" * 65) for name, a, b, c in compositions: total = a + b + c print(f"{name:<30} {a:<8.2f} {b:<8.2f} {c:<8.2f} {total:<8.2f}") print("\nHow to read a ternary phase diagram:") print(" 1. The three vertices are the pure elements (A, B, C)") print(" 2. Each edge is a binary system") print(" 3. The interior represents ternary compositions") print(" 4. Gibbs phase rule: C = 3, T and P fixed → F = 3 - P") print(" - Single-phase region: F = 2 (two composition variables can be chosen freely)") print(" - Two-phase region: F = 1 (only one variable is free)") print(" - Three-phase region: F = 0 (compositions fixed; tie lines)")

💻 Example 3.7: Calculating Phase Transition Enthalpies

Python Implementation: Phase Transition Enthalpy and the Clapeyron Equation
import numpy as np import matplotlib.pyplot as plt from scipy.optimize import curve_fit # Experimental data (mock): vapor pressure of water T_exp = np.array([273.15, 283.15, 293.15, 303.15, 313.15, 323.15, 333.15, 343.15, 353.15, 363.15, 373.15]) # K P_exp = np.array([611, 1228, 2339, 4246, 7384, 12350, 19940, 31190, 47410, 70140, 101325]) # Pa # Fitting the Clausius-Clapeyron equation # ln(P) = -L_vap/(R·T) + C def clausius_clapeyron_fit(T, L_vap, C): """ln(P) = -L_vap/(R·T) + C""" R = 8.314 return -L_vap / (R * T) + C # Fitting params, cov = curve_fit(clausius_clapeyron_fit, T_exp, np.log(P_exp)) L_vap_fit, C_fit = params L_vap_err = np.sqrt(cov[0, 0]) print("=== Fitting the Clausius-Clapeyron Equation ===\n") print(f"Enthalpy of vaporization L_vap = {L_vap_fit:.0f} ± {L_vap_err:.0f} J/mol") print(f"Literature value (100°C): 40660 J/mol") print(f"Relative error: {abs(L_vap_fit - 40660) / 40660 * 100:.2f}%\n") # Visualize the fit results T_fit = np.linspace(270, 380, 100) P_fit = np.exp(clausius_clapeyron_fit(T_fit, L_vap_fit, C_fit)) fig, axes = plt.subplots(1, 2, figsize=(14, 6)) # Linear plot ax1 = axes[0] ax1.plot(T_exp - 273.15, P_exp / 1e3, 'bo', markersize=8, label='Experimental data') ax1.plot(T_fit - 273.15, P_fit / 1e3, 'r-', linewidth=2, label='Fit') ax1.set_xlabel('Temperature (°C)') ax1.set_ylabel('Vapor pressure (kPa)') ax1.set_title('Vapor Pressure Curve of Water') ax1.legend() ax1.grid(True, alpha=0.3) # Clausius-Clapeyron plot ax2 = axes[1] ax2.plot(1000 / T_exp, np.log(P_exp), 'bo', markersize=8, label='Experimental data') ax2.plot(1000 / T_fit, np.log(P_fit), 'r-', linewidth=2, label='Fit') ax2.set_xlabel('1000/T (K⁻¹)') ax2.set_ylabel('ln(P)') ax2.set_title('Clausius-Clapeyron Plot') ax2.legend() ax2.grid(True, alpha=0.3) plt.tight_layout() plt.savefig('phase_transition_enthalpy_fit.png', dpi=300, bbox_inches='tight') plt.show() # Comparison with other substances substances = { 'H₂O': 40660, 'C₂H₅OH (ethanol)': 38560, 'CH₃OH (methanol)': 35270, 'C₆H₆ (benzene)': 30720, 'N₂': 5577, 'O₂': 6820, } print("=== Comparison of Enthalpies of Vaporization ===\n") print(f"{'Substance':<20} {'L_vap (J/mol)':<18} {'L_vap (kJ/mol)':<18}") print("-" * 60) for substance, L in substances.items(): print(f"{substance:<20} {L:<18.0f} {L/1000:<18.2f}") print("\nEffect of hydrogen bonding:") print(" - Water has a large enthalpy of vaporization due to strong hydrogen bonds") print(" - Ethanol and methanol also form hydrogen bonds") print(" - N₂ and O₂ have weak intermolecular forces and small enthalpies of vaporization")

📚 Summary

💡 Practice Problems

  1. [Easy] Use the Gibbs phase rule to determine the degrees of freedom at the triple point of CO₂ (solid, liquid, and gas coexisting).
  2. [Easy] Use the lever rule to calculate the fraction of each phase when a Pb-Sn alloy with Sn 30% separates at 200°C into an α phase (Sn 15%) and a β phase (Sn 90%).
  3. [Medium] Given that the enthalpy of vaporization of water is 40.66 kJ/mol, calculate the vapor pressure at 90°C, assuming the vapor pressure at 100°C is 1 atm.
  4. [Medium] Show that at the critical point of a van der Waals gas \(\left(\frac{\partial P}{\partial V}\right)_T = \left(\frac{\partial^2 P}{\partial V^2}\right)_T = 0\), and from this derive \(T_c = \frac{8a}{27Rb}\).
  5. [Hard] For a binary eutectic system with eutectic composition 40%, eutectic temperature 300°C, and melting points of pure A and pure B of 500°C and 450°C respectively, determine the liquidus using a parabolic approximation and calculate the liquidus temperature of an alloy with composition 30%.

Disclaimer