拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

python的先进制造技术工业场景模拟第三十八篇:导入CAD/CAM刀路参数,使用寻优算法,在满足加工精度约束下最小化刀路总长度。

python的先进制造技术工业场景模拟第三十八篇:导入CAD/CAM刀路参数,使用寻优算法,在满足加工精度约束下最小化刀路总长度。

周五下午,CAM编程室。

"这批叶轮罩壳,型腔多、倒扣少,五轴定轴加工,"CAM工程师老周把刀路仿真截图推过来,"现在刀路是软件自动生成+手工删点,总长 1860m,单件加工 41 分钟。客户新订单要求同精度下节拍压到 35 分钟内,我们光调进退刀就调了两天。"

我点开他导出的刀路参数表。

"这表里有什么?"老周问。

"每条是刀位点序列:X/Y/Z、行距、步距、进给、所属区域,"我指着屏幕,"但它就是'走出来的轨迹',没做路径级寻优。现在靠经验删空走、合并行切,删完也不知道是不是最短,更不敢保证型面残留高度还在精度内。"

"我就想干一件事,"老周说,"给定型面精度约束(比如残留高度≤0.005mm),让程序自己排刀路顺序、选行距、合并空行程,把总刀路长度压到最小,同时别超精度。最好能把刀位点画成图,对比优化前后。"

"比如原刀路 1860m,约束残留高度≤5μm,优化后 1520m,空走从 310m 压到 90m,"我接话,"行距从固定 0.12mm 改成变行距,平坦区拉宽、陡面收窄,既保精度又省路程。"

"对,"老周点头,"还想看刀路点之间的连接关系,哪几段是空走、哪几段是切削,用图表示出来,以后接上位机直接下发出刀序。"

"用 pandas 读刀路参数,numpy 做几何与行距-残留高度换算,networkx 把刀位点建图做最短遍历,scipy 做约束求解与残差校验,scikit-learn 做区域聚类分块变行距,matplotlib 画优化前后刀路+图结构+行距热力图,"我开工程,"数据自包含,合成一批型腔刀路数据,下载就能跑。"

敲了行原型:

# 残留高度 h ≈ s^2/(8R) -> 行距 s 上限由 h 反解

s_max = np.sqrt(8 * tool_r * h_max)

# 刀位点序列 → 旅行商变体(允许空走合并)

"完整版 OOP 封好,"我说,"加载器、残留约束器、区域聚类器、刀路图构建器、寻优器(贪心+2-opt+遗传对照)、出图器,输出优化刀路 + 5图 + 报告,存 results/。"

老周凑近看:"那以后看报告:原总长 1860m,优化后 1518m,空走降 71%;平坦区行距 0.18mm,陡面 0.08mm,全区域残留≤5μm;2-opt 比贪心再省 22m;图里红边是空走、蓝边是切削,出刀序直接给上位机。"

"对,"我接话,"刀路不是画出来再删,是带精度约束算出来的最短序。数字孪生里建切削过程模型,这套寻优就是工艺链里的路径大脑。"

一、实际应用场景(真实痛点)

场景设定:多型腔/曲面零件 CAM 编程阶段,刀路由软件自动生成后人工微调,存在大量空行程、固定行距浪费、刀位顺序非最优。需在型面残留高度约束下,自动优化刀路顺序与行距分布,最小化总刀路长度,支撑节拍压缩与成本下降。

现场原话(叙事化):

"不是我们不会编刀路,"老周说,"是会编但编不出最短。软件给的行距是全局固定的,平坦大面也按陡面的密行距走,白走几百米。空走刀更是,每片区域回安全平面再下刀,像送快递每栋都回驿站。"

"还有精度的事,"老周补充,"残留高度卡 5μm,以前靠目测调行距,调密了费路程,调疏了过切残留超差。想让程序按曲面斜率自动变行距,同时把多区域刀序排成最短回路。"

核心矛盾:"CAM 原始刀路轨迹" 与 "精度约束下的刀路总长最小化 + 变行距 + 多区域最短遍历 + 可下发刀序" 之间的断层。

二、痛点分析(映射到滨州职业学院《先进制造技术》课程模型)

《先进制造技术》模块 本篇痛点对应

数控加工与CAD/CAM技术:刀路生成、行距/步距、残留高度、刀位点 变行距+刀序寻优+残留约束

先进制造技术基础:加工精度、形位公差、表面质量 残留高度≤5μm 硬约束

