thermoelasticsim.visualization.elastic.response_plotter 源代码

#!/usr/bin/env python3
"""
弹性常数响应曲线绘制器

从v7 system_size_comparison_v7.py提取的绘图功能,重构为模块化、可复用的组件。
支持C11、C12、C44等所有弹性常数的应力-应变响应可视化。

Author: Gilbert Young
Created: 2025-08-15
"""

from pathlib import Path

import numpy as np

from thermoelasticsim.utils.plot_config import plt


[文档] class ResponsePlotter: """ 弹性常数响应曲线绘制器 提供统一的绘图接口,支持: - C11/C12联合响应图 - C44/C55/C66剪切响应图 - 弹性常数对比图 - 收敛性分析图 Examples -------- >>> plotter = ResponsePlotter() >>> plotter.plot_c11_c12_response(c11_data, c12_data, 'output.png') >>> plotter.plot_shear_response(c44_data, 'shear_output.png') """
[文档] def __init__( self, dpi: int = 300, figsize_scale: float = 1.0, literature_values: dict[str, float] | None = None, ): """ 初始化绘图器 Parameters ---------- dpi : int 图像分辨率 figsize_scale : float 图像尺寸缩放因子 """ self.dpi = dpi self.figsize_scale = figsize_scale # 预定义颜色和标记 self.colors = { "C11": "#2E86C1", "C12": "#E74C3C", "C44": "#2E86C1", "C55": "#E74C3C", "C66": "#58D68D", } self.markers = {"C11": "o", "C12": "s", "C44": "o", "C55": "s", "C66": "^"} # 文献值 (GPa) - 默认采用铝,允许外部覆盖 default_lit = { "C11": 110.0, "C12": 61.0, "C44": 33.0, "C55": 33.0, "C66": 33.0, } if literature_values: default_lit.update(literature_values) self.literature_values = default_lit
[文档] def plot_c11_c12_combined_response( self, c11_data: list[dict], c12_data: list[dict], supercell_size: tuple[int, int, int], output_path: str, slope_override_c11: float | None = None, slope_override_c12: float | None = None, subtitle_c11: str | None = None, subtitle_c12: str | None = None, ) -> str: """ 生成C11/C12联合应力-应变响应关系图 从v7的plot_c11_c12_combined_response提取和重构 Parameters ---------- c11_data : List[Dict] C11数据点列表 c12_data : List[Dict] C12数据点列表 supercell_size : Tuple[int, int, int] 系统尺寸 output_path : str 输出文件路径 Returns ------- str 生成的图像文件名 """ fig, ((ax1, ax2), (ax3, ax4)) = plt.subplots( 2, 2, figsize=(16 * self.figsize_scale, 12 * self.figsize_scale), constrained_layout=True, ) # 准备C11数据 c11_strains = [row["applied_strain"] for row in c11_data] c11_stresses = [row["measured_stress_GPa"] for row in c11_data] c11_converged_states = [row["optimization_converged"] for row in c11_data] # 准备C12数据 c12_strains = [row["applied_strain"] for row in c12_data] c12_stresses = [row["measured_stress_GPa"] for row in c12_data] c12_converged_states = [row["optimization_converged"] for row in c12_data] # C11图 self._plot_stress_strain_scatter( ax1, c11_strains, c11_stresses, c11_converged_states, "C11", self.literature_values["C11"], "εxx", "σxx", subtitle_c11 or "C11: xx应变→xx应力", override_slope=slope_override_c11, ) # C12图 self._plot_stress_strain_scatter( ax2, c12_strains, c12_stresses, c12_converged_states, "C12", self.literature_values["C12"], "εxx", "σyy", subtitle_c12 or "C12: xx应变→yy应力", override_slope=slope_override_c12, ) # C11/C12对比图 self._plot_elastic_constant_bar( ax3, c11_strains, c11_stresses, c11_converged_states, "C11", self.literature_values["C11"], override_value=slope_override_c11, ) self._plot_elastic_constant_bar( ax4, c12_strains, c12_stresses, c12_converged_states, "C12", self.literature_values["C12"], override_value=slope_override_c12, ) plt.suptitle( f"C11/C12联合计算 - {supercell_size[0]}x{supercell_size[1]}x{supercell_size[2]}系统", fontsize=16 * self.figsize_scale, weight="bold", ) # 保存图片 filepath = Path(output_path) plt.savefig(filepath, dpi=self.dpi, bbox_inches="tight") plt.close() return filepath.name
[文档] def plot_shear_response( self, detailed_results: list[dict], supercell_size: tuple[int, int, int], output_path: str, ) -> str: """ 生成C44/C55/C66剪切应力-应变响应关系图 从v7的plot_stress_strain_response提取和重构 Parameters ---------- detailed_results : List[Dict] 详细的剪切响应结果 supercell_size : Tuple[int, int, int] 系统尺寸 output_path : str 输出文件路径 Returns ------- str 生成的图像文件名 """ fig = plt.figure( figsize=(18 * self.figsize_scale, 12 * self.figsize_scale), constrained_layout=True, ) # 创建2行2列子图布局 gs = fig.add_gridspec(2, 3, hspace=0.4, wspace=0.3, height_ratios=[3, 2]) ax_shear = [fig.add_subplot(gs[0, i]) for i in range(3)] # 上排3个剪切子图 ax_summary = fig.add_subplot(gs[1, :]) # 下排汇总图 # 准备数据 directions = ["yz(C44)", "xz(C55)", "xy(C66)"] colors = [self.colors["C44"], self.colors["C55"], self.colors["C66"]] markers = [self.markers["C44"], self.markers["C55"], self.markers["C66"]] literature_values = [ self.literature_values["C44"], self.literature_values["C55"], self.literature_values["C66"], ] # 为每个剪切模式单独绘制子图 elastic_constants = [] convergence_quality = [] for i, result in enumerate(detailed_results): ax = ax_shear[i] # 优先使用人类可读名称(如 "yz(C44)"),兼容整数方向码 direction = result.get("name") or result.get("direction") or directions[i] if isinstance(direction, int): direction = directions[i] color = colors[i] marker = markers[i] lit_value = literature_values[i] # 获取多点数据 strains = result["strains"] stresses = result["stresses"] converged_states = result["converged_states"] # 绘制散点图和拟合线 elastic_constant = self._plot_shear_scatter( ax, strains, stresses, converged_states, direction, color, marker, lit_value, ) elastic_constants.append(elastic_constant) convergence_quality.append(sum(converged_states) / len(converged_states)) # 下方汇总图:弹性常数对比 self._plot_elastic_constants_summary( ax_summary, directions, elastic_constants, convergence_quality, colors, literature_values[0], supercell_size, ) # 保存图片 filepath = Path(output_path) plt.savefig(filepath, dpi=self.dpi, bbox_inches="tight") plt.close() return filepath.name
def _plot_stress_strain_scatter( self, ax, strains: list[float], stresses: list[float], converged_states: list[bool], constant_name: str, literature_value: float, strain_label: str, stress_label: str, title: str, override_slope: float | None = None, ) -> float: """绘制应力-应变散点图和拟合线""" # 分别绘制收敛和不收敛的点 converged_strains = [ s for s, c in zip(strains, converged_states, strict=False) if c ] converged_stresses = [ st for st, c in zip(stresses, converged_states, strict=False) if c ] failed_strains = [ s for s, c in zip(strains, converged_states, strict=False) if not c ] failed_stresses = [ st for st, c in zip(stresses, converged_states, strict=False) if not c ] color = self.colors[constant_name] marker = self.markers[constant_name] # 收敛点:实心符号 if converged_strains: ax.scatter( converged_strains, converged_stresses, marker=marker, color=color, s=80, label=f"{constant_name} (收敛)", alpha=0.8, edgecolors="black", ) # 不收敛点:空心符号 if failed_strains: ax.scatter( failed_strains, failed_stresses, marker=marker, facecolors="none", edgecolors=color, s=80, label=f"{constant_name} (未收敛)", alpha=0.8, linewidth=2, ) # 添加文献值理论斜率参考线(自适应范围) if converged_strains: xmin, xmax = float(min(converged_strains)), float(max(converged_strains)) pad = 0.2 * (xmax - xmin if xmax > xmin else (abs(xmax) + 1e-12)) strain_range = np.linspace(xmin - pad, xmax + pad, 100) elif strains: xmin, xmax = float(min(strains)), float(max(strains)) pad = 0.2 * (xmax - xmin if xmax > xmin else (abs(xmax) + 1e-12)) strain_range = np.linspace(xmin - pad, xmax + pad, 100) else: strain_range = np.linspace(-0.003, 0.003, 100) theory_stress = literature_value * strain_range ax.plot( strain_range, theory_stress, "k:", linewidth=2, alpha=0.7, label=f"理论斜率 ({literature_value} GPa)", ) # 线性拟合(只用收敛点;包含零应变点,保持与旧版一致) # 为避免“仅两个点”导致的误导,这里要求至少3个点才绘制拟合线 fitted_constant = 0.0 if len(converged_strains) >= 3: xs = np.array(converged_strains, dtype=float) ys = np.array(converged_stresses, dtype=float) coeffs = np.polyfit(xs, ys, 1) fit_strains = np.linspace(float(xs.min()), float(xs.max()), 100) fit_stresses = np.polyval(coeffs, fit_strains) ax.plot( fit_strains, fit_stresses, "--", color=color, alpha=0.7, linewidth=2, label=f"拟合斜率 ({coeffs[0]:.1f} GPa)", ) fitted_constant = coeffs[0] # 计算R² y_pred = np.polyval(coeffs, xs) ss_res = np.sum((ys - y_pred) ** 2) ss_tot = np.sum((ys - np.mean(ys)) ** 2) r_squared = 1 - (ss_res / ss_tot) if ss_tot != 0 else 1.0 # 在图上显示拟合质量 ax.text( 0.05, 0.95, f"R² = {r_squared:.4f}", transform=ax.transAxes, fontsize=10, bbox=dict(boxstyle="round,pad=0.3", facecolor="white", alpha=0.8), ) # 覆盖斜率:绘制“常数”参考线(通过原点) if override_slope is not None and converged_strains: xs = np.array(converged_strains, dtype=float) xspan = np.linspace(float(xs.min()), float(xs.max()), 100) yspan = override_slope * xspan ax.plot( xspan, yspan, ":", color="tab:purple", alpha=0.9, linewidth=2, label=f"常数 ({override_slope:.1f} GPa)", ) ax.set_xlabel(strain_label, fontsize=12) ax.set_ylabel(f"{stress_label} (GPa)", fontsize=12) ax.set_title(title, fontsize=13) ax.grid(True, alpha=0.3) ax.legend(fontsize=10, loc="best") return fitted_constant def _plot_shear_scatter( self, ax, strains: list[float], stresses: list[float], converged_states: list[bool], direction: str, color: str, marker: str, lit_value: float, ) -> float: """绘制剪切模式的散点图""" # 分别绘制收敛和不收敛的点 converged_strains = [ s for s, c in zip(strains, converged_states, strict=False) if c ] converged_stresses = [ st for st, c in zip(stresses, converged_states, strict=False) if c ] failed_strains = [ s for s, c in zip(strains, converged_states, strict=False) if not c ] failed_stresses = [ st for st, c in zip(stresses, converged_states, strict=False) if not c ] # 收敛点:实心符号 if converged_strains: ax.scatter( converged_strains, converged_stresses, marker=marker, color=color, s=100, label="收敛点", alpha=0.8, edgecolors="black", linewidth=1, ) # 不收敛点:空心符号 if failed_strains: ax.scatter( failed_strains, failed_stresses, marker=marker, facecolors="none", edgecolors=color, s=100, label="未收敛", alpha=0.8, linewidth=2, ) # 添加文献值理论斜率参考线 if (hasattr(strains, "__len__") and len(strains) > 0) or ( not hasattr(strains, "__len__") and bool(strains) ): strain_range = np.linspace(min(strains) * 1.2, max(strains) * 1.2, 100) theory_stress = lit_value * strain_range ax.plot( strain_range, theory_stress, "k:", linewidth=2, alpha=0.7, label=f"理论斜率 ({lit_value} GPa)", ) # 只对收敛点进行线性拟合 elastic_constant = 0.0 if len(converged_strains) >= 2: coeffs = np.polyfit(converged_strains, converged_stresses, 1) fit_strains = np.linspace( min(converged_strains), max(converged_strains), 100 ) fit_stresses = np.polyval(coeffs, fit_strains) ax.plot( fit_strains, fit_stresses, "--", color=color, alpha=0.9, linewidth=3, label=f"拟合 ({coeffs[0]:.1f} GPa)", ) elastic_constant = coeffs[0] # 计算R² y_pred = np.polyval(coeffs, converged_strains) ss_res = np.sum((converged_stresses - y_pred) ** 2) ss_tot = np.sum((converged_stresses - np.mean(converged_stresses)) ** 2) r_squared = 1 - (ss_res / ss_tot) if ss_tot != 0 else 1.0 # 在图上显示拟合质量 ax.text( 0.05, 0.95, f"R² = {r_squared:.4f}", transform=ax.transAxes, fontsize=10, bbox=dict(boxstyle="round,pad=0.3", facecolor="white", alpha=0.8), ) # 设置子图属性 # 兼容 direction 为纯标签或包含括号注释 shear_component = ( direction.split("(")[0] if isinstance(direction, str) else str(direction) ) ax.set_xlabel(f"{shear_component}剪切应变", fontsize=12) ax.set_ylabel(f"{shear_component}剪切应力 (GPa)", fontsize=12) ax.set_title(f"{direction}", fontsize=14, weight="bold") ax.grid(True, alpha=0.3) ax.legend(fontsize=10, loc="best") return elastic_constant def _plot_elastic_constant_bar( self, ax, strains: list[float], stresses: list[float], converged_states: list[bool], constant_name: str, literature_value: float, override_value: float | None = None, ) -> float: """绘制单个弹性常数对比柱状图""" # 计算拟合值 converged_strains = [ s for s, c in zip(strains, converged_states, strict=False) if c ] converged_stresses = [ st for st, c in zip(stresses, converged_states, strict=False) if c ] fitted_value = 0.0 convergence_rate = sum(converged_states) / len(converged_states) if len(converged_strains) >= 2: coeffs = np.polyfit(converged_strains, converged_stresses, 1) fitted_value = coeffs[0] color = self.colors[constant_name] display_value = fitted_value if override_value is None else override_value bar = ax.bar( [constant_name], [display_value], color=color, alpha=0.3 + 0.7 * convergence_rate, edgecolor="black", linewidth=1, width=0.6, ) # 文献值参考线 ax.axhline( y=literature_value, color=color, linestyle="--", linewidth=2, alpha=0.7, label=f"文献值 ({literature_value} GPa)", ) # 数值标签 if display_value > 0: height = bar[0].get_height() ax.text( bar[0].get_x() + bar[0].get_width() / 2.0, height + max(height * 0.02, 2), f"{display_value:.1f}", ha="center", va="bottom", fontsize=14, weight="bold", ) # 误差 error = (display_value - literature_value) / literature_value * 100 ax.text( bar[0].get_x() + bar[0].get_width() / 2.0, height + max(height * 0.08, 8), f"({error:+.1f}%)", ha="center", va="bottom", fontsize=12, color="gray", ) # 收敛率 ax.text( bar[0].get_x() + bar[0].get_width() / 2.0, height / 2, f"{convergence_rate:.0%}", ha="center", va="center", fontsize=12, color="white", weight="bold", ) ax.set_ylabel("弹性常数 (GPa)", fontsize=12) ax.set_title(f"{constant_name}计算结果", fontsize=13) ax.grid(True, alpha=0.3, axis="y") ax.legend(fontsize=10) ax.set_ylim( 0, max(display_value if display_value > 0 else 0, literature_value) * 1.3 ) return display_value def _plot_elastic_constants_summary( self, ax, directions: list[str], elastic_constants: list[float], convergence_quality: list[float], colors: list[str], literature_value: float, supercell_size: tuple[int, int, int], ): """绘制弹性常数汇总对比图""" x_pos = np.arange(len(directions)) bars = ax.bar( x_pos, elastic_constants, color=colors, alpha=0.7, edgecolor="black", linewidth=1, ) # 根据收敛质量调整透明度 for bar, quality in zip(bars, convergence_quality, strict=False): bar.set_alpha(0.3 + 0.7 * quality) # 文献值参考线 ax.axhline( y=literature_value, color="red", linestyle="--", linewidth=2, label=f"文献值 ({literature_value} GPa)", ) # 添加数值标签和收敛质量 for i, (bar, value, quality) in enumerate( zip(bars, elastic_constants, convergence_quality, strict=False) ): height = bar.get_height() ax.text( bar.get_x() + bar.get_width() / 2.0, height + max(height * 0.02, 2), f"{value:.1f}", ha="center", va="bottom", fontsize=12, weight="bold", ) # 计算误差 error = (value - literature_value) / literature_value * 100 ax.text( bar.get_x() + bar.get_width() / 2.0, height + max(height * 0.08, 8), f"({error:+.0f}%)", ha="center", va="bottom", fontsize=10, color="gray", ) # 显示收敛率 ax.text( bar.get_x() + bar.get_width() / 2.0, height / 2, f"{quality:.0%}", ha="center", va="center", fontsize=10, color="white", weight="bold", ) ax.set_xlabel("剪切模式", fontsize=12) ax.set_ylabel("弹性常数 (GPa)", fontsize=12) ax.set_title( f"弹性常数汇总 - {supercell_size[0]}×{supercell_size[1]}×{supercell_size[2]}系统\n平均值: {np.mean(elastic_constants):.1f} GPa", fontsize=14, ) ax.set_xticks(x_pos) ax.set_xticklabels(directions) ax.grid(True, alpha=0.3, axis="y") ax.legend(fontsize=10) # 设置y轴范围确保能看到所有数据 max_val = max(max(elastic_constants), literature_value) ax.set_ylim(0, max_val * 1.3)