import os import json import pandas as pd import io import logging from flask import Flask, render_template, request, jsonify, send_file from werkzeug.exceptions import HTTPException # Configure logging logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) app = Flask(__name__) app.config['MAX_CONTENT_LENGTH'] = 16 * 1024 * 1024 # 16MB max upload # Global Error Handler @app.errorhandler(Exception) def handle_exception(e): # Pass through HTTP errors if isinstance(e, HTTPException): return jsonify(error=str(e)), e.code # Log non-HTTP errors logger.error(f"Unhandled Exception: {e}", exc_info=True) return jsonify(error=f"Internal Server Error: {str(e)}"), 500 # Core Logic: 1D Cutting Stock Optimization def optimize_cutting(stock_sizes, requirements, kerf_width=0): """ stock_sizes: List of available stock lengths [6000, 3000] requirements: List of dicts [{'length': 500, 'quantity': 10}, ...] kerf_width: Blade width (loss per cut) """ try: # 1. Expand requirements into individual items items = [] for req in requirements: qty = int(req.get('quantity', 0)) if qty <= 0: continue for _ in range(qty): items.append({ 'length': float(req['length']), 'id': req.get('label', '') }) # 2. Sort items descending (First Fit Decreasing heuristic) items.sort(key=lambda x: x['length'], reverse=True) # 3. Initialize used stocks used_stocks = [] # Sort stock sizes to facilitate Best Fit stock_sizes.sort() unfitted_items = [] for item in items: item_len = item['length'] placed = False # Try to fit in existing used stocks (First Fit) for stock in used_stocks: needed = item_len + (kerf_width if stock['cuts'] else 0) if stock['remaining'] >= needed: stock['cuts'].append({ 'length': item_len, 'label': item['id'] }) stock['remaining'] -= needed placed = True break # If not placed, try to open a new stock if not placed: best_stock_len = None for s_len in stock_sizes: if s_len >= item_len: best_stock_len = s_len break if best_stock_len: used_stocks.append({ 'length': best_stock_len, 'cuts': [{'length': item_len, 'label': item['id']}], 'remaining': best_stock_len - item_len }) placed = True else: unfitted_items.append(item) # 4. Calculate Statistics total_stock_length = sum(s['length'] for s in used_stocks) total_parts_length = sum(c['length'] for s in used_stocks for c in s['cuts']) total_waste = total_stock_length - total_parts_length waste_percent = (total_waste / total_stock_length * 100) if total_stock_length > 0 else 0 return { 'solution': used_stocks, 'unfitted': unfitted_items, 'stats': { 'total_stock_used': len(used_stocks), 'total_length_consumed': total_stock_length, 'total_parts_length': total_parts_length, 'waste_length': total_waste, 'waste_percent': round(waste_percent, 2), 'efficiency': round(100 - waste_percent, 2) } } except Exception as e: logger.error(f"Optimization error: {e}") raise @app.route('/') def index(): return render_template('index.html') @app.route('/api/optimize', methods=['POST']) def api_optimize(): try: data = request.json if not data: return jsonify({'error': 'Invalid JSON data'}), 400 stock_sizes = [float(x) for x in data.get('stock_sizes', [])] requirements = data.get('requirements', []) kerf = float(data.get('kerf', 0)) if not stock_sizes: return jsonify({'error': '未提供原材料尺寸 (No stock sizes provided)'}), 400 result = optimize_cutting(stock_sizes, requirements, kerf) return jsonify(result) except ValueError as e: return jsonify({'error': f'数据格式错误: {str(e)}'}), 400 except Exception as e: return jsonify({'error': f'计算出错: {str(e)}'}), 500 @app.route('/api/import', methods=['POST']) def api_import(): try: if 'file' not in request.files: return jsonify({'error': 'No file uploaded'}), 400 file = request.files['file'] if file.filename == '': return jsonify({'error': 'No file selected'}), 400 if file.filename.endswith('.json'): data = json.load(file) return jsonify(data) elif file.filename.endswith(('.xlsx', '.xls')): # Assume headers: Length, Quantity, Label df = pd.read_excel(file) requirements = [] # Try to map columns cols = df.columns.astype(str).str.lower() len_col = next((c for c in cols if 'len' in c or '长' in c), None) qty_col = next((c for c in cols if 'qty' in c or 'num' in c or 'count' in c or '数' in c), None) lbl_col = next((c for c in cols if 'lbl' in c or 'label' in c or 'rem' in c or 'id' in c or '注' in c or '号' in c), None) if not len_col or not qty_col: # Fallback to index 0, 1, 2 if len(df.columns) >= 2: len_col = df.columns[0] qty_col = df.columns[1] lbl_col = df.columns[2] if len(df.columns) > 2 else None else: return jsonify({'error': 'Excel must have at least Length and Quantity columns'}), 400 for _, row in df.iterrows(): try: l = float(row[len_col]) q = int(row[qty_col]) if l > 0 and q > 0: requirements.append({ 'length': l, 'quantity': q, 'label': str(row[lbl_col]) if lbl_col else '' }) except: continue return jsonify({'requirements': requirements}) return jsonify({'error': 'Unsupported file format'}), 400 except Exception as e: logger.error(f"Import error: {e}") return jsonify({'error': str(e)}), 500 @app.route('/api/export', methods=['POST']) def api_export(): try: data = request.json solution = data.get('solution', []) stats = data.get('stats', {}) # Flatten solution for Excel rows = [] for stock_idx, stock in enumerate(solution): stock_len = stock['length'] for cut in stock['cuts']: rows.append({ 'Stock Index': stock_idx + 1, 'Stock Length': stock_len, 'Part Length': cut['length'], 'Part Label': cut['label'], 'Waste/Remnant': '' }) # Add remnant row if stock['remaining'] > 0: rows.append({ 'Stock Index': stock_idx + 1, 'Stock Length': stock_len, 'Part Length': '', 'Part Label': 'REMNANT', 'Waste/Remnant': stock['remaining'] }) df = pd.DataFrame(rows) # Summary sheet summary_data = [{k: v for k, v in stats.items()}] df_stats = pd.DataFrame(summary_data) output = io.BytesIO() with pd.ExcelWriter(output, engine='openpyxl') as writer: df.to_excel(writer, sheet_name='Cutting List', index=False) df_stats.to_excel(writer, sheet_name='Statistics', index=False) output.seek(0) return send_file( output, mimetype='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', as_attachment=True, download_name='cutting_optimization_result.xlsx' ) except Exception as e: logger.error(f"Export error: {e}") return jsonify({'error': str(e)}), 500 if __name__ == '__main__': # Use 0.0.0.0 for container/HF spaces app.run(host='0.0.0.0', port=7860)