:從算法選型到利用率優(yōu)化)
簡介這是一份面向制造業(yè)與CAD/CAM開發(fā)者的2D板材排版套料開源代碼聚焦激光切割、水切割等場景下的排樣優(yōu)化問題。項目以遺傳算法為核心通過選擇、交叉與變異迭代求解板材利用率近似最優(yōu)解適合具備一定前端與算法基礎(chǔ)、希望研究自動排樣系統(tǒng)的技術(shù)人員。壓縮包為rar格式共88個文件、約2.53MB以JavaScript源碼為主體輔以SVG矢量圖形、字體文件eot/woff/ttf/woff2、CSS樣式與HTML頁面另含少量配置與說明文檔整體結(jié)構(gòu)接近一個可直接運行的Web應(yīng)用。目前已有6261人學(xué)習(xí)下載。讀者可從中獲取完整的遺傳算法排樣實現(xiàn)、SVG圖形解析與布局渲染邏輯、適應(yīng)度評估與并行計算模塊以及可二次定制的工程骨架便于理解算法落地細(xì)節(jié)并擴(kuò)展切割工藝約束。1. 2D板材排版套料開源代碼從一張鋼板到一份可執(zhí)行排樣方案車間里最常見的浪費不是切壞了而是切剩了。一張 2440×1220 的板材排得好能出 12 個件排得差只能出 9 個中間差的不是手藝是算法。2D板材排版套料開源代碼要解決的就是這件事在給定板材尺寸、零件形狀、數(shù)量、紋理方向和間距約束下自動算出怎么擺最省料。它適合三類人——做激光切割/等離子下料的工藝員、想給自家 MES 或 CAM 加排樣模塊的開發(fā)者、以及做鈑金家具或廣告字牌的小廠技術(shù)負(fù)責(zé)人。你不需要從零推導(dǎo)幾何算法開源社區(qū)里已經(jīng)有可跑的骨架關(guān)鍵是知道選哪套、怎么接自己的數(shù)據(jù)、參數(shù)怎么調(diào)才不翻車。2. 套料問題的數(shù)學(xué)骨架與開源方案選型2.1 排樣問題到底難在哪NP-hard 與三類約束2D 排樣在學(xué)術(shù)上叫 2D Irregular Strip Packing 或 2D Bin Packing前者允許零件旋轉(zhuǎn)、板材無限長后者板材固定、零件數(shù)量有限。鈑金下料多數(shù)落在后者板材尺寸固定零件數(shù)量確定目標(biāo)是最小化板材張數(shù)或最大化利用率。它的難度在于組合爆炸——10 個零件有 10! 種排列100 個零件就是天文數(shù)字所以沒有多項式時間的精確解工程上全靠啟發(fā)式加局部搜索。約束分三類必須先在代碼里建模清楚幾何約束零件之間不重疊、不超出板材邊界、零件可旋轉(zhuǎn)角度集合常見 0°/90°也有 0°/180° 或任意角。工藝約束零件間距kerf 補(bǔ)償、邊距、紋理方向鎖定、共邊切割是否允許。數(shù)量約束每種零件需求數(shù)量、可混排的板材規(guī)格集合。很多開源代碼跑出來結(jié)果難看不是算法差是約束沒建模對。比如忘了設(shè)間距排出來零件貼在一起實際切割直接報廢。2.2 開源方案怎么選三類技術(shù)路線對比目前能拿到的 2D 套料開源代碼大致分三類選型時按你的零件形狀復(fù)雜度決定路線代表思路適合形狀利用率上限上手難度矩形裝箱MaxRects / Guillotine全是矩形件85%~92%低多邊形 NestingNFP 啟發(fā)式任意多邊形88%~95%中高柵格/像素法位圖布爾運算異形、含孔80%~88%中矩形件占多數(shù)的鈑金廠直接用 MaxRects 類實現(xiàn)就夠了代碼短、跑得快。異形件比如法蘭、支架必須上 NFPNo-Fit Polygon臨界多邊形路線核心是先算兩兩零件的不重疊可行域再用啟發(fā)式在可行域里選落點。柵格法實現(xiàn)簡單但精度受網(wǎng)格分辨率限制分辨率調(diào)高內(nèi)存爆炸調(diào)低邊緣鋸齒導(dǎo)致實際切不出來一般只做快速預(yù)覽。我一般建議先用矩形裝箱跑通數(shù)據(jù)鏈路確認(rèn) DXF 解析、數(shù)量匯總、結(jié)果導(dǎo)出沒問題再換 NFP 內(nèi)核。這樣出問題時能快速定位是數(shù)據(jù)層還是算法層。2.3 最小可跑骨架數(shù)據(jù)流與模塊劃分一套能落地的套料代碼模塊劃分比算法本身更重要。常見做法是拆成四層# nesting_pipeline.py # 套料主流程骨架解析 - 預(yù)處理 - 排樣 - 導(dǎo)出 from dxf_parser import load_parts # DXF 解析輸出多邊形頂點 from preprocess import normalize, merge_duplicates # 單位統(tǒng)一、重復(fù)件合并 from nester import MaxRectsNester # 排樣內(nèi)核可替換為 NFP from exporter import to_dxf, to_csv # 結(jié)果導(dǎo)出 def run(dxf_dir, sheet_w, sheet_h, gap, allow_rotate): parts load_parts(dxf_dir) # 每個零件: {id, poly, qty, rotatable} parts normalize(parts, unitmm) # 統(tǒng)一到毫米避免英寸混入 parts merge_duplicates(parts) # 相同幾何合并數(shù)量減少搜索規(guī)模 nester MaxRectsNester( sheet(sheet_w, sheet_h), gapgap, # 零件間距等于割縫安全余量 allow_rotateallow_rotate # 是否允許 90 度旋轉(zhuǎn) ) layout nester.pack(parts) to_dxf(layout, out/nest_result.dxf) to_csv(layout, out/report.csv) # 利用率、板材張數(shù)、每張擺放清單 return layout if __name__ __main__: run(./parts, 2440, 1220, gap3.0, allow_rotateTrue)邏輯說明load_parts負(fù)責(zé)把 DXF 里的 LWPOLYLINE、CIRCLE 等實體轉(zhuǎn)成閉合多邊形這一步最容易出問題圓弧離散精度直接決定后續(xù)幾何計算是否穩(wěn)定。normalize統(tǒng)一單位很多翻車案例是圖紙里混了英寸和毫米。merge_duplicates把幾何相同的零件合并數(shù)量能把搜索規(guī)模降一個量級。MaxRectsNester是內(nèi)核接口設(shè)計成pack(parts) - layout方便你后面換成 NFP 實現(xiàn)而不動上下游。參數(shù)說明gap不是隨便填的它等于割縫寬度加安全余量激光切割一般 0.2~0.5mm 割縫但熱變形和定位誤差要留 2~3mm所以實際填 3.0 是常見值。allow_rotate對紋理件必須設(shè) False否則排出來紋理方向亂了外觀件直接報廢。3. 用 NFP 內(nèi)核把異形件排樣跑起來3.1 NFP 的計算流程與關(guān)鍵幾何操作NFP 的核心思想對零件 A 和 B把所有讓 B 與 A 剛好接觸的位置連起來形成一條閉合邊界B 的參考點落在這條邊界外就說明不重疊。有了 NFP排樣就變成在可行域里找最左下角或最省空間的落點。計算流程分四步多邊形預(yù)處理去自交、統(tǒng)一繞向逆時針、凸化分解凹多邊形拆成凸塊。兩兩算 NFP常用 Minkowski 和凸多邊形用頂點歸并凹多邊形先分解再合并。求可行域板材邊界內(nèi)縮 gap減去已放置零件的 NFP 并集。選落點按啟發(fā)式最低重心、最左、最小包圍盒增量在可行域頂點里挑。凹多邊形分解是性能瓶頸也是精度陷阱。分解算法選不好NFP 會出現(xiàn)自交排出來的零件看著不重疊實際有微小穿插切割時才發(fā)現(xiàn)。3.2 落點啟發(fā)式與搜索順序利用率差 5% 的地方同樣的 NFP落點策略不同利用率能差 5 個百分點。常見啟發(fā)式Bottom-Left選最低再最左的可行點實現(xiàn)簡單適合矩形件。Bottom-Left-Fill在 Bottom-Left 基礎(chǔ)上優(yōu)先填洞異形件提升明顯。Minimum Bounding Box選讓整體包圍盒增量最小的點適合板材張數(shù)敏感場景。搜索順序同樣關(guān)鍵。零件按面積從大到小排是常規(guī)做法但純面積排序會讓小件最后沒地方塞。我一般用面積降序 長寬比分組把細(xì)長件單獨一批避免它們在大件縫隙里卡住。# nester.py 片段落點選擇與搜索順序 def sort_parts(parts): # 面積降序長寬比接近的歸為一組減少碎片 return sorted(parts, keylambda p: (-p.area, abs(p.aspect - 1.0))) def select_position(feasible_points, placed, sheet): best None for pt in feasible_points: # 優(yōu)先最低其次最左最后看包圍盒增量 score (pt.y, pt.x, bbox_growth(pt, placed, sheet)) if best is None or score best[0]: best (score, pt) return best[1]邏輯說明sort_parts先按面積降序再按長寬比與 1 的差值升序讓接近正方形的件優(yōu)先細(xì)長件靠后減少它們制造窄縫的概率。select_position用元組比較實現(xiàn)多級排序Python 里元組逐項比較天然支持最低優(yōu)先、再最左、再包圍盒增量最小。參數(shù)說明feasible_points是可行域頂點集合數(shù)量隨零件數(shù)增長超過 200 個零件時要做剪枝只保留 Pareto 前沿上的點否則單次選點就卡住。bbox_growth是包圍盒增量值越小說明這個落點讓整體越緊湊。3.3 結(jié)果校驗怎么確認(rèn)排樣真的能切排樣結(jié)果不能只看利用率數(shù)字必須做幾何校驗。常見做法是寫一個獨立校驗?zāi)_本把排樣結(jié)果里所有零件多邊形做兩兩布爾交集交集面積大于閾值就報錯。# verify.py排樣結(jié)果幾何校驗 from shapely.geometry import Polygon from shapely.ops import unary_union def verify(layout, sheet_w, sheet_h, gap, tol1e-6): errors [] for i, a in enumerate(layout.parts): pa Polygon(a.poly) # 邊界檢查 if not Polygon([(0,0),(sheet_w,0),(sheet_w,sheet_h),(0,sheet_h)]).contains(pa): errors.append(f零件 {a.id} 超出板材邊界) # 重疊檢查間距不足也算錯 for b in layout.parts[i1:]: pb Polygon(b.poly) inter pa.buffer(gap/2).intersection(pb.buffer(gap/2)) if inter.area tol: errors.append(f零件 {a.id} 與 {b.id} 間距不足重疊面積 {inter.area:.3f}) return errors邏輯說明用buffer(gap/2)把兩個零件各外擴(kuò)半個間距再求交集這樣能把間距不足也納入重疊檢查比單純判重疊更嚴(yán)格。contains檢查邊界注意浮點誤差必要時給板材邊界加tol容差。參數(shù)說明tol是面積容差設(shè) 1e-6 是防止浮點誤差誤報。gap要和排樣時一致否則校驗標(biāo)準(zhǔn)對不上。這個腳本建議每次排樣后自動跑作為出圖前的最后一道關(guān)。4. 避坑套料代碼落地最常見的 5 個翻車點4.1 圓弧離散精度不夠?qū)е铝慵比猬F(xiàn)象排樣結(jié)果看著正常導(dǎo)出 DXF 后零件邊緣是折線圓弧處明顯缺一塊切割出來尺寸超差。原因DXF 解析時把圓弧按固定段數(shù)離散段數(shù)太少曲率大的地方弦高誤差大。很多開源解析器默認(rèn) 16 段對 R5 的小圓弧誤差能到 0.1mm 以上。解決按弦高誤差控制離散而不是固定段數(shù)。弦高誤差取 0.01~0.02mm公式n ceil(pi / acos(1 - tol/r))r 越小段數(shù)越多。解析后立刻算多邊形面積和 DXF 里圓弧理論面積對比偏差超過 0.5% 就報警。4.2 單位混用英寸圖紙混進(jìn)毫米板材現(xiàn)象排樣利用率異常低零件小得離譜一張板上排了幾百個件。原因部分 DXF 用英寸解析后沒換算零件尺寸變成實際的 1/25.4自然排得下。解決解析時讀 DXF 的$INSUNITS頭變量為 1 是英寸、4 是毫米按它換算。讀不到就按包圍盒尺寸猜鈑金件包圍盒通常 10~2000mm如果算出來普遍小于 10大概率是英寸。統(tǒng)一在normalize里處理下游不再關(guān)心單位。4.3 間距設(shè)成 0 或只設(shè)割縫現(xiàn)象排樣利用率很高但切割后零件邊緣有粘連需要二次打磨。原因gap只填了割縫寬度0.2mm沒留熱變形和定位余量。激光切割熱影響區(qū)、等離子切割掛渣、板材不平整都會吃掉余量。解決gap 割縫 工藝余量激光取 2~3mm等離子取 4~6mm火焰切割取 6~8mm。這個值按你的設(shè)備和材料試切確定不要照搬別人的參數(shù)。4.4 紋理方向沒鎖死外觀件報廢現(xiàn)象不銹鋼拉絲件排樣后紋理方向不一致成品外觀有明顯色差。原因allow_rotateTrue讓零件旋轉(zhuǎn)了 90°紋理跟著轉(zhuǎn)。解決對紋理件單獨分組allow_rotateFalse并且限制旋轉(zhuǎn)角度集合為{0}。如果必須混排把紋理件排在同一張板上且方向一致非紋理件排另一張。代碼里給零件加grain_locked字段排樣前按它分組。4.5 大零件優(yōu)先導(dǎo)致小件無處可放現(xiàn)象前幾張板排得很滿最后剩一堆小件單獨開一張板利用率只有 30%。原因純面積降序讓大件先占滿小件最后只能在剩余碎片里找位置碎片不夠就開新板。解決用分層排樣——先排大件預(yù)留一部分區(qū)域給小件或者用面積降序 小件預(yù)分配按面積把零件分成大中小三檔每張板按比例混排。更簡單的做法是最后一張板專門排小件用 Bottom-Left-Fill 填洞利用率能拉回 70% 以上。5. 把利用率再壓 3%參數(shù)調(diào)優(yōu)與批量驗證的實操技巧排樣代碼跑通只是起點真正拉開差距的是參數(shù)調(diào)優(yōu)和批量驗證。我一般按這個順序壓利用率第一步固定幾何內(nèi)核只調(diào)搜索順序。把sort_parts的排序鍵從純面積換成面積 長寬比 周長跑 20 組真實訂單看平均利用率變化。周長大的件優(yōu)先能減少窄縫。這一步通常能提 1%~2%。第二步調(diào)落點啟發(fā)式的權(quán)重。select_position里的(pt.y, pt.x, bbox_growth)元組順序可以換試試(bbox_growth, pt.y, pt.x)對板材張數(shù)敏感的場景更有效。不要憑感覺寫個批量腳本跑對比# batch_eval.py批量對比不同參數(shù)組合的利用率 import itertools, csv from nesting_pipeline import run orders [order_01, order_02, order_03] # 真實訂單目錄 strategies [ {sort: area, pos: bottom_left}, {sort: area_ar, pos: bottom_left}, {sort: area_ar, pos: bbox_growth}, ] results [] for order, strat in itertools.product(orders, strategies): layout run(f./orders/{order}, 2440, 1220, gap3.0, allow_rotateTrue, strategystrat) results.append({ order: order, strategy: strat, util: layout.utilization, # 利用率 sheets: layout.sheet_count, # 板材張數(shù) }) with open(eval.csv, w, newline) as f: writer csv.DictWriter(f, fieldnames[order,strategy,util,sheets]) writer.writeheader() writer.writerows(results)邏輯說明用itertools.product遍歷訂單和策略組合每個組合跑一次排樣記錄利用率和板材張數(shù)。strategy字典傳給run由它分發(fā)給sort_parts和select_position。跑完看eval.csv按訂單分組對比選每個訂單的最優(yōu)策略或者選全局平均最優(yōu)。參數(shù)說明gap固定 3.0 是為了公平對比調(diào)策略時不要動工藝參數(shù)。util是面積利用率sheets是板材張數(shù)實際生產(chǎn)更關(guān)心sheets因為每開一張板都是成本。如果兩個策略利用率差 0.5% 但板材張數(shù)一樣選跑得快的那個。第三步做敏感性分析。把gap從 2 到 6 每隔 1mm 跑一遍看利用率下降曲線。如果 gap 從 3 加到 4 利用率只掉 0.3%說明你的排樣對間距不敏感可以適當(dāng)加大 gap 換切割穩(wěn)定性。如果掉 2% 以上說明排樣太緊湊要考慮換內(nèi)核或調(diào)搜索順序。第四步建立回歸測試集。挑 10~20 個有代表性的歷史訂單每次改代碼后跑一遍記錄利用率和板材張數(shù)。利用率掉超過 1% 或板材張數(shù)增加就回滾。這個習(xí)慣能防止你為了優(yōu)化某個訂單把整體效果搞壞。最后說個血淚經(jīng)驗不要迷信利用率數(shù)字。我見過排樣利用率 94% 但實際切割報廢率 8% 的方案原因是排得太緊熱變形導(dǎo)致相鄰零件粘連。真正該盯的指標(biāo)是每張板的實際成品數(shù)這個數(shù)要拿試切結(jié)果校準(zhǔn)。排樣代碼是工具工藝參數(shù)才是底線。希望幫到你。本文還有配套的精品資源點擊獲取