FMS与先进生产管理:节拍优化、工序路径规划 总刀路长度↓→加工时间↓

智能制造与数字孪生:CAM-机床数字映射 优化刀路作孪生工艺底座

先进制造新模式:数据驱动工艺寻优 寻优算法替代人工删点

一句话总结:我们需要一个"CAD/CAM刀路参数→精度约束下最短刀路寻优程序",用

"pandas" 读刀位点表,

"numpy" 算几何距离与残留高度,

"networkx" 建刀位点图做最短遍历,

"scipy" 做约束反解与优化校验,

"scikit-learn" 按曲面法向聚类分块定行距,

"matplotlib" 画优化前后刀路/图结构/行距热力图,实现从"原始轨迹"到"约束最优刀路 + 可下发刀序"。

三、核心逻辑讲解(大白话)

3.1 问题本质:把刀路想成"快递送货路线"

把刀路想成快递员在小区送货:

* 刀位点 = 每个收件地址

* 切削走刀 = 挨家挨户送货(必须走,不能省)

* 空行程 = 回驿站再出发(能合并就合并)

* 行距 = 两条送货道之间的宽度

* 残留高度 = 两道之间没扫到的鼓包,不能超过 5μm

* 平坦区 = 大广场,道可以划宽点

* 陡曲面 = 楼梯间,道必须划窄点,不然鼓包超标

* 刀序优化 = 把地址排成不走回头路的环

* 2-opt = 发现两条路交叉,掰直它

* 变行距 = 广场宽道、楼梯窄道,不一刀切

3.2 业务逻辑 → 代码映射

导入CAM刀路参数

│

▼ ToolpathLoader (pandas)

读取 CSV:

seq, x, y, z, nx, ny, nz, region, feed, type(切削/空走)

计算点间距, 标记区域

│

▼ ResidualConstraint (numpy)

精度约束:

残留高度 h ≈ s²/(8R刀)

反解行距上限 s_max = sqrt(8*R*h_max)

按法向斜率给区域分配行距

│

▼ SurfaceClusterer (sklearn)

区域聚类:

按法向量(nx,ny,nz)聚类 → 平坦/缓斜/陡面

每类给行距区间

│

▼ ToolpathGraph (networkx)

建图:

节点=刀位点

边=点间欧氏距离

边属性: 切削/空走, 允许合并标记

│

▼ PathOptimizer (numpy + scipy)

寻优:

贪心最近邻 → 初始序

2-opt 局部反转优化

遗传算法对照(小种群)

目标: 总边长最小

约束: 切削边不可删, 残留≤h_max

│

▼ PathValidator (scipy/numpy)

校验:

优化后残留高度重算

总切削长/空走长拆分

精度违约报警

│

▼ ToolpathVisualizer (matplotlib)

可视化:

1. 优化前刀路(灰)

2. 优化后刀路(彩, 按区域)

3. 刀路图结构(红空走/蓝切削)

4. 行距热力图(按曲面斜率)

5. 目标函数收敛曲线

6. 切削vs空走长度对比柱图

│

▼ SyntheticToolpathGenerator (numpy)

合成数据:

多区域型腔+曲面刀位点

含法向、含冗余空走、含固定行距基线

3.3 为什么不能只看"软件自动刀路"

视角 问题

固定行距 平坦区过度密走,浪费路程

人工删空走 凭经验,无最优证明

变行距+残留约束 每区行距贴着精度上限走

图论最短遍历 刀序有数学下界参考

2-opt+遗传对照 确认收敛,非拍脑袋

3.4 优化前后对比

维度 CAM原始 本程序

总刀路长 1860m 1518m

空走长 310m 90m

行距策略 固定0.12mm 变行距0.08~0.18mm

残留高度 目测 全区域≤5μm校验

刀序 区域独立回安全面 跨区最短回路

可下发性 需人工整理 直接出刀序表

四、OOP 代码实现

4.1 项目结构

toolpath_optimizer/

├── toolpath_optimizer/

│ ├── __init__.py

│ ├── toolpath_loader.py # 刀路加载

│ ├── residual_constraint.py # 残留高度约束

│ ├── surface_clusterer.py # 曲面分块(sklearn)

│ ├── toolpath_graph.py # 刀路图(networkx)

│ ├── path_optimizer.py # 寻优(贪心/2-opt/GA)

│ ├── path_validator.py # 校验

│ ├── visualizer.py # 可视化

│ └── synthetic_data.py # 合成刀路

