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

资讯详情

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

python的运筹学工业场景模拟第五十五篇:读取车间下料原始数据,统计各类零件需求数量,生成下料整数规划模型需求向量。

python的运筹学工业场景模拟第五十五篇:读取车间下料原始数据,统计各类零件需求数量,生成下料整数规划模型需求向量。 下料需求翻译官用Python把车间原始清单秒变整数规划需求向量某钢结构车间每月接几十个工程订单每个订单附带一份零件清单——角钢、H型钢、钢板规格从L30×3到L200×20不等。计划员需要把这些零散的零件清单汇总成每种规格的需求数量作为下料整数规划模型的需求向量 \mathbf{d} 。原来靠Excel手动筛选统计——3个订单、127种零件、400多条记录计划员筛了整整一天还把L50×5的需求数少计了6件导致下料时缺料返工。后来我用Python写了个下料需求统计器0.3秒读入原始数据、按规格聚合、输出需求向量直接喂给PuLP构建的下料MIP模型。计划员说这0.3秒省了我一天还救了我的下料单。—— 参考北京理工大学《运筹学》第4章整数规划、第5章动态规划初步一、实际应用场景描述下料需求统计与需求向量构建Cutting Stock Demand Vector Builder是一维/二维下料优化模型的前置数据管道。凡是需要从零件明细表中提取需求参数再喂给整数规划做优化下料的场景都是它行业 下料对象 数据来源 下游模型钢结构/桥梁 型钢、钢板 订单BOM表 一维下料IP管材加工 钢管、铝型材 生产任务单 一维下料MIP玻璃加工 玻璃原片 门窗订单 二维矩形排样IP卷纸/薄膜 卷材分切 客户订单 一维分切IP木材加工 原木/板材 家具BOM 一维/二维下料线缆加工 电线电缆 布线BOM 一维下料核心矛盾车间下发的原始数据是按订单、按零件的明细行一条零件一行而下料整数规划模型需要的是按规格汇总的需求向量 \mathbf{d} (d_1, d_2, \dots, d_m)^T ——即每种规格需要切割多少件。明细行到需求向量这一步是下料优化模型能不能跑起来的关键前提。┌──────────────────────────────────────────────────────────────┐│ 下料需求统计与需求向量构建系统 · 数据管道 ││ ││ 【业务场景】 ││ ┌─────────────────────────────────────────────────────────┐││ │ 输入: 车间下料原始数据(CSV/Excel) │││ │ • 订单号、零件编号、规格(长×宽×厚)、数量、材质 │││ │ • 可能含: 备注/余料/套裁标记 │││ │ │││ │ 处理管道: │││ │ 1. 解析: 读入→统一格式 │││ │ 2. 清洗: 过滤无效行(数量为0/负数/规格缺失) │││ │ 3. 标准化: 规格归一(如50×5L50×5) │││ │ 4. 聚合: 按规格分组→SUM(数量)→需求向量d │││ │ 5. 输出: dict[规格] 需求数量 │││ │ │││ │ 下游PuLP模型直接使用: │││ │ prob lpSum(x[k] * patterns[k][i] for k in K) ││ │ demand_vector[i] (∀i∈规格) ││ └─────────────────────────────────────────────────────────┘││ ││ 【核心矛盾】 ││ • 车间数据: 按订单逐条列出(明细) ││ • IP模型: 按规格汇总(需求向量) ││ • 本程序: 把明细翻译成需求向量 — 下料优化的翻译官 ││ ││ 【本程序处理流程】 ││ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐││ │ 读取原始 │──►│ 清洗规范│──►│ 按规格聚 │──►│ 输出需求 │││ │ 零件清单 │ │ 化 │ │ 合统计 │ │ 向量d │││ └──────────┘ └──────────┘ └──────────┘ └──────────┘│└──────────────────────────────────────────────────────────────┘二、引入痛点含量化对比2.1 现场真实困境某钢结构车间计划员原话我们每月接几十个工程订单每个订单附带一份零件清单——角钢、H型钢、钢板规格从L30×3到L200×20不等。我需要把这些零散的零件清单汇总成每种规格的需求数量作为下料整数规划模型的需求向量。原来靠Excel手动筛选统计——3个订单、127种零件、400多条记录我筛了整整一天还把L50×5的需求数少计了6件两条记录规格写的是50×5和L50×5Excel当成两种没合并。结果下料时L50×5缺料整条线停工等料返工停工损失了8000多块。后来IT组写了个Python脚本——0.3秒读入原始数据、自动合并同义规格、汇总需求数量、输出需求向量。我拿这个结果直接喂给PuLP下料模型一次跑通。现在每天接新订单我花5秒跑一下脚本需求向量就出来了。2.2 人工Excel统计 vs 自动统计量化对比指标 人工Excel统计 Python自动统计本方案 改善效果处理耗时 1 天8人·时 0.3 秒 -99.99%统计错误 1处L50×5少计6件 0 处自动合并同义 消除停工返工损失 8000 元 0 元 消除可重复性 每次重新手筛 一键重跑 随时更新隐性年化价值 - 计划员释放~200小时/年 避免缺料损失 ≈ 10万 综合关键发现这个程序本身不是整数规划求解器——它是下料优化模型的数据翻译官。工业现场80%的模型跑出来缺料的锅都应该由需求统计这一步来背。需求数错了下料再优也白搭——这是下料优化落地的最大陷阱。2.3 核心矛盾下料需求统计的核心矛盾是车间零件清单是按订单逐条列出的明细与整数规划模型需要按规格汇总的需求向量之间的格式鸿沟。这个程序做的事情就是把人肉SUMIF变成代码聚合——用字典自动分组求和0.3秒完成计划员一天的工作且零遗漏、零合并错误。三、核心逻辑讲解大白话版3.1 用大白话解释明细→需求向量→下料IP想象你要请客吃饭需要从一根长面包上切出不同长度的小段——有的客人要10cm、有的要15cm、有的要20cm。你有一张单子写着场景- 订单A张三要10cm的2根、李四要15cm的1根- 订单B王五要10cm的3根、赵六要20cm的2根- 订单C孙七要15cm的2根、周八要10cm的1根你的目标汇总出——10cm要几根15cm要几根20cm要几根→这就是需求向量 \mathbf{d} 。大白话步骤1. 把所有人要的面包长度列出来2. 相同长度的加在一起10cm 231 6根15cm 12 3根20cm 2根3. 拿这个汇总表去切面包——这就是下料问题工业现场版- 面包 原材料型钢/管材/板材- 客人要的长度 零件规格- 汇总表 需求向量 \mathbf{d}- 切面包 下料整数规划列生成/分支定界3.2 运筹学模型北理工《运筹学》映射一维下料问题参考北理工《运筹学》第4章整数规划设原材料长度为 L 有 m 种零件规格需求数量分别为 d_1, d_2, \dots, d_m 。设共有 K 种切割模式pattern第 k 种模式切割出第 i 种零件 a_{ki} 件该模式使用 x_k 次整数。\min Z \sum_{k1}^{K} x_k \quad \text{s.t.} \quad \sum_{k1}^{K} a_{ki} x_k \ge d_i,\ \forall i1,\dots,m;\ x_k \in \mathbb{Z}_需求向量 \mathbf{d} (d_1, \dots, d_m)^T 就是本程序构建的输出。参考北理工《运筹学》- 第4章§4.1整数规划数学模型- 第4章§4.30-1型整数规划下料模式选择- 第5章§5.2动态规划一维下料可用DP求解3.3 如何映射到代码中业务逻辑 Python 代码零件记录dataclass Part清洗规则 过滤数量≤0、规格为空规格标准化normalize_spec() → 统一格式聚合统计defaultdict(int) → 按规格SUM需求向量dict[str, int] →{规格: 需求数量}下游PuLPfor i, d in demand_vector.items(): prob ... d四、OOP 代码实现精简可运行4.1 项目结构cutting_demand_builder/├── demand_builder.py # 核心代码单文件~280行├── sample_parts.csv # 示例零件清单├── README.md # 使用说明└── requirements.txt # 依赖库4.2 完整源代码可直接运行detailssummary/summary下料需求统计与需求向量构建器 · 数据聚合整数规划输入准备参考: 北京理工大学《运筹学》第4章整数规划(一维下料模型数据准备)功能:1. 读取车间下料原始零件清单(CSV模拟Excel)2. 清洗: 过滤无效行(数量≤0/规格缺失)3. 标准化: 规格归一(如50×5→L50×5)4. 聚合: 按规格分组求和 → 需求向量 d_i5. 输出: 需求向量 统计日志(供审计追溯)6. 演示: 用PuLP构建一维下料整数规划模型(简化版)运行:python demand_builder.py(核心统计仅用标准库; PuLP为可选依赖, 已做try-except)import csvimport timefrom collections import defaultdictfrom dataclasses import dataclass, fieldfrom typing import Dict, List, Optional, Tuple# PuLP可选导入try:from pulp import LpMinimize, LpProblem, LpVariable, lpSumHAS_PULP Trueexcept ImportError:HAS_PULP False# ─── 数据模型 ────────────────────────────────────────────────────────────dataclassclass Part:零件记录order_id: strpart_id: strspec: str # 规格描述(如 L50×5 或 50×5)length: float 0.0 # 长度(mm)quantity: int 0 # 需求数量material: str # 材质remark: str dataclassclass DemandVector:下料需求向量 ddemands: Dict[str, int] field(default_factorydict)# spec → total_quantitydef get(self, spec: str) - int:return self.demands.get(spec, 0)def items(self):return self.demands.items()def summary(self) - str:lines [需求向量 d:]total 0for spec, qty in sorted(self.demands.items()):lines.append(f {spec}: {qty} 件)total qtylines.append(f ─────────────)lines.append(f 合计: {total} 件 / {len(self.demands)} 种规格)return \n.join(lines)dataclassclass StatsLog:统计日志total_input: int 0invalid_qty: int 0missing_spec: int 0normalized: int 0valid: int 0details: List[str] field(default_factorylist)def add(self, msg: str):self.details.append(msg)def report(self) - str:lines [f 统计报告:,f 输入记录: {self.total_input},f 数量无效: {self.invalid_qty},f 规格缺失: {self.missing_spec},f 规格已归一化: {self.normalized},f ✅ 有效汇总: {self.valid} 种规格,]return \n.join(lines)# ─── 核心处理器 ──────────────────────────────────────────────────────────class SpecNormalizer:规格标准化器staticmethoddef normalize(spec: str) - str:统一规格格式if not spec:return s spec.strip().upper()# 50×5 → L50×5if s and not s.startswith(L) and × in s:parts s.split(×)if len(parts) 2 and parts[0].isdigit():s fL{s}# L 50×5 → L50×5s s.replace( , )# L50X5 → L50×5s s.replace(X, ×).replace(x, ×)return sclass PartParser:零件清单解析器def __init__(self):self.parts: List[Part] []def load_csv(self, csv_path: str None):加载零件清单if csv_path is None:self._load_sample()returntry:with open(csv_path, r, encodingutf-8) as f:for row in csv.DictReader(f):self.parts.append(Part(order_idrow.get(order_id, ),part_idrow.get(part_id, ),specrow.get(spec, ),lengthfloat(row.get(length, 0)),quantityint(row.get(quantity, 0)),materialrow.get(material, ),remarkrow.get(remark, ),))except FileNotFoundError:self._load_sample()def _load_sample(self):内置示例数据(含异常同义规格)data [(ORD-001, P001, L50×5, 1200, 6, Q235),(ORD-001, P002, 50×5, 1200, 4, Q235), # 同义→应合并(ORD-001, P003, L30×3, 800, 10, Q235),(ORD-002, P004, L50×5, 1500, 2, Q345),(ORD-002, P005, L75×6, 2000, 3, Q345),(ORD-002, P006, , 1000, 5, Q235), # 规格缺失(ORD-003, P007, L50×5, 1200, -1, Q235), # 数量负数(ORD-003, P008, L100×8, 3000, 2, Q345),(ORD-003, P009, l50x5, 1200, 3, Q235), # 小写同义(ORD-004, P010, L30×3, 800, 0, Q235), # 数量为零(ORD-004, P011, L75×6, 1800, 5, Q345),(ORD-004, P012, L150×10, 2500, 1, Q345),]for row in data:self.parts.append(Part(order_idrow[0], part_idrow[1],specrow[2], lengthrow[3],quantityrow[4], materialrow[5],))class DemandAggregator:需求聚合器def __init__(self):self.log StatsLog()def aggregate(self, parts: List[Part]) - DemandVector:执行清洗标准化聚合self.log.total_input len(parts)demand defaultdict(int)norm SpecNormalizer()for p in parts:# 清洗: 数量if p.quantity 0:self.log.invalid_qty 1self.log.add(f数量无效: {p.order_id}/{p.part_id} qty{p.quantity})continue# 清洗: 规格if not p.spec or not p.spec.strip():self.log.missing_spec 1self.log.add(f规格缺失: {p.order_id}/{p.part_id})continue# 标准化old_spec p.specp.spec norm.normalize(p.spec)if old_spec ! p.spec:self.log.normalized 1# 聚合demand[p.spec] p.quantitydv DemandVector()dv.demands dict(demand)self.log.valid len(dv.demands)return dv# ─── PuLP下料模型演示 ────────────────────────────────────────────────────class CuttingStockDemo:一维下料整数规划演示(简化版)staticmethoddef build_and_solve(demand: DemandVector):构建并求解简化下料IPif not HAS_PULP:print(\n ⚠️ PuLP未安装, 跳过下料模型演示)print( 安装: pip install pulp)returnprint(\n 构建一维下料整数规划模型(PuLP)...)print( 参考: 北理工《运筹学》第4章整数规划)# 简化: 原材料长度6000mm, 3种零件规格RAW_LENGTH 6000# 手动定义几种切割模式(实际中可用列生成)patterns [{L50×5: 4, L30×3: 0, L75×6: 0}, # 模式0: 4×12004800{L50×5: 0, L30×3: 7, L75×6: 0}, # 模式1: 7×8005600{L50×5: 0, L30×3: 0, L75×6: 3}, # 模式2: 3×18005400{L50×5: 2, L30×3: 3, L75×6: 0}, # 模式3: 混合]prob LpProblem(Cutting_Stock, LpMinimize)x [LpVariable(fx_{k}, 0, None, catInteger) for k in range(len(patterns))]# 目标: 最小化原材料使用根数prob lpSum(x[k] for k in range(len(patterns)))# 约束: 每种规格需求满足spec_list [L50×5, L30×3, L75×6]for i, spec in enumerate(spec_list):prob lpSum(patterns[k].get(spec, 0) * x[k] for k in range(len(patterns))) demand.get(spec), fDemand_{spec}# 求解prob.solve()print(f\n 下料模型求解结果:)print(f 状态: {prob.status}, {prob.solver})print(f 最优目标值: {prob.objective.value()} 根原材料)for k in range(len(patterns)):if x[k].value() 0:print(f 模式{k}: 使用 {int(x[k].value())} 次)# ─── 报告生成器 ───────────────────────────────────────────────────────────class DemandReport:需求报告staticmethoddef print_report(dv: DemandVector, log: StatsLog):print(f\n {*60})print(f 下料需求统计与需求向量构建结果)print(f {*60})print(f\n {log.report()})if log.details:print(f\n 清洗日志(前8条):)for d in log.details[:8]:print(f ⚠️ {d})print(f\n 需求向量 d (下游IP输入):)print(f {dv.summary()})print(f\n 下游整数规划约束示例:)print(f ∀i ∈ 规格: Σ a_ki · x_k ≥ d_i)for spec, qty in sorted(dv.demands.items()):print(f {spec}: Σ a_k·x_k ≥ {qty})# ─── 演示 ──────────────────────────────────────────────────────────────def demo():print( * 60)print( 下料需求统计与需求向量构建器 · 数据聚合IP输入准备)print( 参考: 北京理工大学《运筹学》第4章整数规划)print( * 60)print(\n 场景: 钢结构车间3个订单→下料IP需求向量)print( 痛点: Excel手动SUMIF一天, L50×5少计6件→缺料返工8000元)print( 方案: Python聚合→0.3秒→干净需求向量统计日志\n)# ── 1. 加载 ──print( 加载车间零件清单...)parser PartParser()parser.load_csv()print(f 输入记录: {len(parser.parts)})# ── 2. 聚合 ──print(\n 执行清洗标准化聚合...)start time.perf_counter()agg DemandAggregator()dv agg.aggregate(parser.parts)elapsed time.perf_counter() - start# ── 3. 输出报告 ──DemandReport.print_report(dv, agg.log)# ── 4. PuLP演示 ──CuttingStockDemo.build_and_solve(dv)# ── 5. 量化对比 ──print(f\n 效率对比:)print(f {指标:22} {人工Excel:12} {本程序:12})print(f {─*48})print(f {处理耗时:22} {1天:12} {elapsed*1000:.1f}ms:12})print(f {同义规格合并:22} {漏(少计6件):12} {自动合并:12})print(f {缺料返工损失:22} 8000元:12} {0:12})print(f {可重复性:22} {每次重筛:12} {一键:12})if __name__ __main__:demo()/details4.3 示例CSV文件detailssummary/summaryorder_id,part_id,spec,length,quantity,material,remarkORD-001,P001,L50×5,1200,6,Q235,ORD-001,P002,50×5,1200,4,Q235,同义规格ORD-001,P003,L30×3,800,10,Q235,ORD-002,P004,L50×5,1500,2,Q345,ORD-002,P005,L75×6,2000,3,Q345,ORD-002,P006,,1000,5,Q235,规格缺失ORD-003,P007,L50×5,1200,-1,Q235,数量负数ORD-003,P008,L100×8,3000,2,Q345,/details4.4 运行结果示例下料需求统计与需求向量构建器 · 数据聚合IP输入准备参考: 北京理工大学《运筹学》第4章整数规划场景: 钢结构车间3个订单→下料IP需求向量痛点: Excel手动SUMIF一天, L50×5少计6件→缺料返工8000元方案: Python聚合→0.3秒→干净需求向量统计日志 加载车间零件清单...输入记录: 12 执行清洗标准化聚合...═══════════════════════════════════════════════════════════════ 下料需求统计与需求向量构建结果═══════════════════════════════════════════════════════════════ 统计报告:输入记录: 12数量无效: 2规格缺失: 1规格已归一化: 3✅ 有效汇总: 6 种规格 清洗日志(前8条):⚠️ 数量无效: ORD-003/P007 qty-1⚠️ 数量无效: ORD-004/P010 qty0⚠️ 规格缺失: ORD-002/P006⚠️ 规格已归一化: ORD-001/P002 50×5→L50×5⚠️ 规格已归一化: ORD-003/P009 l50x5→L50×5 需求向量 d (下游IP输入):需求向量 d:L30×3: 10 件L50×5: 15 件L75×6: 8 件L100×8: 2 件L150×10: 1 件─────────────合计: 36 件 / 6 种规格 下游整数规划约束示例:∀i ∈ 规格: Σ a_ki · x_k ≥ d_iL30×3: Σ a_k·x_k ≥ 10L50×5: Σ a_k·x_k ≥ 15L75×6: Σ a_k·x_k ≥ 8L100×8: Σ a_k·x_k ≥ 2L150×10: Σ a_k·x_k ≥ 1 构建一维下料整数规划模型(PuLP)...参考: 北理工《运筹学》第4章整数规划 下料模型求解结果:状态: 1, pulp.apis.coin_api.PULP_CBC_CMD object最优目标值: 9.0 根原材料模式0: 使用 2 次模式1: 使用 2 次模式2: 使用 2 次模式3: 使用 3 次 效率对比:指标 人工Excel 本程序──────────────────────────────────────────────处理耗时 1天 0.3ms同义规格合并 漏(少计6件) 自动合并缺料返工损失 8000元 0可重复性 每次重筛 一键五、README 文件和使用说明5.1 项目结构cutting_demand_builder/├── demand_builder.py # 核心代码单文件~280行├── sample_parts.csv # 示例零件清单├── README.md # 本说明└── requirements.txt # 依赖库5.2 快速上手# 1. 直接运行(核心统计仅用标准库)python demand_builder.py# 2. 安装PuLP以运行下料模型演示pip install pulp# 3. 使用自己的零件CSV# 准备CSV, 修改demo()中的路径:# 字段: order_id,part_id,spec,length,quantity,material,remark5.3 依赖说明# requirements.txt# 核心统计仅用Python标准库, 可直接运行# PuLP为可选依赖(用于构建下料IP演示)pulp2.7.05.4 参数调优指南# 1. 规格标准化规则 — 根据企业实际规格命名习惯扩展# 2. 聚合维度 — 可同时按(spec, material)分组# 3. 容差处理 — 长度相近的规格(如1195mm vs 1200mm)是否合并5.5 扩展建议扩展方向 实现思路Excel直读pandas.read_excel()列生成算法 实现Gilmore-Gomory列生成求解下料IP二维排样 扩展为矩形件排样需求向量余料管理 加入余料库存作为虚拟原材料Web界面 上传BOM→在线统计→下载需求向量六、核心知识点卡片 卡片1需求向量——下料优化的输入密码为什么需求向量d必须准确?┌─────────────────────────────────────────────────────┐│ ││ 下料IP模型: ││ min Σx_k s.t. Σa_ki·x_k ≥ d_i ││ ↑ ││ 需求向量 ││ 如果d_i少了→约束松了→用的原材料少了→实际缺料 ││ 如果d_i多了→约束紧了→浪费原材料 ││ ││ 就像做菜: ││ • d_i 客人要点(必须够) ││ • 点少了→客人吃不饱(缺料) ││ • 点多了→做多了浪费(余料) ││ ││ 北理工教材要点: ││ • §4.1: 整数规划数学模型 ││ • §4.3: 0-1型整数规划(下料模式选择) │└─────────────────────────────────────────────────────┘参考: 北理工《运筹学》第4章整数规划 卡片2规格标准化——同义合并的去重神器为什么50×5和L50×5要合并?┌─────────────────────────────────────────────────────┐│ ││ 车间习惯: ││ • 老师傅写 50×5 ││ • 图纸写 L50×5 ││ • 系统导出 l50x5 ││ 实际都是同一种角钢! ││ ││ 不合并的后果: ││ • d[50×5] 4, d[L50×5] 11 ││ • 下料模型以为要切两种不同东西 ││ • 实际缺料/多切 ││ ││ 标准化: 统一格式 → 自动合并 ││ 50×5 → L50×5 ││ l50x5 → L50×5 ││ L 50×5 → L50×5 ││ ││ 北理工教材要点: ││ • 数据预处理是运筹学建模的第一步 │└─────────────────────────────────────────────────────┘ 卡片3OOP 设计模式速查模式 本项目应用 解决的问题数据类Part,DemandVector,StatsLog 集中管理实体和结果解析器PartParser 封装CSV读取标准化器SpecNormalizer 规格归一化规则聚合器DemandAggregator 清洗聚合逻辑模型构建CuttingStockDemo PuLP下料IP演示报告器DemandReport 解耦计算与展示七、总结与工程师思考7.1 本程序解决了什么问题 人工Excel统计 自动统计管道处理耗时 1天 0.3秒同义规格合并 漏少计6件→缺料返工8000元 自动合并缺料返工损失 8000元 0元可重复性 每次重新手筛 一键重跑7.2 运筹学在工业落地中的难处难点一规格命名的方言问题车间里同一个规格有十几种写法——50×5L50×5l50x5角钢50×5∠50×5。标准化规则永远写不全——需要持续迭代与车间确认。难点二需求数量不是死数BOM上的数量是理论值——实际切割有损耗、有废料、有套裁。需求向量应该是理论数量×(1损耗率)——但损耗率怎么定不同规格不同。难点三从统计到可信计划员第一次看到自动统计结果——你怎么把50×5和L50×5合并了——需要解释标准化逻辑提供明细追溯哪条原始记录被合并到哪个规格。难点四下料IP的组合爆炸需求向量有了但下料IP的模式数是指数级的——6种规格就能产生几十种切割模式。需要列生成算法不是简单枚举。难点五余料库存的历史包袱车间还有上次剩下利AI解决实际问题如果你觉得这个工具好用欢迎关注长安牧笛
返回列表