Spaces:
Sleeping
Sleeping
| 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 | |
| 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 | |
| def index(): | |
| return render_template('index.html') | |
| 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 | |
| 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 | |
| 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) | |