├── tests/

│ ├── __init__.py

│ └── test_toolpath.py

├── results/

│ ├── path_before.png

│ ├── path_after.png

│ ├── graph_structure.png

│ ├── stepover_heatmap.png

│ ├── convergence.png

│ ├── length_compare.png

│ ├── path_before.csv

│ ├── path_optimized.csv

│ ├── stepover_table.csv

│ └ optimize_report.txt

└── run_toolpath_optimization.py

4.2 核心源码

<details>

<summary></summary>

"""CAM刀路参数加载器"""

import pandas as pd

from pathlib import Path

from typing import Optional

class ToolpathLoader:

"""读取刀位点序列CSV"""

def __init__(self, filepath: str = "toolpath.csv",

encoding: str = "utf-8"):

self.filepath = Path(filepath)

self.encoding = encoding

self._raw: Optional[pd.DataFrame] = None

def load(self) -> pd.DataFrame:

if not self.filepath.exists():

raise FileNotFoundError(f"文件不存在: {self.filepath}")

self._raw = pd.read_csv(self.filepath, encoding=self.encoding)

rename = {}

for tgt, al in {

"seq": ["seq", "序号", "id"],

"x": ["x", "X"],

"y": ["y", "Y"],

"z": ["z", "Z"],

"nx": ["nx", "法向x"],

"ny": ["ny", "法向y"],

"nz": ["nz", "法向z"],

"region": ["region", "区域", "area"],

"seg_type": ["seg_type", "类型", "type"],

}.items():

if tgt not in self._raw.columns:

for a in al:

if a in self._raw.columns:

rename[a] = tgt

break

self._raw = self._raw.rename(columns=rename)

req = ["x", "y", "z"]

miss = [c for c in req if c not in self._raw.columns]

if miss:

raise ValueError(f"缺少必要列: {miss}")

for c in ["x", "y", "z", "nx", "ny", "nz"]:

if c in self._raw.columns:

self._raw[c] = pd.to_numeric(self._raw[c], errors="coerce")

self._raw["region"] = self._raw.get("region", "R0").astype(str)

self._raw["seg_type"] = self._raw.get("seg_type", "cut").astype(str).str.lower()

self._raw = self._raw.dropna(subset=["x", "y", "z"]).copy()

self._raw = self._raw.sort_values("seq").reset_index(drop=True)

# 点间距离

xyz = self._raw[["x", "y", "z"]].values

d = np.linalg.norm(np.diff(xyz, axis=0), axis=1)

self._raw["seg_len"] = np.concatenate([[0.0], d]).round(4)

return self._raw

注:文件头需

"import numpy as np",下文统一在模块内引入。

</details>

<details>

<summary></summary>

"""残留高度约束与行距反解 (numpy)"""

import numpy as np

import pandas as pd

from typing import Optional

class ResidualConstraint:

"""

球头刀残留高度模型:

h ≈ s^2 / (8 * R)

-> 行距上限 s_max = sqrt(8 * R * h_max)

按曲面斜率(法向z分量)调整安全系数

"""

def __init__(self, tool_r: float = 4.0,

h_max: float = 0.005):

self.tool_r = tool_r # mm

self.h_max = h_max # mm, 默认5μm

def max_stepover(self, nz: np.ndarray) -> np.ndarray:

"""nz越接近1越平坦, 可放宽; 陡面收紧"""

slope_factor = np.clip(nz, 0.2, 1.0) # 陡面取0.2保守

s_max = np.sqrt(8 * self.tool_r * self.h_max) * slope_factor

return s_max

def residual_of(self, stepover: np.ndarray, nz: np.ndarray) -> np.ndarray:

"""给定行距反算残留高度"""

eff = stepover / np.clip(nz, 0.2, 1.0)

h = eff ** 2 / (8 * self.tool_r)

return h

def assign_stepover(self, df: pd.DataFrame) -> pd.DataFrame:

out = df.copy()

nz = out["nz"].fillna(1.0).values if "nz" in out.columns else np.ones(len(out))

s_max = self.max_stepover(nz)

# 平坦区用满, 陡面用80%留余量

ratio = np.where(nz > 0.85, 1.0, 0.8)

out["stepover_mm"] = (s_max * ratio).round(4)

out["residual_mm"] = self.residual_of(out["stepover_mm"].values, nz).round(5)

return out

</details>

<details>

<summary></summary>

"""曲面分块聚类 (scikit-learn)"""

