Edit this Page

Optimal Cutting Calculator - Python


Optimal Cutting Calculator - Python

This is a basic application to calculate how to cut pieces from stock. It can be used to cut lumber or iron rods, for your wood/iron projects. It's simple but useful. It's vibe coded, cause i wanted to calculate the cuts for a project of mine and didn't have the time to program it my self. I tested it with other online tools and gives the same results. This can either mean that the script is OK or also the other tools were vibe coded :P

Anyway... here it is.

#!/usr/bin/python3
import sys
from itertools import combinations
from collections import defaultdict

def get_positive_float(prompt):
    """Get a positive float from user input"""
    while True:
        try:
            value = float(input(prompt))
            if value >= 0:
                return value
            print("Please enter a positive number.")
        except ValueError:
            print("Invalid input. Please enter a number.")

def get_positive_int(prompt):
    """Get a positive integer from user input"""
    while True:
        try:
            value = int(input(prompt))
            if value >= 0:
                return value
            print("Please enter a positive integer.")
        except ValueError:
            print("Invalid input. Please enter an integer.")

def find_best_cut_pattern(stock_length, pieces_to_cut, kerf=0):
    """
    Find the best combination of pieces that fit in one stock length.
    Uses dynamic programming approach for optimal packing.
    """
    # Convert pieces to list of (length, id) for tracking
    piece_list = []
    for length, quantity in pieces_to_cut.items():
        for i in range(quantity):
            piece_list.append((length, length))

    # Sort by length descending for better packing
    piece_list.sort(reverse=True)

    # Dynamic programming to find best combination
    # dp[i] = (max pieces, min waste, list of piece indices)
    n = len(piece_list)
    best_result = None
    best_waste = float('inf')
    best_count = 0

    # Try different combinations using recursive approach with memoization
    def try_combinations(index, current_length, current_pieces):
        nonlocal best_result, best_waste, best_count

        if current_length > stock_length:
            return

        # Check if this is better than current best
        waste = stock_length - current_length
        count = len(current_pieces)

        # Prefer more pieces, then less waste
        if count > best_count or (count == best_count and waste < best_waste):
            best_count = count
            best_waste = waste
            best_result = current_pieces.copy()

        if index >= n:
            return

        # Try including current piece
        if current_length + piece_list[index][0] + kerf <= stock_length:
            current_pieces.append(index)
            try_combinations(index + 1, current_length + piece_list[index][0] + kerf, current_pieces)
            current_pieces.pop()

        # Try skipping current piece
        try_combinations(index + 1, current_length, current_pieces)

    try_combinations(0, 0, [])

    if best_result is None:
        return None, piece_list

    # Return the lengths of pieces in best combination
    selected_pieces = [piece_list[i][0] for i in best_result]
    return selected_pieces, piece_list

def calculate_cutting_plan(stock_length, pieces_needed, kerf=0):
    """
    Calculate the optimal cutting plan.
    Returns list of cuts for each stock piece.
    """
    # Make a copy of pieces needed
    remaining_pieces = pieces_needed.copy()
    cutting_plan = []

    while any(qty > 0 for qty in remaining_pieces.values()):
        # Find best combination for one stock piece
        pieces_available = {k: v for k, v in remaining_pieces.items() if v > 0}

        if not pieces_available:
            break

        # Try to find best pattern
        best_pattern = find_optimal_pattern(stock_length, pieces_available, kerf)

        if not best_pattern:
            # Can't fit any more pieces
            break

        # Remove used pieces from remaining
        for piece_length in best_pattern:
            remaining_pieces[piece_length] -= 1

        cutting_plan.append(best_pattern)

    return cutting_plan, remaining_pieces

def find_optimal_pattern(stock_length, pieces_available, kerf=0):
    """
    Find the optimal pattern of cuts for a single stock piece.
    Uses a greedy approach with lookahead.
    """
    # Convert to list of (length, quantity)
    pieces_list = [(length, qty) for length, qty in pieces_available.items()]
    pieces_list.sort(reverse=True)  # Sort by length descending

    best_pattern = []
    best_waste = stock_length
    best_count = 0

    # Try greedy approach - always take the longest piece that fits
    def greedy_fill(remaining_length, available_pieces):
        pattern = []
        remaining = available_pieces.copy()

        # Keep taking pieces while they fit
        changed = True
        while changed:
            changed = False
            for length in sorted(remaining.keys(), reverse=True):
                if remaining[length] > 0 and length + kerf <= remaining_length:
                    pattern.append(length)
                    remaining[length] -= 1
                    remaining_length -= (length + kerf)
                    changed = True
                    break

        return pattern

    # Try different starting pieces
    for start_length in sorted(pieces_available.keys(), reverse=True):
        if pieces_available[start_length] > 0 and start_length + kerf <= stock_length:
            # Create a temporary copy with one less of start piece
            temp_available = pieces_available.copy()
            temp_available[start_length] -= 1

            # Greedily fill the rest
            pattern = [start_length]
            remaining_length = stock_length - start_length - kerf
            rest_pattern = greedy_fill(remaining_length, temp_available)
            pattern.extend(rest_pattern)

            waste = stock_length - sum(pattern) - kerf * len(pattern)
            count = len(pattern)

            # Choose pattern with most pieces, then least waste
            if count > best_count or (count == best_count and waste < best_waste):
                best_count = count
                best_waste = waste
                best_pattern = pattern

    return best_pattern if best_pattern else None

