Skip to content

Latest commit

 

History

History
478 lines (381 loc) · 16.4 KB

File metadata and controls

478 lines (381 loc) · 16.4 KB

Profile Cutting Optimization - Implementation Plan

Confirmed Requirements

  • Input: CSV files for stock inventory and demand list
  • Output: Excel (.xlsx) with 3 sheets, print-ready, timestamped filename
  • UI: Simple GUI (tkinter) with file selectors and parameter fields
  • Algorithm: Best Fit Decreasing (BFD) with local swap optimization
  • Dependencies: openpyxl only

Project Structure

c:\Projects\ProfileCutOptim\
├── src/
│   ├── __init__.py
│   ├── main.py                    # GUI entry point
│   ├── models/
│   │   ├── __init__.py
│   │   ├── stock.py               # StockPiece, StockInventory
│   │   └── demand.py              # DemandItem, DemandList
│   ├── parsers/
│   │   ├── __init__.py
│   │   ├── stock_parser.py        # CSV parser for stock
│   │   └── demand_parser.py       # CSV parser for demands
│   ├── optimizer/
│   │   ├── __init__.py
│   │   ├── cutting_optimizer.py   # Main orchestration
│   │   ├── bfd_algorithm.py       # Best Fit Decreasing
│   │   └── local_optimizer.py     # Swap optimization
│   ├── output/
│   │   ├── __init__.py
│   │   └── excel_writer.py        # Excel generation (3 sheets)
│   └── gui/
│       ├── __init__.py
│       └── app.py                 # Tkinter GUI
├── tests/
│   ├── __init__.py
│   ├── fixtures/                  # Synthetic test data
│   │   ├── stock_basic.csv
│   │   ├── stock_splitting.csv
│   │   ├── demands_basic.csv
│   │   ├── demands_edge_cases.csv
│   │   └── ...
│   ├── test_models.py
│   ├── test_parsers.py
│   ├── test_bfd_algorithm.py
│   ├── test_local_optimizer.py
│   ├── test_cutting_optimizer.py
│   ├── test_excel_writer.py
│   └── test_integration.py
├── requirements.txt
└── README.md

Proposed Changes

Core Models

[NEW] stock.py

@dataclass
class StockPiece:
    part_number: str
    lot_batch: str
    length: float
    length_limit: float | None
    storage_location: str
    uom: str
  • split_if_oversized(): Splits into pieces of length_limit + remainder
  • create_remnant(remaining_length): Creates new piece with "-1" suffix

[NEW] demand.py

@dataclass
class DemandItem:
    order_no: str
    line_no: str
    component_part_number: str
    part_description: str
    lot_size: int
    required_qty: int
    length: float
    demand_date: date
    cut_type: str  # "straight" or "miter"
    angle_left: float
    angle_right: float
    cutting_code: str
    uom: str
  • total_pieces: property returning lot_size × required_qty
  • is_valid: checks length > 0 and required_qty > 0

Parsers

[NEW] stock_parser.py

  • Parse CSV with columns: part_number, lot_batch, length, length_limit, storage_location, uom
  • Handle missing length_limit (default: None = no limit)
  • Return list of StockPiece objects

[NEW] demand_parser.py

  • Parse CSV with all demand columns including part_description, uom
  • Filter by demand_date ≤ today + timespan_days
  • Sort by demand_date ascending (oldest first)
  • Expand to individual pieces (lot_size × required_qty)

Optimization Engine

[NEW] bfd_algorithm.py

Best Fit Decreasing algorithm:

  1. Sort demand pieces by length (descending)
  2. For each piece, find stock with least remaining space that still fits
  3. Account for cut_thickness per cut
  4. Track assignments: {stock_piece: [list of cuts]}

[NEW] local_optimizer.py

Swap optimization:

  1. Iterate through all stock pieces with cuts
  2. Try swapping pieces between stock bars
  3. Accept swap if total waste decreases
  4. Stop when waste < good_enough threshold or no improvement found

[NEW] cutting_optimizer.py

Main orchestration:

  1. Group demands by part_number
  2. For each part number:
    • Run BFD to assign pieces to stock
    • Run local optimizer to reduce waste
    • Group cuts by angle within each stock piece
  3. Track unfulfilled demands with reasons
  4. Generate remnants for partially used stock

Output Generation

[NEW] excel_writer.py

Creates Excel with 3 sheets:

  • Sheet 1: Cutting Plan - Hierarchical format with stock headers, cuts (including Demand Date), remnant/waste
  • Sheet 2: Unfulfilled Demands - Table with order, part, qty, reason
  • Sheet 3: Summary - Waste stats per part number

Cutting Plan Columns: Cut #, Order, Demand Date, Line, Length, Cut Type, L/R Angle, Code, Remaining

Sorting: Plans are sorted based on user-selected sort order (Date or Part Number)

Print formatting:

  • Page orientation: landscape
  • Fit to page width
  • Headers repeat on each page
  • Borders and alternating row colors

GUI

[MODIFY] app.py