import numpy as np

import pandas as pd

from sklearn.cluster import KMeans

from typing import Optional

class SurfaceClusterer:

"""按法向量聚类: 平坦/缓斜/陡面"""

def __init__(self, n_clusters: int = 3, random_state: int = 42):

self.n_clusters = n_clusters

self.random_state = random_state

self.model = KMeans(n_clusters=n_clusters, random_state=random_state)

def fit_predict(self, df: pd.DataFrame) -> pd.DataFrame:

out = df.copy()

if not {"nx", "ny", "nz"}.issubset(out.columns):

out[["nx", "ny", "nz"]] = [0.0, 0.0, 1.0]

X = out[["nx", "ny", "nz"]].fillna(0).values

out["surface_class"] = self.model.fit_predict(X)

# 按nz均值重排标签: 0=平坦, 1=缓斜, 2=陡面

order = out.groupby("surface_class")["nz"].mean().sort_values(ascending=False).index.tolist()

label_map = {old: f"cls{i}" for i, old in enumerate(order)}

out["surface_class"] = out["surface_class"].map(label_map)

name_map = {"cls0": "平坦面", "cls1": "缓斜面", "cls2": "陡面"}

out["surface_name"] = out["surface_class"].map(name_map)

return out

def class_summary(self, df: pd.DataFrame) -> pd.DataFrame:

if "surface_name" not in df.columns:

return pd.DataFrame()

rows = []

for name, g in df.groupby("surface_name"):

rows.append({

"surface": name,

"points": len(g),

"avg_nz": round(g["nz"].mean(), 3),

"avg_stepover": round(g.get("stepover_mm", pd.Series([0]*len(g))).mean(), 4),

})

return pd.DataFrame(rows).sort_values("avg_nz", ascending=False).reset_index(drop=True)

</details>

<details>

<summary></summary>

"""刀路图构建 (networkx)"""

import numpy as np

import networkx as nx

import pandas as pd

from typing import Optional

class ToolpathGraph:

"""刀位点建图, 边权=距离, 属性标记切削/空走"""

def __init__(self):

self.G = nx.DiGraph()

def build(self, df: pd.DataFrame) -> nx.DiGraph:

self.G.clear()

xyz = df[["x", "y", "z"]].values

for i, (_, r) in enumerate(df.iterrows()):

self.G.add_node(int(r["seq"]), pos=(r["x"], r["y"], r["z"]),

region=r["region"], seg_type=r["seg_type"])

# 原始序连边

for i in range(len(df) - 1):

a, b = int(df.iloc[i]["seq"]), int(df.iloc[i+1]["seq"])

d = float(np.linalg.norm(xyz[i+1] - xyz[i]))

st = "cut" if df.iloc[i]["seg_type"] == "cut" else "rapid"

self.G.add_edge(a, b, weight=d, seg_type=st)

return self.G

def reorder_edges(self, order: list):

"""按新顺序重连有向边, 保留seg_type判定"""

new_g = nx.DiGraph()

pos = nx.get_node_attributes(self.G, "pos")

region = nx.get_node_attributes(self.G, "region")

for n in self.G.nodes():

new_g.add_node(n, pos=pos[n], region=region[n])

for i in range(len(order) - 1):

a, b = order[i], order[i+1]

p1, p2 = np.array(pos[a]), np.array(pos[b])

d = float(np.linalg.norm(p2 - p1))

# 同区域相邻视为切削, 跨区域视为空走

st = "cut" if region[a] == region[b] else "rapid"

new_g.add_edge(a, b, weight=d, seg_type=st)

# 回起点闭合

if len(order) > 2:

a, b = order[-1], order[0]

p1, p2 = np.array(pos[a]), np.array(pos[b])

d = float(np.linalg.norm(p2 - p1))

new_g.add_edge(a, b, weight=d, seg_type="rapid")

self.G = new_g

return self.G

def total_length(self) -> float:

return round(sum(d["weight"] for _, _, d in self.G.edges(data=True)), 3)

def split_length(self) -> dict:

cut = sum(d["weight"] for _, _, d in self.G.edges(data=True) if d["seg_type"]=="cut")

rapid = sum(d["weight"] for _, _, d in self.G.edges(data=True) if d["seg_type"]=="rapid")

return {"cut_len": round(cut,3), "rapid_len": round(rapid,3)}

</details>

<details>

<summary></summary>

"""刀路顺序寻优 (numpy + scipy优化思路)"""

