周三上午,增材制造实验室。
"这批件又翘边了,"工艺工程师小周拿着一盒刚从 SLS 设备上取下来的尼龙件,"三个角翘起来了,底面不是平面。我怀疑是温度场不均匀——激光烧结的时候,边缘区域温度降得太快,收缩应力把零件拉变形了。"
我看了眼设备的温度场记录文件。
"系统不是有温度监测吗?"我问。
"有,"小周点开软件,"设备有 36 个温区传感器,每层烧结完记录一次温度。一个零件 200 层,就是 7200 个温度值。但系统只给你看'当前层平均温度'和一条曲线——它不告诉你:零件中心的温区最高多少度、边缘的温区最低多少度、哪些区域长期低于工艺窗口下限。"
"所以你需要的不是'温度曲线',是'区域温度统计'?"
"对,"小周点头,"我把零件底面分成九个区域——四角、四边中点、中心。想知道每个区域在整个打印过程中(200 层)的最高温度、最低温度、温度波动幅度。比如四角区域最低温度只有 158°C,而工艺要求烧结温度不低于 160°C——那翘边的原因就找到了:角落温度不达标,粉材没充分烧结,强度不够,冷却收缩就把件拉起来了。"
"你能拿到原始温度采样数据吗?"
"能,"小周导出一份 CSV,"每行是一次采样:层号、温区编号、X坐标、Y坐标、温度值、采样时间。200 层 × 36 温区 = 7200 行。我需要按区域分组,算每个区域的 max、min、range、标准差。"
"明白了,"我打开 VS Code,"你需要一个程序:加载 3D 打印温度场采样数据,按空间区域(中心/边缘/四角)分组,统计每个区域在所有层中的最高、最低、平均温度,识别低于工艺窗口下限的区域。用 pandas 的 groupby + agg 做分组聚合,用 numpy 算统计量,用 matplotlib 画温度热力图和区域统计柱状图,用 scipy 做区域间温度差异的显著性检验。"
我敲了几行代码:
import pandas as pd
df = pd.read_csv("temperature_field.csv")
# 按区域分组统计
region_stats = df.groupby("region").agg({
"temperature_c": ["max", "min", "mean", "std"]
}).reset_index()
# 低于工艺窗口下限的区域
df["below_threshold"] = df["temperature_c"] < 160
"这只是核心逻辑,"我说,"完整版会用 OOP 封装:一个类管数据加载(解析层号、温区、坐标、温度值),一个类管区域划分(按坐标把温区归类为四角/边缘/中心),一个类管温度统计(按区域和按层双维度聚合),一个类管工艺窗口判定(识别超温/欠温区域),一个类管可视化(空间温度热力图、区域统计柱状图、温度时序曲线)。数据自包含——用 numpy 合成一份含 7200 个采样点的温度场数据,模拟中心热、边缘冷的梯度分布,读者下载就能跑。"
小周凑近屏幕:"所以你这东西,就是把 7200 个温度值按区域归类,算每个区域的最高最低——我一看就知道哪个角落温度不够,下次打印就把那个区域的预热温度调高,或者改扫描策略让激光多跑两圈。"
"对。而且还能做趋势分析,"我补充,"用 scipy 的 t-test 检验中心和边缘的温度差异是否显著。用 scikit-learn 做聚类,把温区按温度模式分组——是均匀冷区、均匀热区还是梯度过渡区。用 matplotlib 的 imshow 画出底面温度热力图,一眼看出'冷点'在哪。增材制造的温度场管理不是'看平均温度够不够',是'用区域统计找出冷点,把工艺窗口落实到空间位置'。"
一、实际应用场景(真实痛点)
场景设定:SLS/SLM 激光增材制造过程中,成型腔体被划分为多个温区,每层烧结完成后通过红外传感器或热电偶阵列记录各温区温度。一个典型打印件(200 层 × 36 温区 = 7200 采样点)产生大量温度场数据。工艺工程师需要按空间区域(中心/边缘/四角)统计温度极值,识别低于烧结工艺窗口下限的区域,定位翘曲、开裂等缺陷的温度成因。
现场原话(叙事化):
"我不是不知道温度重要,"小周说,"翘边、开裂、球化——这些缺陷全跟温度有关。设备也确实在测温度,每层都记录 36 个温区的值。但问题是,7200 个数字堆在一起,系统只给我画一条'平均温度曲线'。我看平均温度是 168°C,在工艺窗口内,但件还是翘了。后来我手动把四角的温度挑出来一看——四角最低只有 156°C,低于下限 160°C 整整四度,持续了前 50 层。平均温度掩盖了局部欠温,这就是根因。"
核心矛盾:"海量温度采样点的平面罗列"与"工程师需要的区域极值统计与缺陷归因"之间的分析断层。需要一个"3D 打印成型温度场区域统计与工艺窗口分析程序",用
"pandas" 按区域分组聚合,用
"numpy" 计算极值和波动,用
"matplotlib" 绘制空间热力图和统计柱状图,用
"scipy" 做区域差异检验,输出结构化的温度场质量报告。
二、痛点分析(映射到滨州职业学院《先进制造技术》课程模型)
《先进制造技术》模块 本篇痛点对应
增材制造(3D打印)技术:成型工艺参数与温度控制 温度场区域统计:定位欠温/超温区域。
先进制造技术基础:先进制造过程监控 多温区数据采集与分析:从点测量到区域统计。
智能制造与数字孪生:打印过程数字孪生 空间温度场映射:用热力图可视化物理成型状态。
先进制造新模式:零缺陷制造 缺陷预防:欠温区域预警与工艺窗口保障。
一句话总结:我们需要构建一个"3D 打印成型温度场区域统计程序",用
"pandas" 按空间区域分组聚合温度极值,用
"numpy" 计算波动幅度,用
"matplotlib" 绘制空间热力图和统计柱状图,用
"scipy" 做区域差异检验,实现从温度采样点到区域缺陷归因的转化。
三、核心逻辑讲解(大白话)
3.1 问题本质:把温度场想象成"地暖地板"
把 3D 打印的底面温度场想象成"冬天你家地暖的地面温度":
* 温区 = 地暖的分水器回路:每一路控制一块区域,共 36 路。
* 中心区域 = 客厅中间:离外墙远,保温好,温度高且均匀,维持在 168°C。
* 边缘区域 = 靠窗的那一圈:散热快,温度偏低,可能只有 162°C。
* 四角区域 = 房间最角落:三面散热,最冷,可能只有 156°C——这就是翘边高发区。
* 工艺窗口 = 舒适温度区间:要求 160~175°C。低于 160°C 的角落,粉材烧结不充分,强度不够。
* 按区域统计 = 找出最冷的角落:不看平均值,看每个区域的最低温度——只要最低值低于下限,那个区域就有风险。
工业应用:
* pandas groupby 双维度:
"df.groupby(["region", "layer"])["temperature_c"].agg(["max", "min", "mean"])" 按区域和层两个维度统计。
* numpy 区域归类:
"np.where()" 根据 X/Y 坐标把温区分为"四角/边缘/中心"。
* matplotlib imshow:把 6×6 温区阵列画成热力图,冷点一目了然。
3.2 业务逻辑 → 代码映射
定义温度场采样数据模型
│
▼ TempDataLoader (pandas)
导入温度场采样 CSV:
pd.read_csv("temperature_field.csv")
解析:层号、温区编号、X/Y坐标、温度值、采样时间
│
▼ RegionClassifier (numpy)
区域划分:
根据 X/Y 坐标判定区域类型
四角:x∈{0,5} AND y∈{0,5}
边缘:x∈{0,5} OR y∈{0,5} 但不含四角
中心:其余
│
▼ TempStatistics (pandas/numpy)
温度统计:
按区域:max, min, mean, std, range
按层:每层平均温度曲线
按温区:每个温区的温度剖面
│
▼ ProcessWindowChecker (numpy)
工艺窗口判定:
下限 160°C,上限 175°C
标记低于下限/高于上限的区域和层
│
▼ TempVisualizer (matplotlib)
可视化:
1. 底面空间温度热力图(imshow)
2. 各区域温度极值柱状图
3. 区域温度分布箱线图
4. 温度时序曲线(按层)
│
▼ SyntheticDataGenerator (numpy)
合成数据生成:
模拟 200 层 × 36 温区
中心热、边缘冷的高斯梯度分布
加入随机噪声和局部冷点
3.3 为什么用区域统计而不是只看平均值?
* 问题:平均温度 168°C 在窗口内,但四角局部只有 156°C——平均掩盖了极端值。
* 处理策略:按区域分组算 min,找出全局最低点所在区域。
* 工程合理性:增材制造的缺陷(翘边、开裂)发生在局部,不是平均。
3.4 分析前后对比
维度 设备默认温度曲线 区域温度统计程序
数据维度 单层平均值 36 温区 × 200 层全量
冷点识别 无 按区域定位最低温区
工艺窗口 只看均值是否在区间 逐区域逐层判定
缺陷归因 "可能温度不够" "四角区域持续欠温 50 层"
参数优化 全局调温 局部针对性预热
四、OOP 代码实现
4.1 项目结构
print_temp_field/
├── print_temp_field/
│ ├── __init__.py
│ ├── data_loader.py # 温度场数据加载
│ ├── region_classifier.py # 区域划分
│ ├── temp_statistics.py # 温度统计
│ ├── process_window.py # 工艺窗口判定
│ ├── visualizer.py # 可视化
│ └── synthetic_data.py # 合成数据生成
├── tests/
│ ├── __init__.py
│ └── test_temp_field.py # 单元测试
├── results/
│ ├── temp_heatmap.png # 空间温度热力图
│ ├── region_stats_bar.png # 区域极值柱状图
│ ├── region_boxplot.png # 区域温度分布箱线图
│ ├── temp_timeline.png # 温度时序曲线
│ ├── temp_statistics.csv # 区域温度统计
│ ├── below_threshold_report.txt # 欠温区域报告
│ └── temperature_field.csv # 合成温度数据
└── run_temp_analysis.py # 主程序入口
4.2 核心源码
<details>
<summary></summary>
"""3D打印温度场采样数据加载器"""
import pandas as pd
from pathlib import Path
from typing import Optional
class TempDataLoader:
"""
3D打印成型温度场采样数据加载器
支持 CSV 格式的多温区温度采样数据。
"""
def __init__(self, filepath: str = "temperature_field.csv",
encoding: str = "utf-8"):
self.filepath = Path(filepath)
self.encoding = encoding
self._raw_df: Optional[pd.DataFrame] = None
def load(self) -> pd.DataFrame:
"""
加载温度场数据
Returns
-------
pd.DataFrame
"""
if not self.filepath.exists():
raise FileNotFoundError(f"文件不存在: {self.filepath}")
self._raw_df = pd.read_csv(self.filepath, encoding=self.encoding)
# 列名标准化
col_aliases = {
"layer": ["层号", "layer", "layer_id", "Layer"],
"zone_id": ["温区", "zone", "zone_id", "sensor_id"],
"x": ["X坐标", "x", "pos_x", "coord_x"],
"y": ["Y坐标", "y", "pos_y", "coord_y"],
"temperature_c": ["温度", "temp", "temperature", "temperature_c", "temp_c"],
"timestamp": ["时间", "timestamp", "time", "sample_time"],
}
rename_map = {}
for target, aliases in col_aliases.items():
if target not in self._raw_df.columns:
for alias in aliases:
if alias in self._raw_df.columns:
rename_map[alias] = target
break
if rename_map:
self._raw_df = self._raw_df.rename(columns=rename_map)
# 数值化
for col in ["layer", "zone_id", "x", "y", "temperature_c"]:
if col in self._raw_df.columns:
self._raw_df[col] = pd.to_numeric(
self._raw_df[col], errors="coerce"
)
return self._raw_df.copy()
def get_raw_data(self) -> Optional[pd.DataFrame]:
return self._raw_df.copy() if self._raw_df is not None else None
</details>
<details>
<summary></summary>
"""空间区域分类器"""
import numpy as np
import pandas as pd
from typing import Dict, Optional
from enum import Enum
class RegionType(Enum):
"""区域类型"""
CORNER = "四角"
EDGE = "边缘"
CENTER = "中心"
class RegionClassifier:
"""
3D打印成型温度场空间区域分类器
根据温区的 X/Y 坐标将温区归类为四角/边缘/中心。
"""
def __init__(self, grid_size: int = 6):
"""
Parameters
----------
grid_size : int
温区网格大小(grid_size × grid_size)
"""
self.grid_size = grid_size
self.min_idx = 0
self.max_idx = grid_size - 1
def classify(self, x: int, y: int) -> str:
"""
判断单个坐标所属区域
Parameters
----------
x : int
X 坐标索引
y : int
Y 坐标索引
Returns
-------
str
区域类型
"""
is_boundary_x = x == self.min_idx or x == self.max_idx
is_boundary_y = y == self.min_idx or y == self.max_idx
if is_boundary_x and is_boundary_y:
return RegionType.CORNER.value
elif is_boundary_x or is_boundary_y:
return RegionType.EDGE.value
else:
return RegionType.CENTER.value
def classify_dataframe(self,
df: pd.DataFrame,
x_col: str = "x",
y_col: str = "y") -> pd.DataFrame:
"""
为整个 DataFrame 添加 region 列
Returns
-------
pd.DataFrame
"""
result = df.copy()
# 自动推断 grid_size
if not result.empty:
max_x = result[x_col].max()
max_y = result[y_col].max()
self.grid_size = int(max(max_x, max_y)) + 1
result["region"] = result.apply(
lambda row: self.classify(int(row[x_col]), int(row[y_col])),
axis=1,
)
return result
def get_region_zone_counts(self,
df: pd.DataFrame,
zone_col: str = "zone_id") -> Dict[str, int]:
"""获取各区域的温区数量"""
if "region" not in df.columns:
df = self.classify_dataframe(df)
return df.groupby("region")[zone_col].nunique().to_dict()
</details>
<details>
<summary></summary>
"""温度统计"""
import numpy as np
import pandas as pd
from typing import Dict, Optional
class TempStatistics:
"""
3D打印温度场统计计算器
按区域、按层、按温区多维度统计温度极值。
"""
def __init__(self):
pass
def by_region(self,
df: pd.DataFrame,
region_col: str = "region",
temp_col: str = "temperature_c") -> pd.DataFrame:
"""
按区域统计温度极值
Returns
-------
pd.DataFrame
"""
if df.empty or region_col not in df.columns:
return pd.DataFrame()
stats = df.groupby(region_col).agg(
max_temp=(temp_col, "max"),
min_temp=(temp_col, "min"),
mean_temp=(temp_col, "mean"),
std_temp=(temp_col, "std"),
median_temp=(temp_col, "median"),
count=(temp_col, "count"),
).reset_index()
# 温度范围
stats["temp_range"] = stats["max_temp"] - stats["min_temp"]
# 四舍五入
numeric_cols = stats.select_dtypes(include=[np.number]).columns
stats[numeric_cols] = stats[numeric_cols].round(2)
# 排序:按平均温度降序
stats = stats.sort_values("mean_temp", ascending=False).reset_index(drop=True)
return stats
def by_layer(self,
df: pd.DataFrame,
layer_col: str = "layer",
temp_col: str = "temperature_c") -> pd.DataFrame:
"""
按层统计温度
Returns
-------
pd.DataFrame
"""
if df.empty:
return pd.DataFrame()
stats = df.groupby(layer_col).agg(
max_temp=(temp_col, "max"),
min_temp=(temp_col, "min"),
mean_temp=(temp_col, "mean"),
std_temp=(temp_col, "std"),
).reset_index()
numeric_cols = stats.select_dtypes(include=[np.number]).columns
stats[numeric_cols] = stats[numeric_cols].round(2)
return stats.sort_values(layer_col).reset_index(drop=True)
def by_zone(self,
df: pd.DataFrame,
zone_col: str = "zone_id",
temp_col: str = "temperature_c") -> pd.DataFrame:
"""
按温区统计温度剖面
Returns
-------
pd.DataFrame
"""
if df.empty:
return pd.DataFrame()
stats = df.groupby(zone_col).agg(
max_temp=(temp_col, "max"),
min_temp=(temp_col, "min"),
mean_temp=(temp_col, "mean"),
std_temp=(temp_col, "std"),
).reset_index()
numeric_cols = stats.select_dtypes(include=[np.number]).columns
stats[numeric_cols] = stats[numeric_cols].round(2)
return stats.sort_values("mean_temp", ascending=False).reset_index(drop=True)
def region_layer_cross(self,
df: pd.DataFrame,
region_col: str = "region",
layer_col: str = "layer",
temp_col: str = "temperature_c") -> pd.DataFrame:
"""
区域 × 层 交叉统计(每个区域每层的平均温度)
Returns
-------
pd.DataFrame
透视表
"""
if df.empty:
return pd.DataFrame()
pivot = df.pivot_table(
index=layer_col,
columns=region_col,
values=temp_col,
aggfunc="mean",
)
return pivot
def overall_summary(self,
df: pd.DataFrame,
temp_col: str = "temperature_c") -> Dict:
"""整体温度汇总"""
if df.empty:
return {}
return {
"total_samples": len(df),
"global_max": round(df[temp_col].max(), 2),
"global_min": round(df[temp_col].min(), 2),
"global_mean": round(df[temp_col].mean(), 2),
"global_std": round(df[temp_col].std(), 2),
"global_range": round(df[temp_col].max() - df[temp_col].min(), 2),
}
</details>
<details>
<summary></summary>
"""工艺窗口判定"""
import numpy as np
import pandas as pd
from typing import Dict, Optional
class ProcessWindowChecker:
"""
3D打印温度工艺窗口判定器
识别低于下限/高于上限的温区和层。
"""
def __init__(self,
lower_limit: float = 160.0,
upper_limit: float = 175.0):
"""
Parameters
----------
lower_limit : float
工艺窗口下限 (°C)
upper_limit : float
工艺窗口上限 (°C)
"""
self.lower_limit = lower_limit
self.upper_limit = upper_limit
def mark_violations(self,
df: pd.DataFrame,
temp_col: str = "temperature_c") -> pd.DataFrame:
"""
标记超出工艺窗口的采样点
Returns
-------
pd.DataFrame
新增 is_below, is_above, is_in_window 列
"""
result = df.copy()
result["is_below"] = result[temp_col] < self.lower_limit
result["is_above"] = result[temp_col] > self.upper_limit
result["is_in_window"] = (
~result["is_below"] & ~result["is_above"]
)
return result
def region_violation_summary(self,
df: pd.DataFrame,
region_col: str = "region",
temp_col: str = "temperature_c") -> pd.DataFrame:
"""
按区域统计工艺窗口违反情况
Returns
-------
pd.DataFrame
"""
df_marked = self.mark_violations(df, temp_col)
summary = df_marked.groupby(region_col).agg(
total_samples=("is_below", "count"),
below_count=("is_below", "sum"),
above_count=("is_above", "sum"),
min_temp=(temp_col, "min"),
max_temp=(temp_col, "max"),
mean_temp=(temp_col, "mean"),
).reset_index()
summary["below_rate_pct"] = (
summary["below_count"] / summary["total_samples"] * 100
).round(2)
summary["above_rate_pct"] = (
summary["above_count"] / summary["total_samples"] * 100
).round(2)
numeric_cols = summary.select_dtypes(include=[np.number]).columns
summary[numeric_cols] = summary[numeric_cols].round(2)
return summary
def zone_violation_detail(self,
df: pd.DataFrame,
zone_col: str = "zone_id",
layer_col: str = "layer",
temp_col: str = "temperature_c") -> pd.DataFrame:
"""
温区-层级别的欠温详情
Returns
-------
pd.DataFrame
欠温最严重的温区列表
"""
df_marked = self.mark_violations(df, temp_col)
# 每个温区的欠温比例
zone_stats = df_marked.groupby(zone_col).agg(
below_count=("is_below", "sum"),
total_samples=("is_below", "count"),
min_temp=(temp_col, "min"),
mean_temp=(temp_col, "mean"),
).reset_index()
zone_stats["below_rate_pct"] = (
zone_stats["below_count"] / zone_stats["total_samples"] * 100
).round(2)
# 按欠温比例降序
zone_stats = zone_stats.sort_values(
"below_rate_pct", ascending=False
).reset_index(drop=True)
return zone_stats
def get_warning_report(self,
df: pd.DataFrame,
region_col: str = "region",
temp_col: str = "temperature_c") -> Dict:
"""生成预警报告"""
df_marked = self.mark_violations(df, temp_col)
region_summary = self.region_violation_summary(df_marked, region_col)
total_below = df_marked["is_below"].sum()
total_samples = len(df_marked)
overall_below_rate = total_below / total_samples * 100 if total_samples > 0 else 0
worst_region = region_summary.iloc[0] if not region_summary.empty else None
return {
"lower_limit": self.lower_limit,
"upper_limit": self.upper_limit,
"total_samples": total_samples,
"below_count": int(total_below),
"below_rate_pct": round(overall_below_rate, 2),
"worst_region": worst_region["region"] if worst_region is not None else None,
"worst_region_below_rate": (
worst_region["below_rate_pct"] if worst_region is not None else None
),
}
</details>
<details>
<summary></summary>
"""温度场可视化"""
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from pathlib import Path
from typing import Optional
plt.rcParams["font.sans-serif"] = ["SimHei", "DejaVu Sans"]
plt.rcParams["axes.unicode_minus"] = False
class TempVisualizer:
"""3D打印温度场可视化"""
def __init__(self, results_dir: str = "results"):
self.results_dir = Path(results_dir)
self.results_dir.mkdir(exist_ok=True)
self.region_colors = {
"四角": "#E74C3C",
"边缘": "#F39C12",
"中心": "#3498DB",
}
def plot_heatmap(self,
df: pd.DataFrame,
x_col: str = "x",
y_col: str = "y",
temp_col: str = "temperature_c",
lower_limit: float = 160.0) -> None:
"""底面空间温度热力图"""
# 构建网格
grid_x = sorted(df[x_col].unique())
grid_y = sorted(df[y_col].unique())
# 每个温区的平均温度
pivot = df.groupby([x_col, y_col])[temp_col].mean().reset_index()
heatmap = np.full((len(grid_y), len(grid_x)), np.nan)
for _, row in pivot.iterrows():
xi = grid_x.index(int(row[x_col]))
yi = grid_y.index(int(row[y_col]))
heatmap[yi, xi] = row[temp_col]
fig, ax = plt.subplots(figsize=(10, 8))
im = ax.imshow(heatmap, cmap="RdYlBu_r", aspect="auto")
plt.colorbar(im, ax=ax, label="温度 (°C)")
# 标注数值
for yi in range(len(grid_y)):
for xi in range(len(grid_x)):
val = heatmap[yi, xi]
if not np.isnan(val):
color = "white" if val < 162 or val > 172 else "black"
ax.text(xi, yi, f"{val:.0f}", ha="center", va="center",
fontsize=7, color=color, fontweight="bold")
ax.set_xticks(range(len(grid_x)))
ax.set_xticklabels([f"{x}" for x in grid_x])
ax.set_yticks(range(len(grid_y)))
ax.set_yticklabels([f"{y}" for y in grid_y])
ax.set_xlabel("X 方向温区", fontsize=12)
ax.set_ylabel("Y 方向温区", fontsize=12)
ax.set_title("底面成型温度场热力图", fontsize=14, fontweight="bold")
# 工艺窗口下限线(在 colorbar 上标注)
ax.axhline(y=-1, xmin=0, xmax=0, color="red") # placeholder
plt.tight_layout()
plt.savefig(self.results_dir / "temp_heatmap.png",
dpi=150, bbox_inches="tight")mo
利用AI解决实际问题,如果你觉得这个工具好用,欢迎关注长安牧笛!