def display_results(stock_length, cutting_plan, remaining_pieces, kerf):
    """Display the cutting plan in a readable format"""
    print("\n" + "="*60)
    print("OPTIMAL CUTTING PLAN")
    print("="*60)

    if not cutting_plan:
        print("No cuts can be made with the given stock length!")
        return

    total_stock_used = len(cutting_plan)
    total_waste = 0
    total_pieces = 0

    print(f"\nStock length: {stock_length}m")
    if kerf > 0:
        print(f"Kerf (blade width): {kerf}m")

    print(f"\nNumber of {stock_length}m pieces needed: {total_stock_used}")
    print("-"*60)

    for i, cuts in enumerate(cutting_plan, 1):
        cuts_used = sum(cuts)
        kerf_total = kerf * len(cuts)
        waste = stock_length - cuts_used - kerf_total
        total_waste += waste
        total_pieces += len(cuts)

        cuts_str = " + ".join(f"{c}m" for c in cuts)
        print(f"Piece {i}: {cuts_str}")
        print(f"         Used: {cuts_used}m, Waste: {waste:.3f}m, Pieces: {len(cuts)}")

    print("-"*60)
    print(f"\nSUMMARY:")
    print(f"  Total {stock_length}m pieces needed: {total_stock_used}")
    print(f"  Total pieces cut: {total_pieces}")
    print(f"  Total material used: {total_pieces * 0 + sum(sum(cuts) for cuts in cutting_plan):.3f}m")
    print(f"  Total waste: {total_waste:.3f}m")
    print(f"  Waste percentage: {(total_waste / (total_stock_used * stock_length) * 100):.2f}%")

    if remaining_pieces and any(qty > 0 for qty in remaining_pieces.values()):
        print(f"\nWARNING: Some pieces could not be cut:")
        for length, qty in remaining_pieces.items():
            if qty > 0:
                print(f"  - {qty} piece(s) of {length}m")

    print("="*60)

def main():
    print("="*60)
    print("LUMBER CUTTING OPTIMIZER")
    print("="*60)
    print("\nThis program calculates the optimal way to cut lumber")
    print("to minimize waste.\n")

    # Get stock length
    stock_length = get_positive_float("Enter the length of stock pieces (e.g., 6 for 6m): ")

    # Get kerf (optional)
    use_kerf = input("\nDo you want to account for blade width (kerf)? (y/n): ").lower().strip()
    kerf = 0
    if use_kerf == 'y':
        kerf = get_positive_float("Enter kerf/blade width (e.g., 0.003 for 3mm): ")

    # Get pieces needed
    pieces_needed = defaultdict(float)
    print("\nEnter the pieces you need to cut.")
    print("(Enter 0 for length when done)\n")

    while True:
        length = get_positive_float("Enter piece length (or 0 to finish): ")
        if length == 0:
            break

        if length > stock_length:
            print(f"  Warning: {length}m is longer than stock length {stock_length}m!")
            continue

        quantity = get_positive_int(f"Enter quantity of {length}m pieces: ")
        pieces_needed[length] += quantity

    if not pieces_needed:
        print("No pieces entered. Exiting.")
        return

    # Show input summary
    print("\n" + "-"*40)
    print("Pieces to cut:")
    total_pieces_needed = 0
    total_length_needed = 0
    for length, qty in sorted(pieces_needed.items(), reverse=True):
        print(f"  {int(qty)} x {length}m = {qty * length:.3f}m")
        total_pieces_needed += qty
        total_length_needed += qty * length

    print(f"\nTotal pieces: {int(total_pieces_needed)}")
    print(f"Total length needed: {total_length_needed:.3f}m")
    print(f"Minimum stock pieces needed: {int(total_length_needed / stock_length) + 1}")

    # Calculate cutting plan
    print("\nCalculating optimal cutting plan...")
    cutting_plan, remaining = calculate_cutting_plan(stock_length, dict(pieces_needed), kerf)

    # Display results
    display_results(stock_length, cutting_plan, remaining, kerf)

if __name__ == "__main__":
    try:
        main()
    except KeyboardInterrupt:
        print("\n\nProgram interrupted. Goodbye!")
        sys.exit(0)

Add a comment

Previous