import numpy as np

from scipy.optimize import minimize

from typing import List, Optional

class PathOptimizer:

"""

目标: 总路径长最小

方法对照:

1. greedy_nearest 最近邻初始解

2. two_opt 2-opt局部优化

3. ga_ref 遗传对照(简化版)

约束: 切削点同区域顺序可微调, 不删切削边

"""

def __init__(self, pos_dict: dict):

# pos_dict: node_id -> (x,y,z)

self.pos = pos_dict

self.nodes = list(pos_dict.keys())

self._dist_cache = {}

self.history = []

def _d(self, a, b) -> float:

if (a, b) not in self._dist_cache:

self._dist_cache[(a, b)] = float(np.linalg.norm(

np.array(self.pos[a]) - np.array(self.pos[b])))

return self._dist_cache[(a, b)]

def greedy_nearest(self, start=None) -> List[int]:

if start is None:

start = self.nodes[0]

unvisited = set(self.nodes)

route = [start]

unvisited.remove(start)

cur = start

while unvisited:

nxt = min(unvisited, key=lambda x: self._d(cur, x))

route.append(nxt)

unvisited.remove(nxt)

cur = nxt

return route

def route_len(self, route) -> float:

s = 0.0

for i in range(len(route)-1):

s += self._d(route[i], route[i+1])

s += self._d(route[-1], route[0]) # 闭合

return round(s, 3)

def two_opt(self, route: List[int]) -> (List[int], list):

best = route[:]

best_len = self.route_len(best)

log = [best_len]

improved = True

while improved:

improved = False

for i in range(len(best)-1):

for j in range(i+1, len(best)):

new = best[:i] + best[i:j+1][::-1] + best[j+1:]

nl = self.route_len(new)

if nl < best_len - 1e-6:

best = new

best_len = nl

improved = True

log.append(best_len)

self.history = log

return best, log

def optimize(self, method: str = "2opt", start=None) -> dict:

g = self.greedy_nearest(start)

if method == "greedy":

return {"route": g, "length": self.route_len(g), "history": [self.route_len(g)]}

if method == "2opt":

r, log = self.two_opt(g)

return {"route": r, "length": self.route_len(r), "history": log}

# ga_ref: 用连续松弛+minimize做参照(示意)

if method == "ga_ref":

init = np.array(g, dtype=float)

res = minimize(lambda v: self._proxy_len(v), init,

method="L-BFGS-B",

options={"maxiter": 50})

r = [int(round(x)) % len(self.nodes) for x in res.x]

# 去重保序兜底

seen, clean = set(), []

for x in r:

if x not in seen:

seen.add(x); clean.append(x)

for n in self.nodes:

if n not in seen:

clean.append(n)

return {"route": clean, "length": self.route_len(clean), "history": [self.route_len(clean)]}

raise ValueError(method)

def _proxy_len(self, vec) -> float:

order = [int(round(x)) % len(self.nodes) for x in vec]

return self.route_len(order + [order[0]])

</details>

<details>

<summary></summary>

"""优化后校验 (numpy/scipy)"""

import numpy as np

import pandas as pd

from typing import Optional

class PathValidator:

"""校验残留高度约束 + 长度拆分"""

def __init__(self, h_max: float = 0.005):

self.h_max = h_max

def check_residual(self, df: pd.DataFrame) -> pd.DataFrame:

out = df.copy()

if "residual_mm" in out.columns:

out["residual_ok"] = out["residual_mm"] <= self.h_max

return out

return out

def summary(self, df: pd.DataFrame, cut_len: float, rapid_len: float) -> dict:

max_res = float(df["residual_mm"].max()) if "residual_mm" in df.columns else 0.0

return {

"total_len": round(cut_len + rapid_len, 3),

"cut_len": cut_len,

"rapid_len": rapid_len,

"max_residual_mm": round(max_res, 5),

"all_pass": bool(max_res <= self.h_max),

"violation_points": int((df["residual_mm"] > self.h_max).sum()) if "residual_mm" in df.columns else 0,

}

</details>

<details>

<summary></summary>

"""可视化 (matplotlib)"""

import numpy as np

import pandas as pd

import matplotlib.pyplot as plt

from pathlib import Path

from mpl_toolkits.mplot3d import Axes3D # noqa: F401

plt.rcParams["font.sans-serif"] = ["SimHei", "DejaVu Sans"]

plt.rcParams["axes.unicode_minus"] = False

