File size: 9,093 Bytes
171dd0a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
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)