周五下午,CNC 加工车间工具室。
"又废了一批活,"车间主任老周把一摞报废的铝合金壳体推到我面前,"刀纹明显,粗糙度超差。调出系统一看——加工中心 3 号机用的那把 Φ10 立铣刀,已经连续切了 6.5 小时。标准寿命是 6 小时。"
我打开刀具管理系统导出的 CSV 文件。几千行记录,字段包括:刀具编号、类型(立铣刀/面铣刀/钻头/丝锥)、安装机床、安装时间、累计加工时间(h)、标准寿命(h)、已加工件数、磨损状态(正常/注意/警告/报废)、最后更新时间。
"系统里不是有寿命数据吗?"我问。
"有,"老周点开一行,"但它是按'把'记录的——每把刀一行,显示累计加工时间和标准寿命。我想知道的是:哪些刀已经达到寿命了、该换了;哪些快到了、要提前准备。系统没有'到期刀具清单'这个功能——它只会在一把刀报废的时候弹一个报警,但那时候活已经废了。"
"所以你现在每次换刀,都是凭经验看加工表面?"
"对,"老周苦笑,"老师傅凭声音和刀纹判断。但新人不行——上次有个新来的,一把刀干了 8 小时才发现不对,前面 2 小时的活全废了。厂长说必须搞一个'刀具更换预警清单',每天早会发一份。"
"明白了,"我打开 VS Code,"你需要一个程序:读取所有刀具的寿命记录,拿'累计加工时间'和'标准寿命'做比较,算出剩余寿命百分比,然后筛出两类刀——已经超期的(累计 > 标准)和即将到期的(剩余 < 20%)。输出一张清单,按紧急程度排序。这样每天早会,你直接把清单打印出来:红色的是必须立刻换的,黄色的是本周要准备的。"
我敲了几行代码:
import pandas as pd
import matplotlib.pyplot as plt
# 读取刀具寿命数据
df = pd.read_csv("tool_life.csv")
# 计算剩余寿命百分比
df["remaining_pct"] = (df["std_life_h"] - df["used_h"]) / df["std_life_h"] * 100
df["remaining_pct"] = df["remaining_pct"].clip(lower=0)
# 筛选到期刀具
expired = df[df["used_h"] >= df["std_life_h"]].copy()
expired["priority"] = "🔴 立即更换"
# 筛选预警刀具(剩余 < 20%)
warning = df[(df["used_h"] < df["std_life_h"]) & (df["remaining_pct"] < 20)].copy()
warning["priority"] = "🟡 近期更换"
# 合并清单
replacement_list = pd.concat([expired, warning]).sort_values("remaining_pct")
print(replacement_list[["tool_id", "tool_type", "machine", "used_h", "std_life_h", "remaining_pct", "priority"]])
"这只是核心逻辑,"我说,"完整版会用 OOP 封装:一个类管数据加载,一个类管数据清洗(处理缺失寿命值、统一磨损状态编码),一个类管寿命计算和阈值筛选,一个类管可视化(剩余寿命条形图、类型分布饼图、机床负载热力图)。数据自包含——用 numpy 合成一份刀具寿命 CSV,读者下载就能跑。"
老周凑近屏幕:"所以你这东西,就是把几千把刀的'用了多久'和'该用多久'比一下——超了的标红,快到的标黄,排个序打印出来。新人照着清单换刀,不会废活了。"
"对。而且还能做趋势分析,"我补充,"比如统计各类型刀具的平均实际寿命 vs 标准寿命,看看是不是某种刀的设定寿命偏短了——如果是,可以跟工艺部门反馈调整标准。刀具管理不是'到点就换',是'用数据证明该不该换'。"
一、实际应用场景(真实痛点)
场景设定:CNC 加工车间拥有数十台加工中心和数百把数控刀具,每把刀具在管理系统中记录安装时间、累计加工时间、标准寿命、已加工件数、磨损状态等字段。车间工具室需要定期生成刀具更换清单,防止刀具超期使用导致工件报废。现场常见痛点:刀具数据按"把"逐条记录,行数庞大(几百把活跃刀具),系统不提供按寿命阈值筛选的"到期清单"功能,依赖操作工凭经验判断,新人容易误判导致批量报废。
现场原话(叙事化):
"我不是不想管刀具寿命,"老周说,"每把刀的数据都在系统里,累计用了多久、标准寿命是多少,清清楚楚。但系统不会告诉我'现在有 12 把刀已经超期了、8 把快到了'——它只会在一把刀彻底报废的时候弹一个报警。那时候活已经废了,报警有什么用?我需要的是每天上班前,桌上有一张清单:哪些刀今天必须换、哪些这周要准备。"
核心矛盾:"逐把记录的刀具寿命明细"与"按阈值筛选的更换预警清单"之间的聚合断层。需要一个"刀具寿命筛选与更换清单生成程序",用
"pandas" 读取并计算剩余寿命,用
"numpy" 做阈值判断和统计,用
"matplotlib" 绘制剩余寿命分布图,输出结构化的刀具更换清单。
二、痛点分析(映射到滨州职业学院《先进制造技术》课程模型)
《先进制造技术》模块 本篇痛点对应
数控加工与CAD/CAM技术:数控刀具系统与寿命管理 刀具寿命模型:基于加工时间的刀具磨损规律与寿命阈值设定。
先进制造技术基础:先进制造系统的维护策略 预防性维护:基于寿命数据的刀具更换预警,避免突发报废。
FMS与先进生产管理:FMS 的刀具管理子系统 刀具调度与准备:提前生成更换清单,减少机床等待时间。
智能制造与数字孪生:加工过程的数字孪生 刀具磨损状态可视化:将寿命数据转化为直观的更换优先级。
一句话总结:我们需要构建一个"刀具寿命记录筛选与更换清单生成程序",用
"pandas" 读取并计算剩余寿命百分比,用
"numpy" 做阈值筛选,用
"matplotlib" 绘制寿命分布图,实现从逐把记录到更换预警清单的转化。
三、核心逻辑讲解(大白话)
3.1 问题本质:把刀具寿命想象成"汽车保养提醒"
把刀具寿命管理想象成"汽车的保养提醒系统":
* 标准寿命 = 保养周期:汽车每 5000 公里换机油,刀具每 6 小时加工时间换刀——都是"标准寿命"。
* 累计加工时间 = 已行驶里程:汽车仪表盘显示已跑 4800 公里,刀具记录显示已用 5.5 小时。
* 剩余寿命百分比 = 保养倒计时:(标准 - 已用) / 标准 × 100%。汽车剩 4% 该保养了,刀具剩 10% 该准备了。
* 更换清单 = 4S 店的"到期保养清单":把所有车按剩余保养里程排序,先保养快到周期的——对应车间里"先换快到期的刀"。
工业应用:
* pandas 计算剩余寿命:
"df["remaining_pct"] = (df["std_life_h"] - df["used_h"]) / df["std_life_h"] * 100",一行代码算出每把刀的剩余百分比。
* numpy 阈值筛选:
"np.where(df["used_h"] >= df["std_life_h"], "到期", np.where(df["remaining_pct"] < 20, "预警", "正常"))",向量化条件判断,比逐行 if-else 快几十倍。
* matplotlib 条形图:横轴刀具编号,纵轴剩余寿命百分比,红色标到期、黄色标预警、绿色标正常——一张图看清全车间刀具状态。
3.2 业务逻辑 → 代码映射
定义刀具寿命数据模型
│
▼ ToolDataLoader (pandas)
读取刀具 CSV:
pd.read_csv("tool_life.csv")
解析刀具编号、类型、机床、累计加工时间、标准寿命
│
▼ DataCleaner (pandas)
数据清洗:
处理缺失寿命值(删除或标记)
统一磨损状态编码(正常/注意/警告/报废 → 0/1/2/3)
│
▼ LifeCalculator (pandas/numpy)
寿命计算:
remaining_h = std_life_h - used_h
remaining_pct = remaining_h / std_life_h * 100
│
▼ ThresholdFilter (numpy)
阈值筛选:
到期:used_h >= std_life_h
预警:remaining_pct < 20%
正常:其余
│
▼ ReplacementListGenerator
更换清单生成:
按紧急程度排序(到期 → 预警 → 正常)
输出 CSV 清单
│
▼ ToolVisualizer (matplotlib)
可视化:
1. 剩余寿命水平条形图(按类型着色)
2. 刀具类型寿命分布箱线图
3. 各机床刀具数量与到期数对比
│
▼ SyntheticDataGenerator (numpy)
合成数据生成:
用 numpy 按类型生成刀具寿命数据
模拟不同刀具的寿命分布
确保数据自包含、可复现
3.3 为什么用向量化条件判断而不是逐行循环?
* 问题:300 把刀,每把写
"if used >= std: status = "到期"",代码冗长且慢。
* 处理策略:
"np.select()" 或
"np.where()" 向量化操作,一行代码完成全部判断,底层 C 加速。
* 工程合理性:车间可能有上千把刀在册,向量化是唯一合理的选择。
3.4 分析前后对比
维度 人工凭经验判断 Python 程序分析
判断依据 听声音、看刀纹(主观) 累计加工时间 vs 标准寿命(客观)
覆盖范围 只能关注正在用的刀 全部在册刀具
预警能力 报废了才发现 剩余 20% 就预警
输出形式 "感觉这把刀差不多了" 按紧急程度排序的更换清单
新人友好度 需要多年经验 照着清单换就行
四、OOP 代码实现
4.1 项目结构
cnc_tool_life_manager/
├── cnc_tool_life_manager/
│ ├── __init__.py
│ ├── data_loader.py # 刀具数据加载
│ ├── data_cleaner.py # 数据清洗
│ ├── life_calculator.py # 寿命计算
│ ├── threshold_filter.py # 阈值筛选
│ ├── replacement_list.py # 更换清单生成
│ ├── visualizer.py # 可视化
│ └── synthetic_data.py # 合成数据生成
├── tests/
│ ├── __init__.py
│ └── test_tool_life.py # 单元测试
├── results/
│ ├── replacement_list.csv # 刀具更换清单
│ ├── expired_only.csv # 已到期刀具
│ ├── warning_only.csv # 预警刀具
│ ├── remaining_life_bar.png # 剩余寿命条形图
│ ├── life_boxplot.png # 寿命分布箱线图
│ ├── machine_tool_heatmap.png # 机床刀具状态热力图
│ └── tool_report.txt # 分析报告
└── run_tool_life.py # 主程序入口
4.2 核心源码
<details>
<summary></summary>
"""数控刀具数据加载器"""
import pandas as pd
from pathlib import Path
from typing import Optional
class ToolDataLoader:
"""
数控刀具数据加载器
支持 CSV 格式的刀具寿命记录。
"""
def __init__(self, filepath: str, encoding: str = "utf-8"):
self.filepath = Path(filepath)
self.encoding = encoding
self._raw_df: Optional[pd.DataFrame] = None
def load(self,
tool_id_col: str = "tool_id",
type_col: str = "tool_type",
machine_col: str = "machine",
used_col: str = "used_h",
std_col: str = "std_life_h",
wear_col: str = "wear_status") -> pd.DataFrame:
"""
加载刀具寿命数据
Parameters
----------
tool_id_col : str
刀具编号列名
type_col : str
刀具类型列名
machine_col : str
安装机床列名
used_col : str
累计加工时间列名
std_col : str
标准寿命列名
wear_col : str
磨损状态列名
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 = {
tool_id_col: ["刀具编号", "tool_no", "tool_id", "id"],
type_col: ["刀具类型", "type", "tool_type", "category"],
machine_col: ["机床", "machine", "mc", "cnc"],
used_col: ["已用时间", "used", "used_h", "accumulated_time"],
std_col: ["标准寿命", "std_life", "std_life_h", "standard_life"],
wear_col: ["磨损状态", "wear", "wear_status", "condition"],
}
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)
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 Optional
class DataCleaner:
"""
数控刀具数据清洗器
处理缺失值、统一磨损状态编码。
"""
# 磨损状态映射
WEAR_MAP = {
"正常": 0, "Normal": 0, "OK": 0, "0": 0,
"注意": 1, "Caution": 1, "Monitor": 1, "1": 1,
"警告": 2, "Warning": 2, "Warn": 2, "2": 2,
"报废": 3, "Failed": 3, "Fail": 3, "3": 3,
}
def __init__(self):
pass
def clean(self,
df: pd.DataFrame,
used_col: str = "used_h",
std_col: str = "std_life_h",
wear_col: str = "wear_status") -> pd.DataFrame:
"""
清洗刀具数据
Parameters
----------
df : pd.DataFrame
原始数据
used_col : str
累计加工时间列名
std_col : str
标准寿命列名
wear_col : str
磨损状态列名
Returns
-------
pd.DataFrame
"""
result = df.copy()
# 数值化
result[used_col] = pd.to_numeric(result[used_col], errors="coerce")
result[std_col] = pd.to_numeric(result[std_col], errors="coerce")
# 处理缺失值:删除关键字段为空且无法推断的行
result = result.dropna(subset=[used_col, std_col], how="all")
# 过滤异常值
result = result[result[used_col] >= 0]
result = result[result[std_col] > 0]
# 统一磨损状态编码
if wear_col in result.columns:
result["wear_code"] = result[wear_col].map(
lambda x: self.WEAR_MAP.get(str(x).strip(), -1)
)
else:
result["wear_code"] = -1
return result.reset_index(drop=True)
</details>
<details>
<summary></summary>
"""刀具寿命计算器"""
import numpy as np
import pandas as pd
from typing import Dict, Optional
class LifeCalculator:
"""
数控刀具寿命计算器
计算剩余寿命小时数和百分比。
"""
def __init__(self,
used_col: str = "used_h",
std_col: str = "std_life_h"):
self.used_col = used_col
self.std_col = std_col
def calculate(self, df: pd.DataFrame) -> pd.DataFrame:
"""
计算剩余寿命
Parameters
----------
df : pd.DataFrame
清洗后的数据
Returns
-------
pd.DataFrame
新增 remaining_h 和 remaining_pct 列
"""
result = df.copy()
result["remaining_h"] = (
result[self.std_col] - result[self.used_col]
).clip(lower=0)
result["remaining_pct"] = (
result["remaining_h"] / result[self.std_col] * 100
)
return result
def get_statistics(self, df: pd.DataFrame) -> Dict:
"""计算整体寿命统计"""
return {
"total_tools": len(df),
"avg_used": df[self.used_col].mean(),
"avg_std": df[self.std_col].mean(),
"avg_remaining_pct": df["remaining_pct"].mean(),
"min_remaining_pct": df["remaining_pct"].min(),
}
</details>
<details>
<summary></summary>
"""阈值筛选器"""
import numpy as np
import pandas as pd
from typing import Dict, Optional
class ThresholdFilter:
"""
数控刀具阈值筛选器
基于寿命阈值筛选到期和预警刀具。
"""
STATUS_EXPIRED = "到期"
STATUS_WARNING = "预警"
STATUS_NORMAL = "正常"
def __init__(self,
used_col: str = "used_h",
std_col: str = "std_life_h",
remaining_pct_col: str = "remaining_pct",
warning_threshold: float = 20.0):
self.used_col = used_col
self.std_col = std_col
self.remaining_pct_col = remaining_pct_col
self.warning_threshold = warning_threshold
def filter(self, df: pd.DataFrame) -> pd.DataFrame:
"""
阈值筛选
Parameters
----------
df : pd.DataFrame
含寿命计算结果的数据
Returns
-------
pd.DataFrame
新增 status 列
"""
result = df.copy()
conditions = [
result[self.used_col] >= result[self.std_col],
result[self.remaining_pct_col] < self.warning_threshold,
]
choices = [self.STATUS_EXPIRED, self.STATUS_WARNING]
result["status"] = np.select(conditions, choices, default=self.STATUS_NORMAL)
return result
def get_expired(self, df: pd.DataFrame) -> pd.DataFrame:
"""获取已到期刀具"""
return df[df["status"] == self.STATUS_EXPIRED].copy()
def get_warning(self, df: pd.DataFrame) -> pd.DataFrame:
"""获取预警刀具"""
return df[df["status"] == self.STATUS_WARNING].copy()
def get_normal(self, df: pd.DataFrame) -> pd.DataFrame:
"""获取正常刀具"""
return df[df["status"] == self.STATUS_NORMAL].copy()
def summary(self, df: pd.DataFrame) -> Dict[str, int]:
"""统计各状态数量"""
return {
self.STATUS_EXPIRED: (df["status"] == self.STATUS_EXPIRED).sum(),
self.STATUS_WARNING: (df["status"] == self.STATUS_WARNING).sum(),
self.STATUS_NORMAL: (df["status"] == self.STATUS_NORMAL).sum(),
}
</details>
<details>
<summary></summary>
"""更换清单生成器"""
import numpy as np
import pandas as pd
from typing import Optional
class ReplacementListGenerator:
"""
刀具更换清单生成器
按紧急程度排序,生成结构化更换清单。
"""
PRIORITY_MAP = {
"到期": 0,
"预警": 1,
"正常": 2,
}
def __init__(self,
tool_id_col: str = "tool_id",
type_col: str = "tool_type",
machine_col: str = "machine"):
self.tool_id_col = tool_id_col
self.type_col = type_col
self.machine_col = machine_col
def generate(self, df: pd.DataFrame) -> pd.DataFrame:
"""
生成更换清单
Parameters
----------
df : pd.DataFrame
已筛选和标记状态的数据
Returns
-------
pd.DataFrame
按优先级排序的更换清单
"""
result = df.copy()
# 添加优先级
result["priority"] = result["status"].map(self.PRIORITY_MAP)
# 按优先级和剩余寿命排序
result = result.sort_values(
["priority", "remaining_pct"],
ascending=[True, True],
)
# 选择输出列
output_cols = [
self.tool_id_col, self.type_col, self.machine_col,
"used_h", "std_life_h", "remaining_h",
"remaining_pct", "status", "wear_status",
]
available_cols = [c for c in output_cols if c in result.columns]
return result[available_cols].reset_index(drop=True)
def to_csv(self, df: pd.DataFrame, filepath: str) -> None:
"""导出为 CSV"""
df.to_csv(filepath, index=False, encoding="utf-8-sig")
</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 ToolVisualizer:
"""数控刀具寿命可视化"""
STATUS_COLORS = {
"到期": "#E74C3C",
"预警": "#F39C12",
"正常": "#2ECC71",
}
TYPE_COLORS = {
"立铣刀": "#3498DB",
"面铣刀": "#9B59B6",
"钻头": "#1ABC9C",
"丝锥": "#E67E22",
"镗刀": "#34495E",
"球头铣刀": "#E91E63",
}
def __init__(self, results_dir: str = "results"):
self.results_dir = Path(results_dir)
self.results_dir.mkdir(exist_ok=True)
def plot_remaining_life_bar(self,
df: pd.DataFrame,
top_n: int = 30,
title: str = "刀具剩余寿命排行") -> None:
"""绘制剩余寿命水平条形图"""
# 取剩余寿命最低的 top_n
plot_df = df.nsmallest(top_n, "remaining_pct").copy()
plot_df = plot_df.sort_values("remaining_pct", ascending=True)
fig, ax = plt.subplots(figsize=(12, max(6, top_n * 0.35)))
colors = [
self.STATUS_COLORS.get(row["status"], "#BDC3C7")
for _, row in plot_df.iterrows()
]
bars = ax.barh(
plot_df["tool_id"].astype(str),
plot_df["remaining_pct"],
color=colors,
height=0.6,
)
ax.set_xlabel("剩余寿命 (%)", fontsize=12)
ax.set_title(title, fontsize=14, fontweight="bold")
ax.set_xlim(0, 105)
# 参考线
ax.axvline(x=20, color="#F39C12", linestyle="--",
linewidth=1, alpha=0.7, label="预警线 20%")
ax.axvline(x=0, color="#E74C3C", linestyle="--",
linewidth=1, alpha=0.7, label="到期线 0%")
ax.legend(loc="lower right")
# 标注数值
for bar, val in zip(bars, plot_df["remaining_pct"]):
ax.text(
bar.get_width() + 1,
bar.get_y() + bar.get_height() / 2,
f"{val:.1f}%",
va="center",
fontsize=8,
)
plt.tight_layout()
plt.savefig(self.results_dir / "remaining_life_bar.png",
dpi=150, bbox_inches="tight")
plt.close()
def plot_life_boxplot(self,
df: pd.DataFrame,
type_col: str = "tool_type",
used_col: str = "used_h",
title: str = "各类型刀具寿命分布") -> None:
"""绘制各类型刀具寿命分布箱线图"""
types = df[type_col].unique()
data = [df[df[type_col] == t][used_col].values for t in types]
fig, ax = plt.subplots(figsize=(10, 6))
bp = ax.boxplot(data, labels=types, patch_artist=True)
# 着色
for patch, t in zip(bp["boxes"], types):
patch.set_facecolor(self.TYPE_COLORS.get(t, "#BDC3C7"))
patch.set_alpha(0.7)
ax.set_ylabel("累计加工时间 (h)", fontsize=12)
ax.set_title(title, fontsize=14, fontweight="bold")
ax.tick_params(axis="x", rotation=30)
plt.tight_layout()
plt.savefig(self.results_dir / "life_boxplot.png",
dpi=150, bbox_inches="tight")
plt.close()
def plot_machine_heatmap(self,
df: pd.DataFrame,
machine_col: str = "machine",
status_col: str = "status") -> None:
"""绘制各机床刀具状态热力图"""
# 统计每台机床各状态数量
cross = df.groupby([machine_col, status_col]).size().unstack(fill_value=0)
if cross.empty:
return
fig, ax = plt.subplots(figsize=(10, max(4, len(cross) * 0.5)))
im = ax.imshow(cross.values, cmap="RdYlGn_r", aspect="auto")
ax.set_xticks(range(len(cross.columns)))
ax.set_xticklabels(cross.columns)
ax.set_yticks(range(len(cross.index)))
ax.set_yticklabels(cross.index)
ax.set_xlabel("状态", fontsize=12)
ax.set_ylabel("机床", fontsize=12)
ax.set_title("各机床刀具状态分布", fontsize=14, fontweight="bold")
plt.colorbar(im, ax=ax, label="刀具数量")
plt.tight_layout()
plt.savefig(self.results_dir / "machine_tool_heatmap.png",
dpi=150, bbox_inches="tight")
plt.close()
</details>
<details>
<summary></summary>
"""合成数据生成器"""
import numpy as np
import pandas as pd
from pathlib import Path
from typing import Optional
class SyntheticDataGenerator:
"""
数控刀具合成数据生成器
模拟多类型、多机床的刀具寿命数据。
"""
def __init__(self, rng: Optional[np.random.RandomState] = None):
self.rng = rng or np.random.RandomState(42)
# 刀具类型配置:(名称, 标准寿命均值 h, 标准寿命标准差, 磨损速率均值)
self.tool_types = [
("立铣刀", 6.0, 1.5, 1.0),
("面铣刀", 12.0, 2.0, 0.8),
("钻头", 4.0, 1.0, 1.2),
("丝锥", 3.0, 0.8, 1.5),
("镗刀", 20.0, 3.0, 0.5),
("球头铣刀", 5.0, 1.2, 1.1),
]
self.machines = [f"CNC-{i:02d}" for i in range(1, 13)] # 12 台机床
def generate(self,
n_tools: int = 300,
output_path: str = "tool_life.csv") -> pd.DataFrame:
"""
生成合成刀具寿命数据
Parameters
----------
n_tools : int
刀具数量
output_path : str
输出路径
Returns
-------
pd.DataFrame
"""
records = []
for i in range(n_tools):
# 随机选择刀具类型
type_name, std_mean, std_std, wear_rate = self.tool_types[
self.rng.randint(0, len(self.tool_types))
]
# 标准寿命(正态分布)
std_life = max(1.0, self.rng.normal(std_mean, std_std))
# 已用时间(取决于磨损速率和随机波动)
# 模拟部分刀具已超期、部分接近寿命、部分正常
usage_factor = self.rn
利用AI解决实际问题,如果你觉得这个工具好用,欢迎关注长安牧笛!