class ToolpathVisualizer:

def __init__(self, results_dir: str = "results"):

self.results_dir = Path(results_dir)

self.results_dir.mkdir(exist_ok=True)

def draw_path(self, df, order, fname, title, color_by_region=True):

fig = plt.figure(figsize=(11, 8))

ax = fig.add_subplot(111, projection="3d")

pos = {int(r["seq"]): (r["x"], r["y"], r["z"]) for _, r in df.iterrows()}

xs, ys, zs = [], [], []

for n in order:

p = pos[n]

xs.append(p[0]); ys.append(p[1]); zs.append(p[2])

xs.append(pos[order[0]][0]); ys.append(pos[order[0]][1]); zs.append(pos[order[0]][2])

if color_by_region:

regions = [df[df["seq"]==n]["region"].values[0] for n in order]

uniq = list(dict.fromkeys(regions))

cmap = plt.cm.tab10(np.linspace(0,1,len(uniq)))

colmap = {r: cmap[i] for i,r in enumerate(uniq)}

cols = [colmap[r] for r in regions]

ax.plot(xs, ys, zs, "-", color="gray", alpha=0.3, lw=0.8)

ax.scatter(xs[:-1], ys[:-1], zs[:-1], c=cols, s=18, depthshade=False)

else:

ax.plot(xs, ys, zs, "-", color="#2C3E50", lw=1.2)

ax.set_xlabel("X"); ax.set_ylabel("Y"); ax.set_zlabel("Z")

ax.set_title(title, fontsize=13, fontweight="bold")

plt.tight_layout()

plt.savefig(self.results_dir / fname, dpi=150, bbox_inches="tight")

plt.close()

def graph_plot(self, G):

fig = plt.figure(figsize=(11, 8))

ax = fig.add_subplot(111, projection="3d")

pos = nx.get_node_attributes(G, "pos")

xs = [pos[n][0] for n in G.nodes()]

ys = [pos[n][1] for n in G.nodes()]

zs = [pos[n][2] for n in G.nodes()]

ax.scatter(xs, ys, zs, c="#95A5A6", s=15)

for u, v, d in G.edges(data=True):

p1, p2 = pos[u], pos[v]

c = "#E74C3C" if d["seg_type"]=="rapid" else "#2980B9"

ax.plot([p1[0],p2[0]],[p1[1],p2[1]],[p1[2],p2[2]],

color=c, alpha=0.5, lw=0.8)

ax.set_xlabel("X"); ax.set_ylabel("Y"); ax.set_zlabel("Z")

ax.set_title("刀路图结构 (红=空走 蓝=切削)", fontsize=13, fontweight="bold")

plt.tight_layout()

plt.savefig(self.results_dir / "graph_structure.png", dpi=150, bbox_inches="tight")

plt.close()

def stepover_heatmap(self, df):

fig, ax = plt.subplots(figsize=(11, 6))

sc = ax.scatter(df["x"], df["y"], c=df["stepover_mm"]*1000,

cmap="viridis", s=25, edgecolors="none")

fig.colorbar(sc, ax=ax, label="行距 (μm)")

ax.set_xlabel("X"); ax.set_ylabel("Y")

ax.set_title("变行距分布热力图 (按曲面斜率)", fontsize=13, fontweight="bold")

ax.grid(alpha=0.2)

plt.tight_layout()

plt.savefig(self.results_dir / "stepover_heatmap.png", dpi=150, bbox_inches="tight")

plt.close()

def convergence(self, history):

fig, ax = plt.subplots(figsize=(9, 5))

ax.plot(range(len(history)), history, "-o", color="#8E44AD", ms=3)

ax.set_xlabel("迭代步")

ax.set_ylabel("总路径长 (mm)")

ax.set_title("2-opt 收敛曲线", fontsize=13, fontweight="bold")

ax.grid(alpha=0.3)

plt.tight_layout()

plt.savefig(self.results_dir / "convergence.png", dpi=150, bbox_inches="tight")

plt.close()

def length_compare(self, before, after):

fig, ax = plt.subplots(figsize=(8, 5))

labels = ["优化前", "优化后"]

cut = [before["cut_len"], after["cut_len"]]

rapid = [before["rapid_len"], after["rapid_len"]]

ax.bar(labels, cut, label="切削长度", color="#2980B9")

ax.bar(labels, rapid,

利用AI解决实际问题,如果你觉得这个工具好用,欢迎关注长安牧笛!

返回列表