Tkinter interface:

  • File browser for Stock Inventory CSV
  • File browser for Demand List CSV
  • Output directory selector
  • Parameter inputs with defaults:
    • Cut Thickness: 3.0
    • Timespan (days): 30
    • Min Remnant Length: 100
    • Good Enough (%): 5
    • Sort By: Dropdown ("Date (Oldest First)" / "Part Number")
  • "Run Optimization" button
  • Progress bar and status label
  • "Open Output Folder" button (enabled after completion)

Sorting Feature:

  • Sort By selection determines order of cutting plans in:
    1. Pattern visualization canvas
    2. Excel output Sheet 1
  • "Date (Oldest First)": Sorts by earliest demand date per stock piece
  • "Part Number": Sorts alphabetically by stock part number

[NEW] Pattern Visualization Component

A canvas-based cutting pattern diagram displayed after optimization:

Layout Structure:

┌─────────────────────────────────────────────────────────────────┐
│                           PATTERN                               │
├─────────────────┬───────────────────────────────────────────────┤
│ PN-001          │ ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓░░░░░░░░░░░░░                 │
│ Steel Profile A │ [ORD-101 ]  [ORD-102] [     ]  ░░░░░░░       │
├─────────────────┼───────────────────────────────────────────────┤
│ PN-001          │ ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓░░░                   │
│ Steel Profile A │ [ORD-103    ]  [ORD-104 ]  ░░░                │
└─────────────────┴───────────────────────────────────────────────┘

Implementation:

  • Use tkinter.Canvas widget for drawing
  • Wrap in tkinter.Frame with vertical scrollbar

Color Palette:

COLORS = [
    '#FFFF00',  # Yellow
    '#FF69B4',  # Pink
    '#90EE90',  # Light Green
    '#87CEEB',  # Sky Blue
    '#DDA0DD',  # Plum
    '#FFB347',  # Orange
    '#98D8C8',  # Teal
    '#F0E68C',  # Khaki
]
WASTE_COLOR = '#C0C0C0'  # Gray

Key Functions:

def draw_pattern(canvas, optimization_result):
    """Draw all stock pieces with their cuts."""
    
def draw_stock_row(canvas, y, stock_plan, color_map):
    """Draw a single stock piece row."""
    
def get_segment_color(part_number, color_map):
    """Get consistent color for a part number."""
    
def fits_text(segment_width, text, font):
    """Check if text fits within segment width."""

Smart Text Visibility:

  • Measure text width using font.measure(text)
  • If segment_width > text_width + padding: draw centered text
  • If too narrow: skip text, rely on tooltip

Hover Tooltips:

  • Bind <Enter> and <Leave> events to segments
  • Show Toplevel window with: Order number, Length, Cut type/angles

Verification Plan

Automated Tests

All tests use pytest with synthetic CSV fixtures.

cd c:\Projects\ProfileCutOptim
python -m pytest tests/ -v --tb=short

Test Module: test_models.py

Test ID Feature Input Expected Output
M1 StockPiece creation Valid data Object created
M2 Stock splitting length=15000, limit=6000 2×6000 + 1×3000
M3 Stock no split needed length=5000, limit=6000 1×5000
M4 Remnant creation lot_batch="LOT-001", remaining=500 lot_batch="LOT-001-1"
M5 DemandItem total_pieces lot_size=5, required_qty=3 15
M6 DemandItem is_valid (valid) length=1000, qty=2 True
M7 DemandItem is_valid (zero length) length=0, qty=2 False
M8 DemandItem is_valid (zero qty) length=1000, qty=0 False

Test Module: test_parsers.py

Test ID Feature Fixture Expected Output
P1 Basic stock parsing stock_basic.csv 3 StockPiece objects
P2 Stock with missing length_limit stock_no_limit.csv length_limit=None
P3 Stock with splitting stock_splitting.csv Correct split pieces
P4 Basic demand parsing demands_basic.csv 5 DemandItem objects
P5 Demand date filtering demands_dated.csv, timespan=7 Only demands within range
P6 Demand sorting demands_unsorted.csv Sorted by demand_date asc
P7 Demand expansion lot_size=2, qty=3 6 individual pieces
P8 Malformed demand (zero length) demands_malformed.csv Flagged as invalid

Fixture: stock_basic.csv

part_number,lot_batch,length,length_limit,storage_location,uom
PN-001,LOT-A001,6000,,Aisle-1-Bin-5,mm
PN-001,LOT-A002,6000,,Aisle-1-Bin-5,mm
PN-002,LOT-B001,4000,4000,Aisle-2-Bin-3,mm

Fixture: demands_basic.csv

order_no,line_no,component_part_number,part_description,lot_size,required_qty,length,demand_date,cut_type,angle_left,angle_right,cutting_code,uom
ORD-101,1,PN-001,Steel Profile A,1,2,1500,2026-01-20,straight,0,0,,mm
ORD-102,1,PN-001,Steel Profile A,1,1,2000,2026-01-22,miter,45,0,MC-001,mm
ORD-103,1,PN-002,Steel Profile B,2,2,800,2026-01-25,straight,0,0,,mm

Test Module: test_bfd_algorithm.py

Test ID Feature Input Expected Output
B1 Single piece fits Stock: 6000mm, Demand: 1500mm, kerf=3 Assigned, remaining=4497
B2 Multiple pieces one stock Stock: 6000mm, Demands: 1500+2000+1200 All fit, waste calculated
B3 Piece doesn't fit Stock: 1000mm, Demand: 1500mm Unfulfilled
B4 Best fit selection Stocks: 6000, 3000. Demand: 2500 Assigned to 3000 (least waste)
B5 Kerf accumulation Stock: 6000, 3×2000mm pieces 6000 - (3×2000 + 3×3) = -9 → only 2 fit
B6 Empty demands Stock: 6000, Demands: [] No assignments
B7 Empty stock Stock: [], Demands: [1500] All unfulfilled

Test Module: test_local_optimizer.py

Test ID Feature Input Expected Output
L1 No improvement possible Optimal assignment Same assignment
L2 Swap reduces waste Suboptimal assignment Improved assignment
L3 Good enough threshold Waste=4%, threshold=5% Stops immediately
L4 Multiple swap iterations Complex assignment Converges to better solution

Test Module: test_cutting_optimizer.py

Test ID Feature Input Expected Output
C1 Full workflow Basic stock + demands Cutting plan + summary
C2 Part number grouping 2 part numbers Separate optimization per PN
C3 Angle grouping Miter + straight cuts Grouped by angle in output
C4 Partial fulfillment Demand=10, Stock fits 8 8 fulfilled, 2 unfulfilled
C5 Remnant generation Stock partially used Remnant with -1 suffix
C6 Min remnant threshold Remaining=50, threshold=100 Marked as waste, not remnant
C7 No stock for part number Demand for PN-999 Unfulfilled "No stock exists"
C8 Demand too long Demand=8000, all stock≤6000 Unfulfilled "no stock long enough"

Test Module: test_excel_writer.py

Test ID Feature Input Expected Output
E1 File creation Valid cutting plan .xlsx file created
E2 Sheet 1 structure Cutting plan data Correct hierarchical format
E3 Sheet 2 structure Unfulfilled list Correct columns
E4 Sheet 3 structure Summary data Waste % calculated correctly
E5 Timestamped filename - Format: cutting_plan_YYYY-MM-DD_HHMMSS.xlsx
E6 Print formatting - Landscape, fit to width

Test Module: test_integration.py

Test ID Scenario Fixtures Validation
I1 Happy path Basic stock + demands Complete Excel output verified
I2 All edge cases Edge case fixtures All 15 business rules validated
I3 Large dataset 100 stock, 500 demands Completes in <5 seconds
I4 Empty inputs Empty CSVs Graceful handling, empty output

Manual Test Cases

Note

These require visual inspection of Excel output and GUI behavior.

MT1: GUI File Selection

Steps:

  1. Launch application
  2. Click "Browse" for Stock Inventory
  3. Select a CSV file
  4. Click "Browse" for Demand List
  5. Select a CSV file

Expected: File paths displayed in text fields

MT2: GUI Parameter Validation

Steps:

  1. Enter negative value for Cut Thickness
  2. Enter non-numeric value for Timespan
  3. Click "Run Optimization"

Expected: Error message, optimization does not run

MT3: Excel Print Preview

Steps:

  1. Run optimization with sample data
  2. Open generated Excel file
  3. Go to Print Preview (Ctrl+P)

Expected:

  • Landscape orientation
  • Fits on page width
  • Headers visible on each page
  • No manual adjustment needed

MT4: GUI Progress Feedback

Steps:

  1. Run optimization with large dataset (500+ demands)

Expected:

  • Progress bar updates
  • Status shows current step
  • UI remains responsive

MT5: Open Output Folder

Steps:

  1. Complete optimization
  2. Click "Open Output Folder"

Expected: File explorer opens to output directory with generated file visible

MT6: Pattern Visualization Display

Steps:

  1. Run optimization with sample data
  2. Observe the pattern diagram in the GUI

Expected:

  • "PATTERN" header visible above diagram
  • Part number and description shown on left of each row
  • Colored segments represent cuts (proportional width)
  • Gray segments at end represent waste/remnant
  • Order numbers visible in wider segments
  • Narrow segments have no text

MT7: Pattern Hover Tooltips

Steps:

  1. Run optimization with sample data
  2. Hover mouse over a narrow segment in the pattern
  3. Hover over a wide segment

Expected:

  • Tooltip appears showing: Order number, Length, Cut type/angles
  • Tooltip follows cursor or appears near segment
  • Tooltip disappears when mouse leaves segment

Synthetic Test Data Generator

Create tests/generate_fixtures.py to generate test CSVs programmatically:

def generate_stock_csv(filename, num_pieces, part_numbers, length_range):
    """Generate synthetic stock inventory."""
    
def generate_demands_csv(filename, num_demands, part_numbers, date_range):
    """Generate synthetic demands with various cut types and angles."""

This ensures repeatable, version-controlled test data.


Dependencies

[NEW] requirements.txt

openpyxl>=3.1.0
pytest>=7.0.0

Ready for Implementation

All requirements documented with comprehensive test coverage. Ready to proceed.